diff --git a/src/main/java/io/kamax/mxisd/HttpMxisd.java b/src/main/java/io/kamax/mxisd/HttpMxisd.java index 41eb181..a17cd87 100644 --- a/src/main/java/io/kamax/mxisd/HttpMxisd.java +++ b/src/main/java/io/kamax/mxisd/HttpMxisd.java @@ -91,6 +91,7 @@ public class HttpMxisd { .get(SessionTpidGetValidatedHandler.Path, SaneHandler.around(new SessionTpidGetValidatedHandler(m.getSession()))) .post(SessionTpidBindHandler.Path, SaneHandler.around(new SessionTpidBindHandler(m.getSession(), m.getInvitationManager()))) .post(SessionTpidUnbindHandler.Path, SaneHandler.around(new SessionTpidUnbindHandler(m.getSession()))) + .post(SignEd25519Handler.Path, SaneHandler.around(new SignEd25519Handler(m.getConfig(), m.getInvitationManager(), m.getSign()))) // Profile endpoints .get(ProfileHandler.Path, SaneHandler.around(new ProfileHandler(m.getProfile()))) diff --git a/src/main/java/io/kamax/mxisd/Mxisd.java b/src/main/java/io/kamax/mxisd/Mxisd.java index bdaceca..31f37f9 100644 --- a/src/main/java/io/kamax/mxisd/Mxisd.java +++ b/src/main/java/io/kamax/mxisd/Mxisd.java @@ -106,7 +106,7 @@ public class Mxisd { pMgr = new ProfileManager(ProfileProviders.get(), clientDns, httpClient); notifMgr = new NotificationManager(cfg.getNotification(), NotificationHandlers.get()); sessMgr = new SessionManager(cfg.getSession(), cfg.getMatrix(), store, notifMgr, idStrategy, httpClient); - invMgr = new InvitationManager(cfg, store, idStrategy, signMgr, fedDns, notifMgr); + invMgr = new InvitationManager(cfg, store, idStrategy, keyMgr, signMgr, fedDns, notifMgr); authMgr = new AuthManager(cfg, AuthProviders.get(), idStrategy, invMgr, clientDns, httpClient); dirMgr = new DirectoryManager(cfg.getDirectory(), clientDns, httpClient, DirectoryProviders.get()); asHander = new AppSvcManager(cfg, store, pMgr, notifMgr, synapse); diff --git a/src/main/java/io/kamax/mxisd/exception/ObjectNotFoundException.java b/src/main/java/io/kamax/mxisd/exception/ObjectNotFoundException.java index 81f79f0..3111470 100644 --- a/src/main/java/io/kamax/mxisd/exception/ObjectNotFoundException.java +++ b/src/main/java/io/kamax/mxisd/exception/ObjectNotFoundException.java @@ -22,8 +22,12 @@ package io.kamax.mxisd.exception; public class ObjectNotFoundException extends RuntimeException { + public ObjectNotFoundException(String message) { + super(message); + } + public ObjectNotFoundException(String type, String id) { - super(type + " with ID " + id + " does not exist"); + this(type + " with ID " + id + " does not exist"); } } diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/SignEd25519Handler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/SignEd25519Handler.java new file mode 100644 index 0000000..f76e7f5 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/SignEd25519Handler.java @@ -0,0 +1,75 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sarl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.http.undertow.handler.identity.v1; + +import com.google.gson.JsonObject; +import io.kamax.matrix.MatrixID; +import io.kamax.matrix._MatrixID; +import io.kamax.matrix.json.GsonUtil; +import io.kamax.matrix.json.MatrixJson; +import io.kamax.mxisd.config.MxisdConfig; +import io.kamax.mxisd.http.IsAPIv1; +import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; +import io.kamax.mxisd.invitation.IThreePidInviteReply; +import io.kamax.mxisd.invitation.InvitationManager; +import io.kamax.mxisd.storage.crypto.SignatureManager; +import io.undertow.server.HttpServerExchange; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SignEd25519Handler extends BasicHttpHandler { + + public static final String Path = IsAPIv1.Base + "/sign-ed25519"; + + private static final Logger log = LoggerFactory.getLogger(SignEd25519Handler.class); + + private final MxisdConfig cfg; + private final InvitationManager invMgr; + private final SignatureManager signMgr; + + public SignEd25519Handler(MxisdConfig cfg, InvitationManager invMgr, SignatureManager signMgr) { + this.cfg = cfg; + this.invMgr = invMgr; + this.signMgr = signMgr; + } + + @Override + public void handleRequest(HttpServerExchange exchange) { + JsonObject body = parseJsonObject(exchange); + + _MatrixID mxid = MatrixID.asAcceptable(GsonUtil.getStringOrThrow(body, "mxid")); + String token = GsonUtil.getStringOrThrow(body, "token"); + String privKey = GsonUtil.getStringOrThrow(body, "private_key"); + + IThreePidInviteReply reply = invMgr.getInvite(token, privKey); + _MatrixID sender = reply.getInvite().getSender(); + + JsonObject res = new JsonObject(); + res.addProperty("token", token); + res.addProperty("sender", sender.getId()); + res.addProperty("mxid", mxid.getId()); + res.add("signatures", signMgr.signMessageGson(cfg.getServer().getName(), MatrixJson.encodeCanonical(res))); + + log.info("Signed data for invite using token {}", token); + respondJson(exchange, res); + } + +} diff --git a/src/main/java/io/kamax/mxisd/invitation/IThreePidInviteReply.java b/src/main/java/io/kamax/mxisd/invitation/IThreePidInviteReply.java index b1b561b..00057e6 100644 --- a/src/main/java/io/kamax/mxisd/invitation/IThreePidInviteReply.java +++ b/src/main/java/io/kamax/mxisd/invitation/IThreePidInviteReply.java @@ -20,6 +20,8 @@ package io.kamax.mxisd.invitation; +import java.util.List; + public interface IThreePidInviteReply { String getId(); @@ -30,4 +32,6 @@ public interface IThreePidInviteReply { String getDisplayName(); + List getPublicKeys(); + } diff --git a/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java b/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java index 4aa5150..7dca267 100644 --- a/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java +++ b/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java @@ -30,16 +30,17 @@ import io.kamax.mxisd.config.ServerConfig; import io.kamax.mxisd.dns.FederationDnsOverwrite; import io.kamax.mxisd.exception.BadRequestException; import io.kamax.mxisd.exception.MappingAlreadyExistsException; +import io.kamax.mxisd.exception.ObjectNotFoundException; import io.kamax.mxisd.lookup.SingleLookupReply; import io.kamax.mxisd.lookup.ThreePidMapping; import io.kamax.mxisd.lookup.strategy.LookupStrategy; import io.kamax.mxisd.notification.NotificationManager; import io.kamax.mxisd.storage.IStorage; -import io.kamax.mxisd.storage.crypto.SignatureManager; +import io.kamax.mxisd.storage.crypto.*; import io.kamax.mxisd.storage.ormlite.dao.ThreePidInviteIO; import org.apache.commons.io.IOUtils; -import org.apache.commons.lang.RandomStringUtils; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.conn.ssl.NoopHostnameVerifier; @@ -72,6 +73,7 @@ public class InvitationManager { private ServerConfig srvCfg; private IStorage storage; private LookupStrategy lookupMgr; + private KeyManager keyMgr; private SignatureManager signMgr; private FederationDnsOverwrite dns; private NotificationManager notifMgr; @@ -85,6 +87,7 @@ public class InvitationManager { MxisdConfig mxisdCfg, IStorage storage, LookupStrategy lookupMgr, + KeyManager keyMgr, SignatureManager signMgr, FederationDnsOverwrite dns, NotificationManager notifMgr @@ -93,6 +96,7 @@ public class InvitationManager { this.srvCfg = mxisdCfg.getServer(); this.storage = storage; this.lookupMgr = lookupMgr; + this.keyMgr = keyMgr; this.signMgr = signMgr; this.dns = dns; this.notifMgr = notifMgr; @@ -109,7 +113,7 @@ public class InvitationManager { io.getProperties() ); - ThreePidInviteReply reply = new ThreePidInviteReply(getId(invite), invite, io.getToken(), ""); + ThreePidInviteReply reply = new ThreePidInviteReply(getId(invite), invite, io.getToken(), "", Collections.emptyList()); invitations.put(reply.getId(), reply); }); @@ -221,7 +225,7 @@ public class InvitationManager { log.info("Invite is already pending for {}:{}, returning data", invitation.getMedium(), invitation.getAddress()); if (!StringUtils.equals(invitation.getRoomId(), reply.getInvite().getRoomId())) { log.info("Sending new notification as new invite room {} is different from the original {}", invitation.getRoomId(), reply.getInvite().getRoomId()); - notifMgr.sendForReply(new ThreePidInviteReply(reply.getId(), invitation, reply.getToken(), reply.getDisplayName())); + notifMgr.sendForReply(new ThreePidInviteReply(reply.getId(), invitation, reply.getToken(), reply.getDisplayName(), reply.getPublicKeys())); } else { // FIXME we should check attempt and send if bigger } @@ -236,8 +240,20 @@ public class InvitationManager { String token = RandomStringUtils.randomAlphanumeric(64); String displayName = invitation.getAddress().substring(0, 3) + "..."; + KeyIdentifier pKeyId = keyMgr.getServerSigningKey().getId(); + KeyIdentifier eKeyId = keyMgr.generateKey(KeyType.Ephemeral); - reply = new ThreePidInviteReply(invId, invitation, token, displayName); + String pPubKey = keyMgr.getPublicKeyBase64(pKeyId); + String ePubKey = keyMgr.getPublicKeyBase64(eKeyId); + + invitation.getProperties().put("p_key_algo", pKeyId.getAlgorithm()); + invitation.getProperties().put("p_key_serial", pKeyId.getSerial()); + invitation.getProperties().put("p_key_public", pPubKey); + invitation.getProperties().put("e_key_algo", eKeyId.getAlgorithm()); + invitation.getProperties().put("e_key_serial", eKeyId.getSerial()); + invitation.getProperties().put("e_key_public", ePubKey); + + reply = new ThreePidInviteReply(invId, invitation, token, displayName, Arrays.asList(pPubKey, ePubKey)); log.info("Performing invite to {}:{}", invitation.getMedium(), invitation.getAddress()); notifMgr.sendForReply(reply); @@ -270,6 +286,28 @@ public class InvitationManager { } } + public IThreePidInviteReply getInvite(String token, String privKey) { + for (IThreePidInviteReply reply : invitations.values()) { + if (StringUtils.equals(reply.getToken(), token)) { + String algo = reply.getInvite().getProperties().get("e_key_algo"); + String serial = reply.getInvite().getProperties().get("e_key_serial"); + + if (StringUtils.isAnyBlank(algo, serial)) { + continue; + } + + String storedPrivKey = keyMgr.getKey(new GenericKeyIdentifier(KeyType.Ephemeral, algo, serial)).getPrivateKeyBase64(); + if (!StringUtils.equals(storedPrivKey, privKey)) { + continue; + } + + return reply; + } + } + + throw new ObjectNotFoundException("No invite with such token and/or private key"); + } + private void publishMapping(IThreePidInviteReply reply, String mxid) { String medium = reply.getInvite().getMedium(); String address = reply.getInvite().getAddress(); diff --git a/src/main/java/io/kamax/mxisd/invitation/ThreePidInviteReply.java b/src/main/java/io/kamax/mxisd/invitation/ThreePidInviteReply.java index 04ce9e6..4def1ca 100644 --- a/src/main/java/io/kamax/mxisd/invitation/ThreePidInviteReply.java +++ b/src/main/java/io/kamax/mxisd/invitation/ThreePidInviteReply.java @@ -20,18 +20,24 @@ package io.kamax.mxisd.invitation; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + public class ThreePidInviteReply implements IThreePidInviteReply { private String id; private IThreePidInvite invite; private String token; private String displayName; + private List publicKeys; - public ThreePidInviteReply(String id, IThreePidInvite invite, String token, String displayName) { + public ThreePidInviteReply(String id, IThreePidInvite invite, String token, String displayName, List publicKeys) { this.id = id; this.invite = invite; this.token = token; this.displayName = displayName; + this.publicKeys = Collections.unmodifiableList(new ArrayList<>(publicKeys)); } @Override @@ -54,4 +60,9 @@ public class ThreePidInviteReply implements IThreePidInviteReply { return displayName; } + @Override + public List getPublicKeys() { + return publicKeys; + } + }