From acd8c7d7c5eeded25de4abf4a8468009ba39eec5 Mon Sep 17 00:00:00 2001 From: Max Dor Date: Tue, 12 Feb 2019 15:39:05 +0100 Subject: [PATCH 01/28] Skeleton for full support of all key types --- build.gradle | 2 +- src/main/java/io/kamax/mxisd/HttpMxisd.java | 2 +- src/main/java/io/kamax/mxisd/Mxisd.java | 11 +- .../io/kamax/mxisd/crypto/CryptoFactory.java | 17 +-- .../handler/identity/v1/KeyGetHandler.java | 17 +-- .../identity/v1/RegularKeyIsValidHandler.java | 8 +- .../identity/v1/SingleLookupHandler.java | 10 +- .../identity/v1/StoreInviteHandler.java | 5 +- .../mxisd/invitation/InvitationManager.java | 14 +- .../crypto/Ed2219RegularKeyIdentifier.java | 29 ++++ .../mxisd/storage/crypto/Ed25519Key.java | 53 +++++++ .../storage/crypto/Ed25519KeyManager.java | 140 ++++++++++++++++++ .../crypto/Ed25519SignatureManager.java | 85 +++++++++++ .../mxisd/storage/crypto/FileKeyStore.java | 78 ++++++++++ .../mxisd/storage/crypto/GenericKey.java | 51 +++++++ .../storage/crypto/GenericKeyIdentifier.java | 54 +++++++ .../io/kamax/mxisd/storage/crypto/Key.java | 44 ++++++ .../mxisd/storage/crypto/KeyAlgorithm.java | 27 ++++ .../mxisd/storage/crypto/KeyIdentifier.java | 50 +++++++ .../mxisd/storage/crypto/KeyManager.java | 41 +++++ .../kamax/mxisd/storage/crypto/KeyStore.java | 98 ++++++++++++ .../kamax/mxisd/storage/crypto/KeyType.java | 39 +++++ .../mxisd/storage/crypto/MemoryKeyStore.java | 109 ++++++++++++++ .../storage/crypto/RegularKeyIdentifier.java | 29 ++++ .../kamax/mxisd/storage/crypto/Signature.java | 29 ++++ .../storage/crypto/SignatureManager.java | 57 +++++++ .../mxisd/test/storage/crypto/KeyTest.java | 31 ++++ .../storage/crypto/SignatureManagerTest.java | 102 +++++++++++++ 28 files changed, 1191 insertions(+), 41 deletions(-) create mode 100644 src/main/java/io/kamax/mxisd/storage/crypto/Ed2219RegularKeyIdentifier.java create mode 100644 src/main/java/io/kamax/mxisd/storage/crypto/Ed25519Key.java create mode 100644 src/main/java/io/kamax/mxisd/storage/crypto/Ed25519KeyManager.java create mode 100644 src/main/java/io/kamax/mxisd/storage/crypto/Ed25519SignatureManager.java create mode 100644 src/main/java/io/kamax/mxisd/storage/crypto/FileKeyStore.java create mode 100644 src/main/java/io/kamax/mxisd/storage/crypto/GenericKey.java create mode 100644 src/main/java/io/kamax/mxisd/storage/crypto/GenericKeyIdentifier.java create mode 100644 src/main/java/io/kamax/mxisd/storage/crypto/Key.java create mode 100644 src/main/java/io/kamax/mxisd/storage/crypto/KeyAlgorithm.java create mode 100644 src/main/java/io/kamax/mxisd/storage/crypto/KeyIdentifier.java create mode 100644 src/main/java/io/kamax/mxisd/storage/crypto/KeyManager.java create mode 100644 src/main/java/io/kamax/mxisd/storage/crypto/KeyStore.java create mode 100644 src/main/java/io/kamax/mxisd/storage/crypto/KeyType.java create mode 100644 src/main/java/io/kamax/mxisd/storage/crypto/MemoryKeyStore.java create mode 100644 src/main/java/io/kamax/mxisd/storage/crypto/RegularKeyIdentifier.java create mode 100644 src/main/java/io/kamax/mxisd/storage/crypto/Signature.java create mode 100644 src/main/java/io/kamax/mxisd/storage/crypto/SignatureManager.java create mode 100644 src/test/java/io/kamax/mxisd/test/storage/crypto/KeyTest.java create mode 100644 src/test/java/io/kamax/mxisd/test/storage/crypto/SignatureManagerTest.java diff --git a/build.gradle b/build.gradle index 5c60ca6..2ac687b 100644 --- a/build.gradle +++ b/build.gradle @@ -101,7 +101,7 @@ dependencies { compile 'com.j256.ormlite:ormlite-jdbc:5.0' // ed25519 handling - compile 'net.i2p.crypto:eddsa:0.1.0' + compile 'net.i2p.crypto:eddsa:0.3.0' // LDAP connector compile 'org.apache.directory.api:api-all:1.0.0' diff --git a/src/main/java/io/kamax/mxisd/HttpMxisd.java b/src/main/java/io/kamax/mxisd/HttpMxisd.java index 83ce897..5534d35 100644 --- a/src/main/java/io/kamax/mxisd/HttpMxisd.java +++ b/src/main/java/io/kamax/mxisd/HttpMxisd.java @@ -82,7 +82,7 @@ public class HttpMxisd { // Identity endpoints .get(HelloHandler.Path, helloHandler) .get(HelloHandler.Path + "/", helloHandler) // Be lax with possibly trailing slash - .get(SingleLookupHandler.Path, SaneHandler.around(new SingleLookupHandler(m.getIdentity(), m.getSign()))) + .get(SingleLookupHandler.Path, SaneHandler.around(new SingleLookupHandler(m.getConfig(), m.getIdentity(), m.getSign()))) .post(BulkLookupHandler.Path, SaneHandler.around(new BulkLookupHandler(m.getIdentity()))) .post(StoreInviteHandler.Path, storeInvHandler) .post(SessionStartHandler.Path, SaneHandler.around(new SessionStartHandler(m.getSession()))) diff --git a/src/main/java/io/kamax/mxisd/Mxisd.java b/src/main/java/io/kamax/mxisd/Mxisd.java index 547c445..bdaceca 100644 --- a/src/main/java/io/kamax/mxisd/Mxisd.java +++ b/src/main/java/io/kamax/mxisd/Mxisd.java @@ -20,8 +20,6 @@ package io.kamax.mxisd; -import io.kamax.matrix.crypto.KeyManager; -import io.kamax.matrix.crypto.SignatureManager; import io.kamax.mxisd.as.AppSvcManager; import io.kamax.mxisd.auth.AuthManager; import io.kamax.mxisd.auth.AuthProviders; @@ -48,6 +46,9 @@ import io.kamax.mxisd.profile.ProfileManager; import io.kamax.mxisd.profile.ProfileProviders; import io.kamax.mxisd.session.SessionManager; import io.kamax.mxisd.storage.IStorage; +import io.kamax.mxisd.storage.crypto.Ed25519KeyManager; +import io.kamax.mxisd.storage.crypto.KeyManager; +import io.kamax.mxisd.storage.crypto.SignatureManager; import io.kamax.mxisd.storage.ormlite.OrmLiteSqlStorage; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; @@ -63,7 +64,7 @@ public class Mxisd { private IStorage store; - private KeyManager keyMgr; + private Ed25519KeyManager keyMgr; private SignatureManager signMgr; // Features @@ -92,7 +93,7 @@ public class Mxisd { store = new OrmLiteSqlStorage(cfg); keyMgr = CryptoFactory.getKeyManager(cfg.getKey()); - signMgr = CryptoFactory.getSignatureManager(keyMgr, cfg.getServer()); + signMgr = CryptoFactory.getSignatureManager(keyMgr); ClientDnsOverwrite clientDns = new ClientDnsOverwrite(cfg.getDns().getOverwrite()); FederationDnsOverwrite fedDns = new FederationDnsOverwrite(cfg.getDns().getOverwrite()); Synapse synapse = new Synapse(cfg.getSynapseSql()); @@ -105,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.getInvite(), store, idStrategy, signMgr, fedDns, notifMgr); + invMgr = new InvitationManager(cfg, store, idStrategy, 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/crypto/CryptoFactory.java b/src/main/java/io/kamax/mxisd/crypto/CryptoFactory.java index d52c233..38f1263 100644 --- a/src/main/java/io/kamax/mxisd/crypto/CryptoFactory.java +++ b/src/main/java/io/kamax/mxisd/crypto/CryptoFactory.java @@ -20,9 +20,8 @@ package io.kamax.mxisd.crypto; -import io.kamax.matrix.crypto.*; import io.kamax.mxisd.config.KeyConfig; -import io.kamax.mxisd.config.ServerConfig; +import io.kamax.mxisd.storage.crypto.*; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; @@ -31,10 +30,10 @@ import java.io.IOException; public class CryptoFactory { - public static KeyManager getKeyManager(KeyConfig keyCfg) { - _KeyStore store; + public static Ed25519KeyManager getKeyManager(KeyConfig keyCfg) { + KeyStore store; if (StringUtils.equals(":memory:", keyCfg.getPath())) { - store = new KeyMemoryStore(); + store = new MemoryKeyStore(); } else { File keyStore = new File(keyCfg.getPath()); if (!keyStore.exists()) { @@ -45,14 +44,14 @@ public class CryptoFactory { } } - store = new KeyFileStore(keyCfg.getPath()); + store = new FileKeyStore(keyCfg.getPath()); } - return new KeyManager(store); + return new Ed25519KeyManager(store); } - public static SignatureManager getSignatureManager(KeyManager keyMgr, ServerConfig cfg) { - return new SignatureManager(keyMgr, cfg.getName()); + public static SignatureManager getSignatureManager(Ed25519KeyManager keyMgr) { + return new Ed25519SignatureManager(keyMgr); } } diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/KeyGetHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/KeyGetHandler.java index 1dc8228..fcc9015 100644 --- a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/KeyGetHandler.java +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/KeyGetHandler.java @@ -21,10 +21,11 @@ package io.kamax.mxisd.http.undertow.handler.identity.v1; import com.google.gson.JsonObject; -import io.kamax.matrix.crypto.KeyManager; -import io.kamax.mxisd.exception.BadRequestException; import io.kamax.mxisd.http.IsAPIv1; import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; +import io.kamax.mxisd.storage.crypto.GenericKeyIdentifier; +import io.kamax.mxisd.storage.crypto.KeyManager; +import io.kamax.mxisd.storage.crypto.KeyType; import io.undertow.server.HttpServerExchange; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,16 +47,12 @@ public class KeyGetHandler extends BasicHttpHandler { public void handleRequest(HttpServerExchange exchange) { String key = getQueryParameter(exchange, Key); String[] v = key.split(":", 2); - String keyType = v[0]; - int keyId = Integer.parseInt(v[1]); + String keyAlgo = v[0]; + String keyId = v[1]; - if (!"ed25519".contentEquals(keyType)) { - throw new BadRequestException("Invalid algorithm: " + keyType); - } - - log.info("Key {}:{} was requested", keyType, keyId); + log.info("Key {}:{} was requested", keyAlgo, keyId); JsonObject obj = new JsonObject(); - obj.addProperty("public_key", mgr.getPublicKeyBase64(keyId)); + obj.addProperty("public_key", mgr.getPublicKeyBase64(new GenericKeyIdentifier(KeyType.Regular, keyAlgo, keyId))); respond(exchange, obj); } diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/RegularKeyIsValidHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/RegularKeyIsValidHandler.java index 171c2b0..628d805 100644 --- a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/RegularKeyIsValidHandler.java +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/RegularKeyIsValidHandler.java @@ -20,10 +20,10 @@ package io.kamax.mxisd.http.undertow.handler.identity.v1; -import io.kamax.matrix.crypto.KeyManager; import io.kamax.mxisd.http.IsAPIv1; +import io.kamax.mxisd.storage.crypto.KeyManager; +import io.kamax.mxisd.storage.crypto.KeyType; import io.undertow.server.HttpServerExchange; -import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,9 +44,7 @@ public class RegularKeyIsValidHandler extends KeyIsValidHandler { String pubKey = getQueryParameter(exchange, "public_key"); log.info("Validating public key {}", pubKey); - // TODO do in manager - boolean valid = StringUtils.equals(pubKey, mgr.getPublicKeyBase64(mgr.getCurrentIndex())); - respondJson(exchange, valid ? validKey : invalidKey); + respondJson(exchange, mgr.isValid(KeyType.Regular, pubKey) ? validKey : invalidKey); } } diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/SingleLookupHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/SingleLookupHandler.java index e341057..8702b5d 100644 --- a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/SingleLookupHandler.java +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/SingleLookupHandler.java @@ -21,15 +21,17 @@ package io.kamax.mxisd.http.undertow.handler.identity.v1; import com.google.gson.JsonObject; -import io.kamax.matrix.crypto.SignatureManager; import io.kamax.matrix.event.EventKey; import io.kamax.matrix.json.GsonUtil; import io.kamax.matrix.json.MatrixJson; +import io.kamax.mxisd.config.MxisdConfig; +import io.kamax.mxisd.config.ServerConfig; import io.kamax.mxisd.http.IsAPIv1; import io.kamax.mxisd.http.io.identity.SingeLookupReplyJson; import io.kamax.mxisd.lookup.SingleLookupReply; import io.kamax.mxisd.lookup.SingleLookupRequest; import io.kamax.mxisd.lookup.strategy.LookupStrategy; +import io.kamax.mxisd.storage.crypto.SignatureManager; import io.undertow.server.HttpServerExchange; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,10 +44,12 @@ public class SingleLookupHandler extends LookupHandler { private transient final Logger log = LoggerFactory.getLogger(SingleLookupHandler.class); + private ServerConfig cfg; private LookupStrategy strategy; private SignatureManager signMgr; - public SingleLookupHandler(LookupStrategy strategy, SignatureManager signMgr) { + public SingleLookupHandler(MxisdConfig cfg, LookupStrategy strategy, SignatureManager signMgr) { + this.cfg = cfg.getServer(); this.strategy = strategy; this.signMgr = signMgr; } @@ -72,7 +76,7 @@ public class SingleLookupHandler extends LookupHandler { // FIXME signing should be done in the business model, not in the controller JsonObject obj = GsonUtil.makeObj(new SingeLookupReplyJson(lookup)); - obj.add(EventKey.Signatures.get(), signMgr.signMessageGson(MatrixJson.encodeCanonical(obj))); + obj.add(EventKey.Signatures.get(), signMgr.signMessageGson(cfg.getName(), MatrixJson.encodeCanonical(obj))); respondJson(exchange, obj); } diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/StoreInviteHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/StoreInviteHandler.java index 7131a19..1d22ab4 100644 --- a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/StoreInviteHandler.java +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/StoreInviteHandler.java @@ -24,7 +24,6 @@ import com.google.gson.JsonObject; import com.google.gson.reflect.TypeToken; import io.kamax.matrix.MatrixID; import io.kamax.matrix._MatrixID; -import io.kamax.matrix.crypto.KeyManager; import io.kamax.matrix.json.GsonUtil; import io.kamax.mxisd.config.ServerConfig; import io.kamax.mxisd.exception.BadRequestException; @@ -36,6 +35,7 @@ import io.kamax.mxisd.invitation.IThreePidInvite; import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.invitation.InvitationManager; import io.kamax.mxisd.invitation.ThreePidInvite; +import io.kamax.mxisd.storage.crypto.KeyManager; import io.undertow.server.HttpServerExchange; import io.undertow.util.QueryParameterUtils; import org.apache.commons.lang3.StringUtils; @@ -96,7 +96,8 @@ public class StoreInviteHandler extends BasicHttpHandler { IThreePidInvite invite = new ThreePidInvite(sender, inv.getMedium(), inv.getAddress(), inv.getRoomId(), parameters); IThreePidInviteReply reply = invMgr.storeInvite(invite); - respondJson(exchange, new ThreePidInviteReplyIO(reply, keyMgr.getPublicKeyBase64(keyMgr.getCurrentIndex()), cfg.getPublicUrl())); + // FIXME the key info must be set by the invitation manager in the reply object! + respondJson(exchange, new ThreePidInviteReplyIO(reply, keyMgr.getPublicKeyBase64(keyMgr.getServerSigningKey().getId()), cfg.getPublicUrl())); } } diff --git a/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java b/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java index 6cc8fb4..4aa5150 100644 --- a/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java +++ b/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java @@ -23,9 +23,10 @@ package io.kamax.mxisd.invitation; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import io.kamax.matrix.MatrixID; -import io.kamax.matrix.crypto.SignatureManager; import io.kamax.matrix.json.GsonUtil; import io.kamax.mxisd.config.InvitationConfig; +import io.kamax.mxisd.config.MxisdConfig; +import io.kamax.mxisd.config.ServerConfig; import io.kamax.mxisd.dns.FederationDnsOverwrite; import io.kamax.mxisd.exception.BadRequestException; import io.kamax.mxisd.exception.MappingAlreadyExistsException; @@ -34,6 +35,7 @@ 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.ormlite.dao.ThreePidInviteIO; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.RandomStringUtils; @@ -67,6 +69,7 @@ public class InvitationManager { private transient final Logger log = LoggerFactory.getLogger(InvitationManager.class); private InvitationConfig cfg; + private ServerConfig srvCfg; private IStorage storage; private LookupStrategy lookupMgr; private SignatureManager signMgr; @@ -79,14 +82,15 @@ public class InvitationManager { private Map invitations = new ConcurrentHashMap<>(); public InvitationManager( - InvitationConfig cfg, + MxisdConfig mxisdCfg, IStorage storage, LookupStrategy lookupMgr, SignatureManager signMgr, FederationDnsOverwrite dns, NotificationManager notifMgr ) { - this.cfg = cfg; + this.cfg = mxisdCfg.getInvite(); + this.srvCfg = mxisdCfg.getServer(); this.storage = storage; this.lookupMgr = lookupMgr; this.signMgr = signMgr; @@ -280,7 +284,7 @@ public class InvitationManager { JsonObject obj = new JsonObject(); obj.addProperty("mxid", mxid); obj.addProperty("token", reply.getToken()); - obj.add("signatures", signMgr.signMessageGson(obj.toString())); + obj.add("signatures", signMgr.signMessageGson(srvCfg.getName(), obj.toString())); JsonObject objUp = new JsonObject(); objUp.addProperty("mxid", mxid); @@ -298,7 +302,7 @@ public class InvitationManager { content.addProperty("address", address); content.addProperty("mxid", mxid); - content.add("signatures", signMgr.signMessageGson(content.toString())); + content.add("signatures", signMgr.signMessageGson(srvCfg.getName(), content.toString())); StringEntity entity = new StringEntity(content.toString(), StandardCharsets.UTF_8); entity.setContentType("application/json"); diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/Ed2219RegularKeyIdentifier.java b/src/main/java/io/kamax/mxisd/storage/crypto/Ed2219RegularKeyIdentifier.java new file mode 100644 index 0000000..091728a --- /dev/null +++ b/src/main/java/io/kamax/mxisd/storage/crypto/Ed2219RegularKeyIdentifier.java @@ -0,0 +1,29 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * 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.storage.crypto; + +public class Ed2219RegularKeyIdentifier extends RegularKeyIdentifier { + + public Ed2219RegularKeyIdentifier(String serial) { + super(KeyAlgorithm.Ed25519, serial); + } + +} diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/Ed25519Key.java b/src/main/java/io/kamax/mxisd/storage/crypto/Ed25519Key.java new file mode 100644 index 0000000..af31552 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/storage/crypto/Ed25519Key.java @@ -0,0 +1,53 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * 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.storage.crypto; + +public class Ed25519Key implements Key { + + private KeyIdentifier id; + private String privKey; + + public Ed25519Key(KeyIdentifier id, String privKey) { + if (!KeyAlgorithm.Ed25519.equals(id.getAlgorithm())) { + throw new IllegalArgumentException(); + } + + this.id = new GenericKeyIdentifier(id); + this.privKey = privKey; + } + + + @Override + public KeyIdentifier getId() { + return id; + } + + @Override + public boolean isValid() { + return true; + } + + @Override + public String getPrivateKeyBase64() { + return privKey; + } + +} diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/Ed25519KeyManager.java b/src/main/java/io/kamax/mxisd/storage/crypto/Ed25519KeyManager.java new file mode 100644 index 0000000..d70eb73 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/storage/crypto/Ed25519KeyManager.java @@ -0,0 +1,140 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * 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.storage.crypto; + +import io.kamax.matrix.codec.MxBase64; +import net.i2p.crypto.eddsa.EdDSAPrivateKey; +import net.i2p.crypto.eddsa.EdDSAPublicKey; +import net.i2p.crypto.eddsa.KeyPairGenerator; +import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable; +import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec; +import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec; +import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.security.KeyPair; +import java.time.Instant; +import java.util.List; + +public class Ed25519KeyManager implements KeyManager { + + private static final Logger log = LoggerFactory.getLogger(Ed25519KeyManager.class); + + private final EdDSAParameterSpec keySpecs; + private final KeyStore store; + + public Ed25519KeyManager(KeyStore store) { + this.keySpecs = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519); + this.store = store; + + if (!store.getCurrentKey().isPresent()) { + List keys = store.list(KeyType.Regular); + if (keys.isEmpty()) { + keys.add(generateKey(KeyType.Regular)); + } + + store.setCurrentKey(keys.get(0)); + } + } + + protected String generateId() { + ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES); + buffer.putLong(Instant.now().toEpochMilli() - 1546297200000L); // TS since 2019-01-01T00:00:00Z to keep IDs short + return Base64.encodeBase64URLSafeString(buffer.array()) + RandomStringUtils.randomAlphanumeric(1); + } + + protected String getPrivateKeyBase64(EdDSAPrivateKey key) { + return MxBase64.encode(key.getSeed()); + } + + public EdDSAParameterSpec getKeySpecs() { + return keySpecs; + } + + @Override + public KeyIdentifier generateKey(KeyType type) { + KeyIdentifier id; + do { + id = new GenericKeyIdentifier(type, KeyAlgorithm.Ed25519, generateId()); + } while (store.has(id)); + + KeyPair pair = (new KeyPairGenerator()).generateKeyPair(); + String keyEncoded = getPrivateKeyBase64((EdDSAPrivateKey) pair.getPrivate()); + + Key key = new GenericKey(id, true, keyEncoded); + store.add(key); + + return id; + } + + @Override + public List getKeys(KeyType type) { + return store.list(type); + } + + @Override + public Key getServerSigningKey() { + return store.get(store.getCurrentKey().orElseThrow(IllegalStateException::new)); + } + + @Override + public Key getKey(KeyIdentifier id) { + return store.get(id); + } + + public EdDSAPrivateKeySpec getPrivateKeySpecs(KeyIdentifier id) { + return new EdDSAPrivateKeySpec(java.util.Base64.getDecoder().decode(getKey(id).getPrivateKeyBase64()), keySpecs); + } + + public EdDSAPrivateKey getPrivateKey(KeyIdentifier id) { + return new EdDSAPrivateKey(getPrivateKeySpecs(id)); + } + + public EdDSAPublicKey getPublicKey(KeyIdentifier id) { + EdDSAPrivateKeySpec privKeySpec = getPrivateKeySpecs(id); + EdDSAPublicKeySpec pubKeySpec = new EdDSAPublicKeySpec(privKeySpec.getA(), keySpecs); + return new EdDSAPublicKey(pubKeySpec); + } + + @Override + public void disableKey(KeyIdentifier id) { + Key key = store.get(id); + key = new GenericKey(id, false, key.getPrivateKeyBase64()); + store.update(key); + } + + @Override + public String getPublicKeyBase64(KeyIdentifier id) { + return MxBase64.encode(getPublicKey(id).getAbyte()); + } + + @Override + public boolean isValid(KeyType type, String publicKeyBase64) { + // TODO caching? + return getKeys(type).stream().anyMatch(id -> StringUtils.equals(getPublicKeyBase64(id), publicKeyBase64)); + } + +} diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/Ed25519SignatureManager.java b/src/main/java/io/kamax/mxisd/storage/crypto/Ed25519SignatureManager.java new file mode 100644 index 0000000..c5b0d50 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/storage/crypto/Ed25519SignatureManager.java @@ -0,0 +1,85 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * 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.storage.crypto; + +import com.google.gson.JsonObject; +import io.kamax.matrix.codec.MxBase64; +import io.kamax.matrix.json.MatrixJson; +import net.i2p.crypto.eddsa.EdDSAEngine; + +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; + +public class Ed25519SignatureManager implements SignatureManager { + + private final Ed25519KeyManager keyMgr; + + public Ed25519SignatureManager(Ed25519KeyManager keyMgr) { + this.keyMgr = keyMgr; + } + + @Override + public JsonObject signMessageGson(String domain, String message) { + Signature sign = sign(message); + + JsonObject keySignature = new JsonObject(); + // FIXME should create a signing key object what would give this ed and index values + keySignature.addProperty(sign.getKey().getAlgorithm() + ":" + sign.getKey().getSerial(), sign.getSignature()); + JsonObject signature = new JsonObject(); + signature.add(domain, keySignature); + + return signature; + } + + @Override + public Signature sign(JsonObject obj) { + + return sign(MatrixJson.encodeCanonical(obj)); + } + + @Override + public Signature sign(byte[] data) { + try { + KeyIdentifier signingKeyId = keyMgr.getServerSigningKey().getId(); + EdDSAEngine signEngine = new EdDSAEngine(MessageDigest.getInstance(keyMgr.getKeySpecs().getHashAlgorithm())); + signEngine.initSign(keyMgr.getPrivateKey(signingKeyId)); + byte[] signRaw = signEngine.signOneShot(data); + String sign = MxBase64.encode(signRaw); + + return new Signature() { + @Override + public KeyIdentifier getKey() { + return signingKeyId; + } + + @Override + public String getSignature() { + return sign; + } + }; + } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/FileKeyStore.java b/src/main/java/io/kamax/mxisd/storage/crypto/FileKeyStore.java new file mode 100644 index 0000000..78c3151 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/storage/crypto/FileKeyStore.java @@ -0,0 +1,78 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * 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.storage.crypto; + +import io.kamax.mxisd.exception.ObjectNotFoundException; + +import java.util.List; +import java.util.Optional; + +public class FileKeyStore implements KeyStore { + + public FileKeyStore(String path) { + } + + @Override + public boolean has(KeyIdentifier id) { + return false; + } + + @Override + public List list() { + return null; + } + + @Override + public List list(KeyType type) { + return null; + } + + @Override + public Key get(KeyIdentifier id) throws ObjectNotFoundException { + return null; + } + + @Override + public void add(Key key) throws IllegalStateException { + + } + + @Override + public void update(Key key) throws ObjectNotFoundException { + + } + + @Override + public void delete(KeyIdentifier id) throws ObjectNotFoundException { + + } + + @Override + public void setCurrentKey(KeyIdentifier id) throws IllegalArgumentException { + + } + + @Override + public Optional getCurrentKey() { + return Optional.empty(); + } + +} diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/GenericKey.java b/src/main/java/io/kamax/mxisd/storage/crypto/GenericKey.java new file mode 100644 index 0000000..7d59079 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/storage/crypto/GenericKey.java @@ -0,0 +1,51 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * 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.storage.crypto; + +public class GenericKey implements Key { + + private final KeyIdentifier id; + private final boolean isValid; + private final String privKey; + + public GenericKey(KeyIdentifier id, boolean isValid, String privKey) { + this.id = new GenericKeyIdentifier(id); + this.isValid = isValid; + this.privKey = privKey; + } + + + @Override + public KeyIdentifier getId() { + return id; + } + + @Override + public boolean isValid() { + return isValid; + } + + @Override + public String getPrivateKeyBase64() { + return privKey; + } + +} diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/GenericKeyIdentifier.java b/src/main/java/io/kamax/mxisd/storage/crypto/GenericKeyIdentifier.java new file mode 100644 index 0000000..0390eda --- /dev/null +++ b/src/main/java/io/kamax/mxisd/storage/crypto/GenericKeyIdentifier.java @@ -0,0 +1,54 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * 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.storage.crypto; + +public class GenericKeyIdentifier implements KeyIdentifier { + + private final KeyType type; + private final String algo; + private final String serial; + + public GenericKeyIdentifier(KeyIdentifier id) { + this(id.getType(), id.getAlgorithm(), id.getSerial()); + } + + public GenericKeyIdentifier(KeyType type, String algo, String serial) { + this.type = type; + this.algo = algo; + this.serial = serial; + } + + @Override + public KeyType getType() { + return type; + } + + @Override + public String getAlgorithm() { + return algo; + } + + @Override + public String getSerial() { + return serial; + } + +} diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/Key.java b/src/main/java/io/kamax/mxisd/storage/crypto/Key.java new file mode 100644 index 0000000..9a98917 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/storage/crypto/Key.java @@ -0,0 +1,44 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * 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.storage.crypto; + +/** + * A signing key + */ +public interface Key { + + KeyIdentifier getId(); + + /** + * If the key is currently valid + * + * @return true if the key is valid, false if not + */ + boolean isValid(); + + /** + * Get the private key + * + * @return the private key encoded as Base64 + */ + String getPrivateKeyBase64(); + +} diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/KeyAlgorithm.java b/src/main/java/io/kamax/mxisd/storage/crypto/KeyAlgorithm.java new file mode 100644 index 0000000..bb453ee --- /dev/null +++ b/src/main/java/io/kamax/mxisd/storage/crypto/KeyAlgorithm.java @@ -0,0 +1,27 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * 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.storage.crypto; + +public interface KeyAlgorithm { + + String Ed25519 = "ed25519"; + +} diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/KeyIdentifier.java b/src/main/java/io/kamax/mxisd/storage/crypto/KeyIdentifier.java new file mode 100644 index 0000000..7ec89da --- /dev/null +++ b/src/main/java/io/kamax/mxisd/storage/crypto/KeyIdentifier.java @@ -0,0 +1,50 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * 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.storage.crypto; + +/** + * Identifying data for a given Key. + */ +public interface KeyIdentifier { + + /** + * Type of key. + * + * @return The type of the key + */ + KeyType getType(); + + /** + * Algorithm of the key. Typically ed25519. + * + * @return The algorithm of the key + */ + String getAlgorithm(); + + /** + * Serial of the key, unique for the algorithm. + * It is typically made of random alphanumerical characters. + * + * @return The serial of the key + */ + String getSerial(); + +} diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/KeyManager.java b/src/main/java/io/kamax/mxisd/storage/crypto/KeyManager.java new file mode 100644 index 0000000..68a2cbc --- /dev/null +++ b/src/main/java/io/kamax/mxisd/storage/crypto/KeyManager.java @@ -0,0 +1,41 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * 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.storage.crypto; + +import java.util.List; + +public interface KeyManager { + + KeyIdentifier generateKey(KeyType type); + + List getKeys(KeyType type); + + Key getServerSigningKey(); + + Key getKey(KeyIdentifier id); + + void disableKey(KeyIdentifier id); + + String getPublicKeyBase64(KeyIdentifier id); + + boolean isValid(KeyType type, String publicKeyBase64); + +} diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/KeyStore.java b/src/main/java/io/kamax/mxisd/storage/crypto/KeyStore.java new file mode 100644 index 0000000..72c24b3 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/storage/crypto/KeyStore.java @@ -0,0 +1,98 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * 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.storage.crypto; + +import io.kamax.mxisd.exception.ObjectNotFoundException; + +import java.util.List; +import java.util.Optional; + +/** + * Store to persist signing keys and the identifier for the current long-term signing key + */ +public interface KeyStore { + + /** + * If a given key is currently stored + * + * @param id The Identifier elements for the key + * @return true if the key is stored, false if not + */ + boolean has(KeyIdentifier id); + + /** + * List all keys within the store + * + * @return The list of key identifiers + */ + List list(); + + /** + * List all keys of a given type within the store + * + * @param type The type to filter on + * @return The list of keys identifiers matching the given type + */ + List list(KeyType type); + + /** + * Get the key that relates to the given identifier + * + * @param id The identifier of the key to get + * @return The key + * @throws ObjectNotFoundException If no key is found for that identifier + */ + Key get(KeyIdentifier id) throws ObjectNotFoundException; + + /** + * Add a key to the store + * + * @param key The key to store + * @throws IllegalStateException If a key already exist for the given identifier data + */ + void add(Key key) throws IllegalStateException; + + void update(Key key) throws ObjectNotFoundException; + + /** + * Delete a key from the store + * + * @param id The key identifier of the key to delete + * @throws ObjectNotFoundException If no key is found for that identifier + */ + void delete(KeyIdentifier id) throws ObjectNotFoundException; + + /** + * Store the information of which key is the current signing key + * + * @param id The key identifier + * @throws ObjectNotFoundException If the key is not known to the store + */ + void setCurrentKey(KeyIdentifier id) throws ObjectNotFoundException; + + /** + * Retrieve the previously stored information of which key is the current signing key, if any + * + * @return The optional key identifier that was previously stored + */ + Optional getCurrentKey(); + +} diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/KeyType.java b/src/main/java/io/kamax/mxisd/storage/crypto/KeyType.java new file mode 100644 index 0000000..84565f6 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/storage/crypto/KeyType.java @@ -0,0 +1,39 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * 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.storage.crypto; + +/** + * Types of keys used by an Identity server. + * See https://matrix.org/docs/spec/identity_service/r0.1.0.html#key-management + */ +public enum KeyType { + + /** + * Ephemeral keys are related to 3PID invites and are only valid while the invite is pending. + */ + Ephemeral, + + /** + * Regular keys are used by the Identity Server itself to sign requests/responses + */ + Regular + +} diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/MemoryKeyStore.java b/src/main/java/io/kamax/mxisd/storage/crypto/MemoryKeyStore.java new file mode 100644 index 0000000..d2d8419 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/storage/crypto/MemoryKeyStore.java @@ -0,0 +1,109 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * 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.storage.crypto; + +import io.kamax.mxisd.exception.ObjectNotFoundException; +import org.apache.commons.lang3.StringUtils; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class MemoryKeyStore implements KeyStore { + + private Map>> keys = new ConcurrentHashMap<>(); + private KeyIdentifier current; + + private Map getMap(KeyType type, String algo) { + return keys.computeIfAbsent(type, k -> new ConcurrentHashMap<>()).computeIfAbsent(algo, k -> new ConcurrentHashMap<>()); + } + + @Override + public boolean has(KeyIdentifier id) { + return getMap(id.getType(), id.getAlgorithm()).containsKey(id.getSerial()); + } + + @Override + public List list() { + List keyIds = new ArrayList<>(); + keys.forEach((key, value) -> value.forEach((key1, value1) -> value1.forEach((key2, value2) -> keyIds.add(new GenericKeyIdentifier(key, key1, key2))))); + return keyIds; + } + + @Override + public List list(KeyType type) { + List keyIds = new ArrayList<>(); + keys.computeIfAbsent(type, t -> new ConcurrentHashMap<>()).forEach((key, value) -> value.forEach((key1, value1) -> keyIds.add(new GenericKeyIdentifier(type, key, key1)))); + return keyIds; + } + + @Override + public Key get(KeyIdentifier id) throws ObjectNotFoundException { + String data = getMap(id.getType(), id.getAlgorithm()).get(id.getSerial()); + if (Objects.isNull(data)) { + throw new ObjectNotFoundException("Key", id.getType() + ":" + id.getAlgorithm() + ":" + id.getSerial()); + } + + return new GenericKey(new GenericKeyIdentifier(id), StringUtils.isEmpty(data), data); + } + + private void set(Key key) { + String data = key.isValid() ? key.getPrivateKeyBase64() : ""; + getMap(key.getId().getType(), key.getId().getAlgorithm()).put(key.getId().getSerial(), data); + } + + @Override + public void add(Key key) throws IllegalStateException { + if (has(key.getId())) { + throw new IllegalStateException(); + } + + set(key); + } + + @Override + public void update(Key key) throws ObjectNotFoundException { + if (!has(key.getId())) { + throw new ObjectNotFoundException("Key", key.getId().getType() + ":" + key.getId().getAlgorithm() + ":" + key.getId().getSerial()); + } + + set(key); + } + + @Override + public void delete(KeyIdentifier id) throws ObjectNotFoundException { + keys.computeIfAbsent(id.getType(), k -> new ConcurrentHashMap<>()).computeIfAbsent(id.getAlgorithm(), k -> new ConcurrentHashMap<>()).remove(id.getSerial()); + } + + @Override + public void setCurrentKey(KeyIdentifier id) throws ObjectNotFoundException { + if (!has(id)) { + throw new ObjectNotFoundException("Key", id.getType() + ":" + id.getAlgorithm() + ":" + id.getSerial()); + } + + current = id; + } + + @Override + public Optional getCurrentKey() { + return Optional.ofNullable(current); + } + +} diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/RegularKeyIdentifier.java b/src/main/java/io/kamax/mxisd/storage/crypto/RegularKeyIdentifier.java new file mode 100644 index 0000000..b1b8721 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/storage/crypto/RegularKeyIdentifier.java @@ -0,0 +1,29 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * 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.storage.crypto; + +public class RegularKeyIdentifier extends GenericKeyIdentifier { + + public RegularKeyIdentifier(String algo, String serial) { + super(KeyType.Regular, algo, serial); + } + +} diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/Signature.java b/src/main/java/io/kamax/mxisd/storage/crypto/Signature.java new file mode 100644 index 0000000..9174449 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/storage/crypto/Signature.java @@ -0,0 +1,29 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * 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.storage.crypto; + +public interface Signature { + + KeyIdentifier getKey(); + + String getSignature(); + +} diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/SignatureManager.java b/src/main/java/io/kamax/mxisd/storage/crypto/SignatureManager.java new file mode 100644 index 0000000..316cb2c --- /dev/null +++ b/src/main/java/io/kamax/mxisd/storage/crypto/SignatureManager.java @@ -0,0 +1,57 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * 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.storage.crypto; + +import com.google.gson.JsonObject; + +import java.nio.charset.StandardCharsets; + +public interface SignatureManager { + + JsonObject signMessageGson(String domain, String message); + + /** + * Sign the canonical form of a JSON object + * + * @param obj The JSON object to canonicalize and sign + * @return The signature + */ + Signature sign(JsonObject obj); + + /** + * Sign the message, using UTF-8 as decoding character set + * + * @param message The UTF-8 encoded message + * @return + */ + default Signature sign(String message) { + return sign(message.getBytes(StandardCharsets.UTF_8)); + } + + /** + * Sign the data + * + * @param data The data to sign + * @return The signature + */ + Signature sign(byte[] data); + +} diff --git a/src/test/java/io/kamax/mxisd/test/storage/crypto/KeyTest.java b/src/test/java/io/kamax/mxisd/test/storage/crypto/KeyTest.java new file mode 100644 index 0000000..4c5a365 --- /dev/null +++ b/src/test/java/io/kamax/mxisd/test/storage/crypto/KeyTest.java @@ -0,0 +1,31 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * 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.test.storage.crypto; + +public class KeyTest { + + // As per https://matrix.org/docs/spec/appendices.html#signing-key + public static final String Private = "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA1"; + + // The corresponding public key, not being documented in the spec + public static final String Public = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; + +} diff --git a/src/test/java/io/kamax/mxisd/test/storage/crypto/SignatureManagerTest.java b/src/test/java/io/kamax/mxisd/test/storage/crypto/SignatureManagerTest.java new file mode 100644 index 0000000..9c4a3ac --- /dev/null +++ b/src/test/java/io/kamax/mxisd/test/storage/crypto/SignatureManagerTest.java @@ -0,0 +1,102 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * 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.test.storage.crypto; + +import com.google.gson.JsonObject; +import io.kamax.matrix.json.GsonUtil; +import io.kamax.matrix.json.MatrixJson; +import io.kamax.mxisd.storage.crypto.*; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertThat; + +public class SignatureManagerTest { + + private static SignatureManager signMgr; + + private static SignatureManager build(String keySeed) { + Ed25519Key key = new Ed25519Key(new Ed2219RegularKeyIdentifier("0"), keySeed); + KeyStore store = new MemoryKeyStore(); + store.add(key); + + return new Ed25519SignatureManager(new Ed25519KeyManager(store)); + } + + @BeforeClass + public static void beforeClass() { + signMgr = build(KeyTest.Private); + } + + private void testSign(String value, String sign) { + assertThat(signMgr.sign(value).getSignature(), is(equalTo(sign))); + } + + // As per https://matrix.org/docs/spec/appendices.html#json-signing + @Test + public void onEmptyObject() { + String value = "{}"; + String sign = "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"; + + testSign(value, sign); + } + + // As per https://matrix.org/docs/spec/appendices.html#json-signing + @Test + public void onSimpleObject() { + JsonObject data = new JsonObject(); + data.addProperty("one", 1); + data.addProperty("two", "Two"); + + String value = GsonUtil.get().toJson(data); + String sign = "KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"; + + testSign(value, sign); + } + + @Test + public void onFederationHeader() { + SignatureManager mgr = build("1QblgjFeL3IxoY4DKOR7p5mL5sQTC0ChmeMJlqb4d5M"); + + JsonObject o = new JsonObject(); + o.addProperty("method", "GET"); + o.addProperty("uri", "/_matrix/federation/v1/query/directory?room_alias=%23a%3Amxhsd.local.kamax.io%3A8447"); + o.addProperty("origin", "synapse.local.kamax.io"); + o.addProperty("destination", "mxhsd.local.kamax.io:8447"); + + String signExpected = "SEMGSOJEsoalrBfHqPO2QrSlbLaUYLHLk4e3q4IJ2JbgvCynT1onp7QF1U4Sl3G3NzybrgdnVvpqcaEgV0WPCw"; + Signature signProduced = mgr.sign(o); + assertThat(signProduced.getSignature(), is(equalTo(signExpected))); + } + + @Test + public void onIdentityLookup() { + String value = MatrixJson.encodeCanonical("{\n" + " \"address\": \"mxisd-federation-test@kamax.io\",\n" + + " \"medium\": \"email\",\n" + " \"mxid\": \"@mxisd-lookup-test:kamax.io\",\n" + + " \"not_after\": 253402300799000,\n" + " \"not_before\": 0,\n" + " \"ts\": 1523482030147\n" + "}"); + + String sign = "ObKA4PNQh2g6c7Yo2QcTcuDgIwhknG7ZfqmNYzbhrbLBOqZomU22xX9raufN2Y3ke1FXsDqsGs7WBDodmzZJCg"; + testSign(value, sign); + } + +} From 91e5e08e7068d69f3e69d77c44b6d8c22be93638 Mon Sep 17 00:00:00 2001 From: Max Dor Date: Wed, 13 Feb 2019 02:05:28 +0100 Subject: [PATCH 02/28] Support for all key types --- src/main/java/io/kamax/mxisd/HttpMxisd.java | 2 +- .../v1/EphemeralKeyIsValidHandler.java | 14 +- .../handler/identity/v1/KeyGetHandler.java | 9 +- .../identity/v1/RegularKeyIsValidHandler.java | 3 +- .../mxisd/storage/crypto/FileKeyJson.java | 61 ++++++ .../mxisd/storage/crypto/FileKeyStore.java | 181 +++++++++++++++++- .../mxisd/storage/crypto/KeyIdentifier.java | 4 + 7 files changed, 263 insertions(+), 11 deletions(-) create mode 100644 src/main/java/io/kamax/mxisd/storage/crypto/FileKeyJson.java diff --git a/src/main/java/io/kamax/mxisd/HttpMxisd.java b/src/main/java/io/kamax/mxisd/HttpMxisd.java index 5534d35..41eb181 100644 --- a/src/main/java/io/kamax/mxisd/HttpMxisd.java +++ b/src/main/java/io/kamax/mxisd/HttpMxisd.java @@ -77,7 +77,7 @@ public class HttpMxisd { // Key endpoints .get(KeyGetHandler.Path, SaneHandler.around(new KeyGetHandler(m.getKeyManager()))) .get(RegularKeyIsValidHandler.Path, SaneHandler.around(new RegularKeyIsValidHandler(m.getKeyManager()))) - .get(EphemeralKeyIsValidHandler.Path, SaneHandler.around(new EphemeralKeyIsValidHandler())) + .get(EphemeralKeyIsValidHandler.Path, SaneHandler.around(new EphemeralKeyIsValidHandler(m.getKeyManager()))) // Identity endpoints .get(HelloHandler.Path, helloHandler) diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/EphemeralKeyIsValidHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/EphemeralKeyIsValidHandler.java index eaa802c..d39ccc1 100644 --- a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/EphemeralKeyIsValidHandler.java +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/EphemeralKeyIsValidHandler.java @@ -21,6 +21,8 @@ package io.kamax.mxisd.http.undertow.handler.identity.v1; import io.kamax.mxisd.http.IsAPIv1; +import io.kamax.mxisd.storage.crypto.KeyManager; +import io.kamax.mxisd.storage.crypto.KeyType; import io.undertow.server.HttpServerExchange; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,11 +33,19 @@ public class EphemeralKeyIsValidHandler extends KeyIsValidHandler { private transient final Logger log = LoggerFactory.getLogger(EphemeralKeyIsValidHandler.class); + private KeyManager mgr; + + public EphemeralKeyIsValidHandler(KeyManager mgr) { + this.mgr = mgr; + } + @Override public void handleRequest(HttpServerExchange exchange) { - log.warn("Ephemeral key was requested but no ephemeral key are generated, replying not valid"); + // FIXME process + correctly in query parameter handling + String pubKey = getQueryParameter(exchange, "public_key").replace(" ", "+"); + log.info("Validating ephemeral public key {}", pubKey); - respondJson(exchange, invalidKey); + respondJson(exchange, mgr.isValid(KeyType.Ephemeral, pubKey) ? validKey : invalidKey); } } diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/KeyGetHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/KeyGetHandler.java index fcc9015..4a1ac69 100644 --- a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/KeyGetHandler.java +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/KeyGetHandler.java @@ -27,13 +27,14 @@ import io.kamax.mxisd.storage.crypto.GenericKeyIdentifier; import io.kamax.mxisd.storage.crypto.KeyManager; import io.kamax.mxisd.storage.crypto.KeyType; import io.undertow.server.HttpServerExchange; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class KeyGetHandler extends BasicHttpHandler { public static final String Key = "key"; - public static final String Path = IsAPIv1.Base + "/pubkey/{key}"; + public static final String Path = IsAPIv1.Base + "/pubkey/{" + Key + "}"; private transient final Logger log = LoggerFactory.getLogger(KeyGetHandler.class); @@ -46,7 +47,11 @@ public class KeyGetHandler extends BasicHttpHandler { @Override public void handleRequest(HttpServerExchange exchange) { String key = getQueryParameter(exchange, Key); - String[] v = key.split(":", 2); + if (StringUtils.isBlank(key)) { + throw new IllegalArgumentException("Key ID cannot be empty or blank"); + } + + String[] v = key.split(":", 2); // Maybe use regex? String keyAlgo = v[0]; String keyId = v[1]; diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/RegularKeyIsValidHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/RegularKeyIsValidHandler.java index 628d805..f181ed9 100644 --- a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/RegularKeyIsValidHandler.java +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/RegularKeyIsValidHandler.java @@ -41,7 +41,8 @@ public class RegularKeyIsValidHandler extends KeyIsValidHandler { @Override public void handleRequest(HttpServerExchange exchange) { - String pubKey = getQueryParameter(exchange, "public_key"); + // FIXME process + correctly in query parameter handling + String pubKey = getQueryParameter(exchange, "public_key").replace(" ", "+"); log.info("Validating public key {}", pubKey); respondJson(exchange, mgr.isValid(KeyType.Regular, pubKey) ? validKey : invalidKey); diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/FileKeyJson.java b/src/main/java/io/kamax/mxisd/storage/crypto/FileKeyJson.java new file mode 100644 index 0000000..15164e9 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/storage/crypto/FileKeyJson.java @@ -0,0 +1,61 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * 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.storage.crypto; + +public class FileKeyJson { + + public static FileKeyJson get(Key key) { + FileKeyJson json = new FileKeyJson(); + json.setVersion("0"); + json.setKey(key.getPrivateKeyBase64()); + json.setValid(key.isValid()); + return json; + } + + private String version; + private boolean isValid; + private String key; + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public boolean isValid() { + return isValid; + } + + public void setValid(boolean valid) { + isValid = valid; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + +} diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/FileKeyStore.java b/src/main/java/io/kamax/mxisd/storage/crypto/FileKeyStore.java index 78c3151..7d77541 100644 --- a/src/main/java/io/kamax/mxisd/storage/crypto/FileKeyStore.java +++ b/src/main/java/io/kamax/mxisd/storage/crypto/FileKeyStore.java @@ -20,59 +20,230 @@ package io.kamax.mxisd.storage.crypto; +import com.google.gson.JsonObject; +import io.kamax.matrix.crypto.KeyFileStore; +import io.kamax.matrix.json.GsonUtil; import io.kamax.mxisd.exception.ObjectNotFoundException; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Optional; public class FileKeyStore implements KeyStore { + private static final Logger log = LoggerFactory.getLogger(FileKeyStore.class); + + private final String currentFilename = "current"; + private final String base; + public FileKeyStore(String path) { + base = new File(path).getAbsoluteFile().toString(); + File f = new File(base); + + if (!f.exists()) { + if (!f.mkdir()) { + throw new RuntimeException("Unable to create key store at " + f.toString()); + } + } else { + if (!f.isFile()) { + log.debug("Key store is already in directory format"); + } else { + try { + log.info("Found old key store format, migrating..."); + File oldStorePath = new File(f.toString() + ".backup-before-migration"); + FileUtils.moveFile(f, oldStorePath); + FileUtils.forceMkdir(f); + + + String privKey = new KeyFileStore(oldStorePath.toString()).load().orElse(""); + if (StringUtils.isBlank(privKey)) { + throw new IllegalStateException("Signing key file is empty. Either fix or delete"); + } else { + // We ensure this is valid Base64 data before migrating + Base64.decodeBase64(privKey); + + // We store the new key + add(new GenericKey(new GenericKeyIdentifier(KeyType.Regular, KeyAlgorithm.Ed25519, "0"), true, privKey)); + + log.info("Store migrated to new directory format"); + } + } catch (IOException e) { + throw new RuntimeException("Unable to migrate store from old single file format to new directory format", e); + } + } + } + + if (!f.isDirectory()) { + throw new RuntimeException("Key store path is not a directory: " + f.toString()); + } + } + + private String toDirName(KeyType type) { + return type.name().toLowerCase(); + } + + private Path ensureDirExists(KeyIdentifier id) { + File b = Paths.get(base, toDirName(id.getType()), id.getAlgorithm()).toFile(); + + if (b.exists()) { + if (!b.isDirectory()) { + throw new RuntimeException("Key store path already exists but is not a directory: " + b.toString()); + } + } else { + try { + FileUtils.forceMkdir(b); + } catch (IOException e) { + throw new RuntimeException("Unable to create key store path at " + b.toString(), e); + } + } + + return b.toPath(); } @Override public boolean has(KeyIdentifier id) { - return false; + return Paths.get(base, toDirName(id.getType()), id.getAlgorithm(), id.getSerial()).toFile().isFile(); } @Override public List list() { - return null; + List keyIds = new ArrayList<>(); + + for (KeyType type : KeyType.values()) { + keyIds.addAll(list(type)); + } + + return keyIds; } @Override public List list(KeyType type) { - return null; + List keyIds = new ArrayList<>(); + + File algoDir = Paths.get(base, toDirName(type)).toFile(); + File[] algos = algoDir.listFiles(); + if (Objects.isNull(algos)) { + throw new IllegalStateException("Cannot list stored key algorithms: was expecting " + algoDir.toString() + " to be a directory"); + } + + for (File algo : algos) { + File[] serials = algo.listFiles(); + if (Objects.isNull(serials)) { + throw new IllegalStateException("Cannot list stored key serials: was expecting " + algo.toString() + " to be a directory"); + } + + for (File serial : serials) { + keyIds.add(new GenericKeyIdentifier(type, algo.getName(), serial.getName())); + } + } + + return keyIds; } @Override public Key get(KeyIdentifier id) throws ObjectNotFoundException { - return null; + File keyFile = ensureDirExists(id).resolve(id.getSerial()).toFile(); + if (!keyFile.exists() || !keyFile.isFile()) { + throw new ObjectNotFoundException("Key", id.getId()); + } + + try (FileInputStream keyIs = new FileInputStream(keyFile)) { + FileKeyJson json = GsonUtil.get().fromJson(IOUtils.toString(keyIs, StandardCharsets.UTF_8), FileKeyJson.class); + return new GenericKey(id, json.isValid(), json.getKey()); + } catch (IOException e) { + throw new RuntimeException("Unable to read key " + id.getId(), e); + } } @Override public void add(Key key) throws IllegalStateException { + File keyFile = ensureDirExists(key.getId()).resolve(key.getId().getSerial()).toFile(); + if (keyFile.exists()) { + throw new IllegalStateException("Key " + key.getId().getId() + " already exists"); + } + FileKeyJson json = FileKeyJson.get(key); + try (FileOutputStream keyOs = new FileOutputStream(keyFile, false)) { + IOUtils.write(GsonUtil.get().toJson(json), keyOs, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException("Unable to create key " + key.getId().getId(), e); + } } @Override public void update(Key key) throws ObjectNotFoundException { + File keyFile = ensureDirExists(key.getId()).resolve(key.getId().getSerial()).toFile(); + if (!keyFile.exists() || !keyFile.isFile()) { + throw new ObjectNotFoundException("Key", key.getId().getId()); + } + FileKeyJson json = FileKeyJson.get(key); + try (FileOutputStream keyOs = new FileOutputStream(keyFile, false)) { + IOUtils.write(GsonUtil.get().toJson(json), keyOs, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException("Unable to create key " + key.getId().getId(), e); + } } @Override public void delete(KeyIdentifier id) throws ObjectNotFoundException { + File keyFile = ensureDirExists(id).resolve(id.getSerial()).toFile(); + if (!keyFile.exists() || !keyFile.isFile()) { + throw new ObjectNotFoundException("Key", id.getId()); + } + if (!keyFile.delete()) { + throw new RuntimeException("Unable to delete key " + id.getId()); + } } @Override public void setCurrentKey(KeyIdentifier id) throws IllegalArgumentException { + JsonObject json = new JsonObject(); + json.addProperty("type", id.getType().name()); + json.addProperty("algo", id.getAlgorithm()); + json.addProperty("serial", id.getSerial()); + File f = Paths.get(base, currentFilename).toFile(); + + try (FileOutputStream keyOs = new FileOutputStream(f, false)) { + IOUtils.write(GsonUtil.get().toJson(json), keyOs, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException("Unable to write to " + f.toString(), e); + } } @Override public Optional getCurrentKey() { - return Optional.empty(); + File f = Paths.get(base, currentFilename).toFile(); + if (!f.exists()) { + return Optional.empty(); + } + + if (!f.isFile()) { + throw new IllegalStateException("Current key file is not a file: " + f.toString()); + } + + try (FileInputStream keyIs = new FileInputStream(f)) { + JsonObject json = GsonUtil.parseObj(IOUtils.toString(keyIs, StandardCharsets.UTF_8)); + return Optional.of(new GenericKeyIdentifier(KeyType.valueOf(GsonUtil.getStringOrThrow(json, "type")), GsonUtil.getStringOrThrow(json, "algo"), GsonUtil.getStringOrThrow(json, "serial"))); + } catch (IOException e) { + throw new RuntimeException("Unable to read " + f.toString(), e); + } } } diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/KeyIdentifier.java b/src/main/java/io/kamax/mxisd/storage/crypto/KeyIdentifier.java index 7ec89da..db28f25 100644 --- a/src/main/java/io/kamax/mxisd/storage/crypto/KeyIdentifier.java +++ b/src/main/java/io/kamax/mxisd/storage/crypto/KeyIdentifier.java @@ -47,4 +47,8 @@ public interface KeyIdentifier { */ String getSerial(); + default String getId() { + return getAlgorithm().toLowerCase() + ":" + getSerial(); + } + } From f3b528d1bae635ffbe64dc51e7267cad0b0ffe7e Mon Sep 17 00:00:00 2001 From: Max Dor Date: Wed, 13 Feb 2019 04:14:30 +0100 Subject: [PATCH 03/28] Store ephemeral key in invite and add support for /sign-ed25519 --- src/main/java/io/kamax/mxisd/HttpMxisd.java | 1 + src/main/java/io/kamax/mxisd/Mxisd.java | 2 +- .../exception/ObjectNotFoundException.java | 6 +- .../identity/v1/SignEd25519Handler.java | 75 +++++++++++++++++++ .../invitation/IThreePidInviteReply.java | 4 + .../mxisd/invitation/InvitationManager.java | 50 +++++++++++-- .../mxisd/invitation/ThreePidInviteReply.java | 13 +++- 7 files changed, 142 insertions(+), 9 deletions(-) create mode 100644 src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/SignEd25519Handler.java 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; + } + } From 77dc75d383f94dbb5a7f631ea0eb7b2f8028daf4 Mon Sep 17 00:00:00 2001 From: Max Dor Date: Wed, 13 Feb 2019 17:18:44 +0100 Subject: [PATCH 04/28] Basic check for pending invite when requesting token on registration --- src/main/java/io/kamax/mxisd/HttpMxisd.java | 4 + src/main/java/io/kamax/mxisd/Mxisd.java | 14 ++- .../v1/Register3pidRequestTokenHandler.java | 101 +++++++++++++++++ .../mxisd/invitation/InvitationManager.java | 17 +++ .../registration/RegistrationManager.java | 102 ++++++++++++++++++ .../mxisd/registration/RegistrationReply.java | 46 ++++++++ 6 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 src/main/java/io/kamax/mxisd/http/undertow/handler/register/v1/Register3pidRequestTokenHandler.java create mode 100644 src/main/java/io/kamax/mxisd/registration/RegistrationManager.java create mode 100644 src/main/java/io/kamax/mxisd/registration/RegistrationReply.java diff --git a/src/main/java/io/kamax/mxisd/HttpMxisd.java b/src/main/java/io/kamax/mxisd/HttpMxisd.java index a17cd87..362db5d 100644 --- a/src/main/java/io/kamax/mxisd/HttpMxisd.java +++ b/src/main/java/io/kamax/mxisd/HttpMxisd.java @@ -33,6 +33,7 @@ import io.kamax.mxisd.http.undertow.handler.directory.v1.UserDirectorySearchHand import io.kamax.mxisd.http.undertow.handler.identity.v1.*; import io.kamax.mxisd.http.undertow.handler.profile.v1.InternalProfileHandler; import io.kamax.mxisd.http.undertow.handler.profile.v1.ProfileHandler; +import io.kamax.mxisd.http.undertow.handler.register.v1.Register3pidRequestTokenHandler; import io.kamax.mxisd.http.undertow.handler.status.StatusHandler; import io.undertow.Handlers; import io.undertow.Undertow; @@ -97,6 +98,9 @@ public class HttpMxisd { .get(ProfileHandler.Path, SaneHandler.around(new ProfileHandler(m.getProfile()))) .get(InternalProfileHandler.Path, SaneHandler.around(new InternalProfileHandler(m.getProfile()))) + // Registration endpoints + .post(Register3pidRequestTokenHandler.Path, SaneHandler.around(new Register3pidRequestTokenHandler(m.getReg(), m.getClientDns(), m.getHttpClient()))) + // Application Service endpoints .get("/_matrix/app/v1/users/**", asNotFoundHandler) .get("/users/**", asNotFoundHandler) // Legacy endpoint diff --git a/src/main/java/io/kamax/mxisd/Mxisd.java b/src/main/java/io/kamax/mxisd/Mxisd.java index 31f37f9..ea9c3eb 100644 --- a/src/main/java/io/kamax/mxisd/Mxisd.java +++ b/src/main/java/io/kamax/mxisd/Mxisd.java @@ -44,6 +44,7 @@ import io.kamax.mxisd.notification.NotificationHandlers; import io.kamax.mxisd.notification.NotificationManager; import io.kamax.mxisd.profile.ProfileManager; import io.kamax.mxisd.profile.ProfileProviders; +import io.kamax.mxisd.registration.RegistrationManager; import io.kamax.mxisd.session.SessionManager; import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.crypto.Ed25519KeyManager; @@ -66,6 +67,7 @@ public class Mxisd { private Ed25519KeyManager keyMgr; private SignatureManager signMgr; + private ClientDnsOverwrite clientDns; // Features private AuthManager authMgr; @@ -76,6 +78,7 @@ public class Mxisd { private AppSvcManager asHander; private SessionManager sessMgr; private NotificationManager notifMgr; + private RegistrationManager regMgr; public Mxisd(MxisdConfig cfg) { this.cfg = cfg.build(); @@ -94,7 +97,7 @@ public class Mxisd { store = new OrmLiteSqlStorage(cfg); keyMgr = CryptoFactory.getKeyManager(cfg.getKey()); signMgr = CryptoFactory.getSignatureManager(keyMgr); - ClientDnsOverwrite clientDns = new ClientDnsOverwrite(cfg.getDns().getOverwrite()); + clientDns = new ClientDnsOverwrite(cfg.getDns().getOverwrite()); FederationDnsOverwrite fedDns = new FederationDnsOverwrite(cfg.getDns().getOverwrite()); Synapse synapse = new Synapse(cfg.getSynapseSql()); BridgeFetcher bridgeFetcher = new BridgeFetcher(cfg.getLookup().getRecursive().getBridge(), srvFetcher); @@ -109,6 +112,7 @@ public class Mxisd { 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()); + regMgr = new RegistrationManager(httpClient, clientDns, idStrategy, invMgr); asHander = new AppSvcManager(cfg, store, pMgr, notifMgr, synapse); } @@ -120,6 +124,10 @@ public class Mxisd { return httpClient; } + public ClientDnsOverwrite getClientDns() { + return clientDns; + } + public IRemoteIdentityServerFetcher getServerFetcher() { return srvFetcher; } @@ -156,6 +164,10 @@ public class Mxisd { return signMgr; } + public RegistrationManager getReg() { + return regMgr; + } + public AppSvcManager getAs() { return asHander; } diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/register/v1/Register3pidRequestTokenHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/register/v1/Register3pidRequestTokenHandler.java new file mode 100644 index 0000000..af2dbcb --- /dev/null +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/register/v1/Register3pidRequestTokenHandler.java @@ -0,0 +1,101 @@ +/* + * 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.register.v1; + +import com.google.gson.JsonObject; +import io.kamax.matrix.ThreePid; +import io.kamax.matrix.ThreePidMedium; +import io.kamax.matrix.json.GsonUtil; +import io.kamax.mxisd.dns.ClientDnsOverwrite; +import io.kamax.mxisd.exception.InternalServerError; +import io.kamax.mxisd.exception.NotAllowedException; +import io.kamax.mxisd.http.io.identity.SessionEmailTokenRequestJson; +import io.kamax.mxisd.http.io.identity.SessionPhoneTokenRequestJson; +import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; +import io.kamax.mxisd.registration.RegistrationManager; +import io.kamax.mxisd.util.RestClientUtils; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.HttpString; +import org.apache.http.Header; +import org.apache.http.HeaderElement; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; + +public class Register3pidRequestTokenHandler extends BasicHttpHandler { + + public static final String Key = "medium"; + public static final String Path = "/_matrix/client/r0/register/{" + Key + "}/requestToken"; + + private static final Logger log = LoggerFactory.getLogger(Register3pidRequestTokenHandler.class); + + private final RegistrationManager mgr; + private final ClientDnsOverwrite dns; + private final CloseableHttpClient client; + + public Register3pidRequestTokenHandler(RegistrationManager mgr, ClientDnsOverwrite dns, CloseableHttpClient client) { + this.mgr = mgr; + this.dns = dns; // FIXME this shouldn't be in here but in the manager + this.client = client; // FIXME this shouldn't be in here but in the manager + } + + @Override + public void handleRequest(HttpServerExchange exchange) { + JsonObject body = parseJsonObject(exchange); + + String medium = getPathVariable(exchange, Key); + String address = GsonUtil.findString(body, "address").orElse(""); + if (ThreePidMedium.Email.is(medium)) { + address = GsonUtil.get().fromJson(body, SessionEmailTokenRequestJson.class).getValue(); + } else if (ThreePidMedium.PhoneNumber.is(medium)) { + address = GsonUtil.get().fromJson(body, SessionPhoneTokenRequestJson.class).getValue(); + } else { + log.warn("Unsupported 3PID medium. We attempted to extract the address but the call might fail"); + } + + ThreePid tpid = new ThreePid(medium, address); + if (!mgr.allow(tpid)) { + throw new NotAllowedException("Your " + medium + " address cannot be used for registration"); + } + + String target = dns.transform(URI.create(exchange.getRequestURL())).toString(); + log.info("Requesting remote: {}", target); + HttpPost req = RestClientUtils.post(target, GsonUtil.get(), body); + try (CloseableHttpResponse res = client.execute(req)) { + exchange.setStatusCode(res.getStatusLine().getStatusCode()); + for (Header h : res.getAllHeaders()) { + for (HeaderElement el : h.getElements()) { + exchange.getResponseHeaders().add(HttpString.tryFromString(h.getName()), el.getValue()); + } + } + res.getEntity().writeTo(exchange.getOutputStream()); + exchange.endExchange(); + } catch (IOException e) { + throw new InternalServerError(e); + } + } + +} diff --git a/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java b/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java index 7dca267..36938f3 100644 --- a/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java +++ b/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java @@ -23,6 +23,7 @@ package io.kamax.mxisd.invitation; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import io.kamax.matrix.MatrixID; +import io.kamax.matrix.ThreePid; import io.kamax.matrix.json.GsonUtil; import io.kamax.mxisd.config.InvitationConfig; import io.kamax.mxisd.config.MxisdConfig; @@ -266,6 +267,22 @@ public class InvitationManager { return reply; } + public boolean hasInvite(ThreePid tpid) { + for (IThreePidInviteReply reply : invitations.values()) { + if (!StringUtils.equals(tpid.getMedium(), reply.getInvite().getMedium())) { + continue; + } + + if (!StringUtils.equals(tpid.getAddress(), reply.getInvite().getAddress())) { + continue; + } + + return true; + } + + return false; + } + public void lookupMappingsForInvites() { if (!invitations.isEmpty()) { log.info("Checking for existing mapping for pending invites"); diff --git a/src/main/java/io/kamax/mxisd/registration/RegistrationManager.java b/src/main/java/io/kamax/mxisd/registration/RegistrationManager.java new file mode 100644 index 0000000..63ecbc9 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/registration/RegistrationManager.java @@ -0,0 +1,102 @@ +/* + * 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.registration; + +import com.google.gson.JsonObject; +import io.kamax.matrix.ThreePid; +import io.kamax.matrix.json.GsonUtil; +import io.kamax.mxisd.dns.ClientDnsOverwrite; +import io.kamax.mxisd.exception.NotImplementedException; +import io.kamax.mxisd.exception.RemoteHomeServerException; +import io.kamax.mxisd.invitation.InvitationManager; +import io.kamax.mxisd.lookup.strategy.LookupStrategy; +import io.kamax.mxisd.util.RestClientUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class RegistrationManager { + + private static final Logger log = LoggerFactory.getLogger(RegistrationManager.class); + + private final CloseableHttpClient client; + private final ClientDnsOverwrite dns; + private final LookupStrategy lookup; + private final InvitationManager invMgr; + + private Map sessions = new ConcurrentHashMap<>(); + + public RegistrationManager(CloseableHttpClient client, ClientDnsOverwrite dns, LookupStrategy lookup, InvitationManager invMgr) { + this.client = client; + this.dns = dns; + this.lookup = lookup; + this.invMgr = invMgr; + } + + private String resolveProxyUrl(URI target) { + URIBuilder builder = dns.transform(target); + String urlToLogin = builder.toString(); + log.info("Proxy resolution: {} to {}", target.toString(), urlToLogin); + return urlToLogin; + } + + public RegistrationReply execute(URI target, JsonObject request) { + HttpPost registerProxyRq = RestClientUtils.post(resolveProxyUrl(target), GsonUtil.get(), request); + try (CloseableHttpResponse response = client.execute(registerProxyRq)) { + int status = response.getStatusLine().getStatusCode(); + if (status == 200) { + // The user managed to register. We check if it had a session + String sessionId = GsonUtil.findObj(request, "auth").flatMap(auth -> GsonUtil.findString(auth, "session")).orElse(""); + if (StringUtils.isEmpty(sessionId)) { + // No session ID was provided. This is an edge case we do not support for now as investigation is needed + // to ensure how and when this happens. + + HttpPost newSessReq = RestClientUtils.post(resolveProxyUrl(target), GsonUtil.get(), new JsonObject()); + try (CloseableHttpResponse newSessRes = client.execute(newSessReq)) { + RegistrationReply reply = new RegistrationReply(); + reply.setStatus(newSessRes.getStatusLine().getStatusCode()); + reply.setBody(GsonUtil.parseObj(EntityUtils.toString(newSessRes.getEntity()))); + return reply; + } + } + } + + throw new NotImplementedException("Registration"); + } catch (IOException e) { + throw new RemoteHomeServerException(e.getMessage()); + } + } + + public boolean allow(ThreePid tpid) { + return invMgr.hasInvite(tpid); + } + +} diff --git a/src/main/java/io/kamax/mxisd/registration/RegistrationReply.java b/src/main/java/io/kamax/mxisd/registration/RegistrationReply.java new file mode 100644 index 0000000..b2e2dde --- /dev/null +++ b/src/main/java/io/kamax/mxisd/registration/RegistrationReply.java @@ -0,0 +1,46 @@ +/* + * 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.registration; + +import com.google.gson.JsonObject; + +public class RegistrationReply { + + private int status; + private JsonObject body; + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public JsonObject getBody() { + return body; + } + + public void setBody(JsonObject body) { + this.body = body; + } + +} From 2f7e5e4025fadf4e1a3ddca6a1852946a4e694c3 Mon Sep 17 00:00:00 2001 From: Max Dor Date: Wed, 13 Feb 2019 23:11:01 +0100 Subject: [PATCH 05/28] Fix migration in case of empty dir --- .../mxisd/storage/crypto/FileKeyStore.java | 54 +++++++++---------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/FileKeyStore.java b/src/main/java/io/kamax/mxisd/storage/crypto/FileKeyStore.java index 7d77541..025353d 100644 --- a/src/main/java/io/kamax/mxisd/storage/crypto/FileKeyStore.java +++ b/src/main/java/io/kamax/mxisd/storage/crypto/FileKeyStore.java @@ -54,37 +54,31 @@ public class FileKeyStore implements KeyStore { base = new File(path).getAbsoluteFile().toString(); File f = new File(base); - if (!f.exists()) { - if (!f.mkdir()) { - throw new RuntimeException("Unable to create key store at " + f.toString()); + if (f.exists() && f.isFile()) { + try { + log.info("Found old key store format at {}, migrating...", base); + File oldStorePath = new File(f.toString() + ".backup-before-migration"); + FileUtils.moveFile(f, oldStorePath); + FileUtils.forceMkdir(f); + + + String privKey = new KeyFileStore(oldStorePath.toString()).load().orElse(""); + if (StringUtils.isBlank(privKey)) { + log.info("Empty file, nothing to migrate"); + } else { + // We ensure this is valid Base64 data before migrating + Base64.decodeBase64(privKey); + + // We store the new key + add(new GenericKey(new GenericKeyIdentifier(KeyType.Regular, KeyAlgorithm.Ed25519, "0"), true, privKey)); + + log.info("Store migrated to new directory format"); + } + } catch (IOException e) { + throw new RuntimeException("Unable to migrate store from old single file format to new directory format", e); } } else { - if (!f.isFile()) { - log.debug("Key store is already in directory format"); - } else { - try { - log.info("Found old key store format, migrating..."); - File oldStorePath = new File(f.toString() + ".backup-before-migration"); - FileUtils.moveFile(f, oldStorePath); - FileUtils.forceMkdir(f); - - - String privKey = new KeyFileStore(oldStorePath.toString()).load().orElse(""); - if (StringUtils.isBlank(privKey)) { - throw new IllegalStateException("Signing key file is empty. Either fix or delete"); - } else { - // We ensure this is valid Base64 data before migrating - Base64.decodeBase64(privKey); - - // We store the new key - add(new GenericKey(new GenericKeyIdentifier(KeyType.Regular, KeyAlgorithm.Ed25519, "0"), true, privKey)); - - log.info("Store migrated to new directory format"); - } - } catch (IOException e) { - throw new RuntimeException("Unable to migrate store from old single file format to new directory format", e); - } - } + log.info("Key store is already in directory format"); } if (!f.isDirectory()) { @@ -137,7 +131,7 @@ public class FileKeyStore implements KeyStore { File algoDir = Paths.get(base, toDirName(type)).toFile(); File[] algos = algoDir.listFiles(); if (Objects.isNull(algos)) { - throw new IllegalStateException("Cannot list stored key algorithms: was expecting " + algoDir.toString() + " to be a directory"); + return keyIds; } for (File algo : algos) { From aadfae2965351cf03e0a255e85092be49390de5b Mon Sep 17 00:00:00 2001 From: Max Dor Date: Thu, 14 Feb 2019 23:02:55 +0100 Subject: [PATCH 06/28] Skeleton for invitation policies (#130) --- src/main/java/io/kamax/mxisd/HttpMxisd.java | 4 + src/main/java/io/kamax/mxisd/Mxisd.java | 2 +- .../mxisd/backend/sql/SqlProfileProvider.java | 40 ++++++- .../backend/sql/synapse/SynapseQueries.java | 4 + .../sql/synapse/SynapseSqlStoreSupplier.java | 5 + .../kamax/mxisd/config/InvitationConfig.java | 45 +++++++- .../io/kamax/mxisd/config/sql/SqlConfig.java | 39 ++++++- .../sql/synapse/SynapseSqlProviderConfig.java | 9 ++ .../undertow/handler/BasicHttpHandler.java | 55 +++++++++- .../handler/invite/v1/RoomInviteHandler.java | 103 ++++++++++++++++++ .../v1/Register3pidRequestTokenHandler.java | 28 +---- .../mxisd/invitation/InvitationManager.java | 31 +++++- .../registration/RegistrationManager.java | 2 +- 13 files changed, 326 insertions(+), 41 deletions(-) create mode 100644 src/main/java/io/kamax/mxisd/http/undertow/handler/invite/v1/RoomInviteHandler.java diff --git a/src/main/java/io/kamax/mxisd/HttpMxisd.java b/src/main/java/io/kamax/mxisd/HttpMxisd.java index 362db5d..22683c8 100644 --- a/src/main/java/io/kamax/mxisd/HttpMxisd.java +++ b/src/main/java/io/kamax/mxisd/HttpMxisd.java @@ -31,6 +31,7 @@ import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginHandler; import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginPostHandler; import io.kamax.mxisd.http.undertow.handler.directory.v1.UserDirectorySearchHandler; import io.kamax.mxisd.http.undertow.handler.identity.v1.*; +import io.kamax.mxisd.http.undertow.handler.invite.v1.RoomInviteHandler; import io.kamax.mxisd.http.undertow.handler.profile.v1.InternalProfileHandler; import io.kamax.mxisd.http.undertow.handler.profile.v1.ProfileHandler; import io.kamax.mxisd.http.undertow.handler.register.v1.Register3pidRequestTokenHandler; @@ -101,6 +102,9 @@ public class HttpMxisd { // Registration endpoints .post(Register3pidRequestTokenHandler.Path, SaneHandler.around(new Register3pidRequestTokenHandler(m.getReg(), m.getClientDns(), m.getHttpClient()))) + // Invite endpoints + .post(RoomInviteHandler.Path, SaneHandler.around(new RoomInviteHandler(m.getHttpClient(), m.getClientDns(), m.getInvitationManager()))) + // Application Service endpoints .get("/_matrix/app/v1/users/**", asNotFoundHandler) .get("/users/**", asNotFoundHandler) // Legacy endpoint diff --git a/src/main/java/io/kamax/mxisd/Mxisd.java b/src/main/java/io/kamax/mxisd/Mxisd.java index ea9c3eb..7c0f82a 100644 --- a/src/main/java/io/kamax/mxisd/Mxisd.java +++ b/src/main/java/io/kamax/mxisd/Mxisd.java @@ -109,7 +109,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, keyMgr, signMgr, fedDns, notifMgr); + invMgr = new InvitationManager(cfg, store, idStrategy, keyMgr, signMgr, fedDns, notifMgr, pMgr); authMgr = new AuthManager(cfg, AuthProviders.get(), idStrategy, invMgr, clientDns, httpClient); dirMgr = new DirectoryManager(cfg.getDirectory(), clientDns, httpClient, DirectoryProviders.get()); regMgr = new RegistrationManager(httpClient, clientDns, idStrategy, invMgr); diff --git a/src/main/java/io/kamax/mxisd/backend/sql/SqlProfileProvider.java b/src/main/java/io/kamax/mxisd/backend/sql/SqlProfileProvider.java index bf208c1..1adaf52 100644 --- a/src/main/java/io/kamax/mxisd/backend/sql/SqlProfileProvider.java +++ b/src/main/java/io/kamax/mxisd/backend/sql/SqlProfileProvider.java @@ -23,7 +23,9 @@ package io.kamax.mxisd.backend.sql; import io.kamax.matrix.ThreePid; import io.kamax.matrix._MatrixID; import io.kamax.matrix._ThreePid; +import io.kamax.mxisd.UserIdType; import io.kamax.mxisd.config.sql.SqlConfig; +import io.kamax.mxisd.exception.InternalServerError; import io.kamax.mxisd.profile.ProfileProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,16 +35,14 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Optional; public abstract class SqlProfileProvider implements ProfileProvider { - private transient final Logger log = LoggerFactory.getLogger(SqlProfileProvider.class); + private static final Logger log = LoggerFactory.getLogger(SqlProfileProvider.class); private SqlConfig.Profile cfg; - private SqlConnectionPool pool; public SqlProfileProvider(SqlConfig cfg) { @@ -50,6 +50,12 @@ public abstract class SqlProfileProvider implements ProfileProvider { this.pool = new SqlConnectionPool(cfg); } + private void setParameters(PreparedStatement stmt, String value) throws SQLException { + for (int i = 1; i <= stmt.getParameterMetaData().getParameterCount(); i++) { + stmt.setString(i, value); + } + } + @Override public Optional getDisplayName(_MatrixID user) { String stmtSql = cfg.getDisplayName().getQuery(); @@ -94,7 +100,33 @@ public abstract class SqlProfileProvider implements ProfileProvider { @Override public List getRoles(_MatrixID user) { - return Collections.emptyList(); + log.info("Querying roles for {}", user.getId()); + + List roles = new ArrayList<>(); + + String stmtSql = cfg.getRole().getQuery(); + try (Connection conn = pool.get()) { + PreparedStatement stmt = conn.prepareStatement(stmtSql); + if (UserIdType.Localpart.is(cfg.getRole().getType())) { + setParameters(stmt, user.getLocalPart()); + } else if (UserIdType.MatrixID.is(cfg.getRole().getType())) { + setParameters(stmt, user.getId()); + } else { + throw new InternalServerError("Unsupported user type in SQL Role fetching: " + cfg.getRole().getType()); + } + + ResultSet rSet = stmt.executeQuery(); + while (rSet.next()) { + String role = rSet.getString(1); + roles.add(role); + log.debug("Found role {}", role); + } + + log.info("Got {} roles", roles.size()); + return roles; + } catch (SQLException e) { + throw new RuntimeException(e); + } } } diff --git a/src/main/java/io/kamax/mxisd/backend/sql/synapse/SynapseQueries.java b/src/main/java/io/kamax/mxisd/backend/sql/synapse/SynapseQueries.java index c775c66..40f621f 100644 --- a/src/main/java/io/kamax/mxisd/backend/sql/synapse/SynapseQueries.java +++ b/src/main/java/io/kamax/mxisd/backend/sql/synapse/SynapseQueries.java @@ -43,6 +43,10 @@ public class SynapseQueries { return "SELECT medium, address FROM user_threepids WHERE user_id = ?"; } + public static String getRoles() { + return "SELECT DISTINCT(group_id) FROM group_users WHERE user_id = ?"; + } + public static String findByDisplayName(String type, String domain) { if (StringUtils.equals("sqlite", type)) { return "select " + getUserId(type, domain) + ", displayname from profiles p where displayname like ?"; diff --git a/src/main/java/io/kamax/mxisd/backend/sql/synapse/SynapseSqlStoreSupplier.java b/src/main/java/io/kamax/mxisd/backend/sql/synapse/SynapseSqlStoreSupplier.java index 018885a..ef9a331 100644 --- a/src/main/java/io/kamax/mxisd/backend/sql/synapse/SynapseSqlStoreSupplier.java +++ b/src/main/java/io/kamax/mxisd/backend/sql/synapse/SynapseSqlStoreSupplier.java @@ -26,9 +26,13 @@ import io.kamax.mxisd.config.MxisdConfig; import io.kamax.mxisd.directory.DirectoryProviders; import io.kamax.mxisd.lookup.ThreePidProviders; import io.kamax.mxisd.profile.ProfileProviders; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class SynapseSqlStoreSupplier implements IdentityStoreSupplier { + private static final Logger log = LoggerFactory.getLogger(SynapseSqlStoreSupplier.class); + @Override public void accept(Mxisd mxisd) { accept(mxisd.getConfig()); @@ -44,6 +48,7 @@ public class SynapseSqlStoreSupplier implements IdentityStoreSupplier { } if (cfg.getSynapseSql().getProfile().isEnabled()) { + log.debug("Profile is enabled, registering provider"); ProfileProviders.register(() -> new SynapseSqlProfileProvider(cfg.getSynapseSql())); } } diff --git a/src/main/java/io/kamax/mxisd/config/InvitationConfig.java b/src/main/java/io/kamax/mxisd/config/InvitationConfig.java index e814fd6..d776e79 100644 --- a/src/main/java/io/kamax/mxisd/config/InvitationConfig.java +++ b/src/main/java/io/kamax/mxisd/config/InvitationConfig.java @@ -20,13 +20,16 @@ package io.kamax.mxisd.config; -import io.kamax.mxisd.util.GsonUtil; +import io.kamax.matrix.json.GsonUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.List; + public class InvitationConfig { - private transient final Logger log = LoggerFactory.getLogger(InvitationConfig.class); + private static final Logger log = LoggerFactory.getLogger(InvitationConfig.class); public static class Resolution { @@ -51,7 +54,34 @@ public class InvitationConfig { } + public static class SenderPolicy { + + private List hasRole = new ArrayList<>(); + + public List getHasRole() { + return hasRole; + } + + public void setHasRole(List hasRole) { + this.hasRole = hasRole; + } + } + + public static class Policies { + + private SenderPolicy ifSender = new SenderPolicy(); + + public SenderPolicy getIfSender() { + return ifSender; + } + + public void setIfSender(SenderPolicy ifSender) { + this.ifSender = ifSender; + } + } + private Resolution resolution = new Resolution(); + private Policies policy = new Policies(); public Resolution getResolution() { return resolution; @@ -61,9 +91,18 @@ public class InvitationConfig { this.resolution = resolution; } + public Policies getPolicy() { + return policy; + } + + public void setPolicy(Policies policy) { + this.policy = policy; + } + public void build() { log.info("--- Invite config ---"); - log.info("Resolution: {}", GsonUtil.build().toJson(resolution)); + log.info("Resolution: {}", GsonUtil.get().toJson(getResolution())); + log.info("Policies: {}", GsonUtil.get().toJson(getPolicy())); } } diff --git a/src/main/java/io/kamax/mxisd/config/sql/SqlConfig.java b/src/main/java/io/kamax/mxisd/config/sql/SqlConfig.java index e30f5ff..6eb5ed3 100644 --- a/src/main/java/io/kamax/mxisd/config/sql/SqlConfig.java +++ b/src/main/java/io/kamax/mxisd/config/sql/SqlConfig.java @@ -193,11 +193,35 @@ public abstract class SqlConfig { } + public static class ProfileRoles { + + private String type; + private String query; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getQuery() { + return query; + } + + public void setQuery(String query) { + this.query = query; + } + + } + public static class Profile { private Boolean enabled; private ProfileDisplayName displayName = new ProfileDisplayName(); private ProfileThreepids threepid = new ProfileThreepids(); + private ProfileRoles role = new ProfileRoles(); public Boolean isEnabled() { return enabled; @@ -223,6 +247,14 @@ public abstract class SqlConfig { this.threepid = threepid; } + public ProfileRoles getRole() { + return role; + } + + public void setRole(ProfileRoles role) { + this.role = role; + } + } private boolean enabled; @@ -323,10 +355,11 @@ public abstract class SqlConfig { log.info("3PID mapping query: {}", getIdentity().getQuery()); log.info("Identity medium queries: {}", GsonUtil.build().toJson(getIdentity().getMedium())); log.info("Profile:"); - log.info("\tEnabled: {}", getProfile().isEnabled()); + log.info(" Enabled: {}", getProfile().isEnabled()); if (getProfile().isEnabled()) { - log.info("\tDisplay name query: {}", getProfile().getDisplayName().getQuery()); - log.info("\tProfile 3PID query: {}", getProfile().getThreepid().getQuery()); + log.info(" Display name query: {}", getProfile().getDisplayName().getQuery()); + log.info(" Profile 3PID query: {}", getProfile().getThreepid().getQuery()); + log.info(" Role query: {}", getProfile().getRole().getQuery()); } } } diff --git a/src/main/java/io/kamax/mxisd/config/sql/synapse/SynapseSqlProviderConfig.java b/src/main/java/io/kamax/mxisd/config/sql/synapse/SynapseSqlProviderConfig.java index c6f3281..747a18e 100644 --- a/src/main/java/io/kamax/mxisd/config/sql/synapse/SynapseSqlProviderConfig.java +++ b/src/main/java/io/kamax/mxisd/config/sql/synapse/SynapseSqlProviderConfig.java @@ -20,6 +20,7 @@ package io.kamax.mxisd.config.sql.synapse; +import io.kamax.mxisd.UserIdType; import io.kamax.mxisd.backend.sql.synapse.SynapseQueries; import io.kamax.mxisd.config.sql.SqlConfig; import org.apache.commons.lang.StringUtils; @@ -48,9 +49,17 @@ public class SynapseSqlProviderConfig extends SqlConfig { if (StringUtils.isBlank(getProfile().getDisplayName().getQuery())) { getProfile().getDisplayName().setQuery(SynapseQueries.getDisplayName()); } + if (StringUtils.isBlank(getProfile().getThreepid().getQuery())) { getProfile().getThreepid().setQuery(SynapseQueries.getThreepids()); } + + if (StringUtils.isBlank(getProfile().getRole().getType())) { + getProfile().getRole().setType(UserIdType.MatrixID.getId()); + } + if (StringUtils.isBlank(getProfile().getRole().getQuery())) { + getProfile().getRole().setQuery(SynapseQueries.getRoles()); + } } printConfig(); diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/BasicHttpHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/BasicHttpHandler.java index 0b21a19..fa059f8 100644 --- a/src/main/java/io/kamax/mxisd/http/undertow/handler/BasicHttpHandler.java +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/BasicHttpHandler.java @@ -23,20 +23,29 @@ package io.kamax.mxisd.http.undertow.handler; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.kamax.matrix.json.GsonUtil; +import io.kamax.mxisd.dns.ClientDnsOverwrite; +import io.kamax.mxisd.exception.AccessTokenNotFoundException; import io.kamax.mxisd.exception.HttpMatrixException; import io.kamax.mxisd.exception.InternalServerError; import io.kamax.mxisd.proxy.Response; +import io.kamax.mxisd.util.RestClientUtils; import io.undertow.server.HttpHandler; import io.undertow.server.HttpServerExchange; import io.undertow.util.HttpString; import org.apache.commons.io.IOUtils; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.Header; +import org.apache.http.HeaderElement; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.InetSocketAddress; +import java.net.URI; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.Deque; @@ -46,7 +55,19 @@ import java.util.Optional; public abstract class BasicHttpHandler implements HttpHandler { - private transient final Logger log = LoggerFactory.getLogger(BasicHttpHandler.class); + private static final Logger log = LoggerFactory.getLogger(BasicHttpHandler.class); + + protected String getAccessToken(HttpServerExchange exchange) { + return Optional.ofNullable(exchange.getRequestHeaders().getFirst("Authorization")) + .flatMap(v -> { + if (!v.startsWith("Bearer ")) { + return Optional.empty(); + } + + return Optional.of(v.substring("Bearer ".length())); + }).filter(StringUtils::isNotEmpty) + .orElseThrow(AccessTokenNotFoundException::new); + } protected String getRemoteHostAddress(HttpServerExchange exchange) { return ((InetSocketAddress) exchange.getConnection().getPeerAddress()).getAddress().getHostAddress(); @@ -149,4 +170,34 @@ public abstract class BasicHttpHandler implements HttpHandler { upstream.getHeaders().forEach((key, value) -> exchange.getResponseHeaders().addAll(HttpString.tryFromString(key), value)); writeBodyAsUtf8(exchange, upstream.getBody()); } + + protected void proxyPost(HttpServerExchange exchange, JsonObject body, CloseableHttpClient client, ClientDnsOverwrite dns) { + String target = dns.transform(URI.create(exchange.getRequestURL())).toString(); + log.info("Requesting remote: {}", target); + HttpPost req = RestClientUtils.post(target, GsonUtil.get(), body); + + exchange.getRequestHeaders().forEach(header -> { + header.forEach(v -> { + String name = header.getHeaderName().toString(); + if (!StringUtils.startsWithIgnoreCase(name, "content-")) { + req.addHeader(name, v); + } + }); + }); + + try (CloseableHttpResponse res = client.execute(req)) { + exchange.setStatusCode(res.getStatusLine().getStatusCode()); + for (Header h : res.getAllHeaders()) { + for (HeaderElement el : h.getElements()) { + exchange.getResponseHeaders().add(HttpString.tryFromString(h.getName()), el.getValue()); + } + } + res.getEntity().writeTo(exchange.getOutputStream()); + exchange.endExchange(); + } catch (IOException e) { + log.warn("Unable to make proxy call: {}", e.getMessage(), e); + throw new InternalServerError(e); + } + } + } diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/invite/v1/RoomInviteHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/invite/v1/RoomInviteHandler.java new file mode 100644 index 0000000..20e3420 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/invite/v1/RoomInviteHandler.java @@ -0,0 +1,103 @@ +/* + * 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.invite.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.mxisd.dns.ClientDnsOverwrite; +import io.kamax.mxisd.exception.InternalServerError; +import io.kamax.mxisd.exception.NotAllowedException; +import io.kamax.mxisd.exception.RemoteHomeServerException; +import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; +import io.kamax.mxisd.invitation.InvitationManager; +import io.undertow.server.HttpServerExchange; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; +import java.util.Optional; + +public class RoomInviteHandler extends BasicHttpHandler { + + public static final String Path = "/_matrix/client/r0/rooms/{roomId}/invite"; + + private static final Logger log = LoggerFactory.getLogger(RoomInviteHandler.class); + + private final CloseableHttpClient client; + private final ClientDnsOverwrite dns; + private final InvitationManager invMgr; + + public RoomInviteHandler(CloseableHttpClient client, ClientDnsOverwrite dns, InvitationManager invMgr) { + this.client = client; + this.dns = dns; + this.invMgr = invMgr; + } + + @Override + public void handleRequest(HttpServerExchange exchange) { + String accessToken = getAccessToken(exchange); + + String whoamiUri = dns.transform(URI.create(exchange.getRequestURL()).resolve(URI.create("/_matrix/client/r0/account/whoami"))).toString(); + log.info("Who Am I URL: {}", whoamiUri); + HttpGet whoAmIReq = new HttpGet(whoamiUri); + whoAmIReq.addHeader("Authorization", "Bearer " + accessToken); + _MatrixID uId; + try (CloseableHttpResponse whoAmIRes = client.execute(whoAmIReq)) { + int sc = whoAmIRes.getStatusLine().getStatusCode(); + String body = EntityUtils.toString(whoAmIRes.getEntity()); + + if (sc != 200) { + log.warn("Unable to get caller identity from Homeserver - Status code: {}", sc); + log.debug("Body: {}", body); + throw new RemoteHomeServerException(body); + } + + JsonObject json = GsonUtil.parseObj(body); + Optional uIdRaw = GsonUtil.findString(json, "user_id"); + if (!uIdRaw.isPresent()) { + throw new RemoteHomeServerException("No User ID provided when checking identity"); + } + + uId = MatrixID.asAcceptable(uIdRaw.get()); + } catch (IOException e) { + InternalServerError ex = new InternalServerError(e); + log.error("Ref {}: Unable to fetch caller identity from Homeserver", ex.getReference()); + throw ex; + } + + log.info("Processing room invite from {}", uId.getId()); + JsonObject reqBody = parseJsonObject(exchange); + if (!invMgr.canInvite(uId, reqBody)) { + throw new NotAllowedException("Your account is not allowed to invite that address"); + } + + log.info("Invite was allowing, relaying to the Homeserver"); + proxyPost(exchange, reqBody, client, dns); + } + +} diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/register/v1/Register3pidRequestTokenHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/register/v1/Register3pidRequestTokenHandler.java index af2dbcb..ed9919c 100644 --- a/src/main/java/io/kamax/mxisd/http/undertow/handler/register/v1/Register3pidRequestTokenHandler.java +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/register/v1/Register3pidRequestTokenHandler.java @@ -25,26 +25,16 @@ import io.kamax.matrix.ThreePid; import io.kamax.matrix.ThreePidMedium; import io.kamax.matrix.json.GsonUtil; import io.kamax.mxisd.dns.ClientDnsOverwrite; -import io.kamax.mxisd.exception.InternalServerError; import io.kamax.mxisd.exception.NotAllowedException; import io.kamax.mxisd.http.io.identity.SessionEmailTokenRequestJson; import io.kamax.mxisd.http.io.identity.SessionPhoneTokenRequestJson; import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; import io.kamax.mxisd.registration.RegistrationManager; -import io.kamax.mxisd.util.RestClientUtils; import io.undertow.server.HttpServerExchange; -import io.undertow.util.HttpString; -import org.apache.http.Header; -import org.apache.http.HeaderElement; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.CloseableHttpClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.net.URI; - public class Register3pidRequestTokenHandler extends BasicHttpHandler { public static final String Key = "medium"; @@ -77,25 +67,11 @@ public class Register3pidRequestTokenHandler extends BasicHttpHandler { } ThreePid tpid = new ThreePid(medium, address); - if (!mgr.allow(tpid)) { + if (!mgr.isAllowed(tpid)) { throw new NotAllowedException("Your " + medium + " address cannot be used for registration"); } - String target = dns.transform(URI.create(exchange.getRequestURL())).toString(); - log.info("Requesting remote: {}", target); - HttpPost req = RestClientUtils.post(target, GsonUtil.get(), body); - try (CloseableHttpResponse res = client.execute(req)) { - exchange.setStatusCode(res.getStatusLine().getStatusCode()); - for (Header h : res.getAllHeaders()) { - for (HeaderElement el : h.getElements()) { - exchange.getResponseHeaders().add(HttpString.tryFromString(h.getName()), el.getValue()); - } - } - res.getEntity().writeTo(exchange.getOutputStream()); - exchange.endExchange(); - } catch (IOException e) { - throw new InternalServerError(e); - } + proxyPost(exchange, body, client, dns); } } diff --git a/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java b/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java index 36938f3..521ac2d 100644 --- a/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java +++ b/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java @@ -24,6 +24,7 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; import io.kamax.matrix.MatrixID; import io.kamax.matrix.ThreePid; +import io.kamax.matrix._MatrixID; import io.kamax.matrix.json.GsonUtil; import io.kamax.mxisd.config.InvitationConfig; import io.kamax.mxisd.config.MxisdConfig; @@ -36,6 +37,7 @@ 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.profile.ProfileManager; import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.crypto.*; import io.kamax.mxisd.storage.ormlite.dao.ThreePidInviteIO; @@ -78,6 +80,7 @@ public class InvitationManager { private SignatureManager signMgr; private FederationDnsOverwrite dns; private NotificationManager notifMgr; + private ProfileManager profileMgr; private CloseableHttpClient client; private Timer refreshTimer; @@ -91,7 +94,8 @@ public class InvitationManager { KeyManager keyMgr, SignatureManager signMgr, FederationDnsOverwrite dns, - NotificationManager notifMgr + NotificationManager notifMgr, + ProfileManager profileMgr ) { this.cfg = mxisdCfg.getInvite(); this.srvCfg = mxisdCfg.getServer(); @@ -101,6 +105,7 @@ public class InvitationManager { this.signMgr = signMgr; this.dns = dns; this.notifMgr = notifMgr; + this.profileMgr = profileMgr; log.info("Loading saved invites"); Collection ioList = storage.getInvites(); @@ -214,6 +219,30 @@ public class InvitationManager { return lookupMgr.find(medium, address, cfg.getResolution().isRecursive()); } + public boolean canInvite(_MatrixID sender, JsonObject request) { + if (!request.has("medium")) { + log.info("Not a 3PID invite, allowing"); + return true; + } + log.info("3PID invite detected, checking policies..."); + + List allowedRoles = cfg.getPolicy().getIfSender().getHasRole(); + if (Objects.isNull(allowedRoles)) { + log.info("No allowed role configured for sender, allowing"); + return true; + } + + List userRoles = profileMgr.getRoles(sender); + if (Collections.disjoint(userRoles, allowedRoles)) { + log.info("Sender does not have any of the required roles, denying"); + return false; + } + log.info("Sender has at least one of the required roles"); + + log.info("Sender pass all policies to invite, allowing"); + return true; + } + public synchronized IThreePidInviteReply storeInvite(IThreePidInvite invitation) { // TODO better sync if (!notifMgr.isMediumSupported(invitation.getMedium())) { throw new BadRequestException("Medium type " + invitation.getMedium() + " is not supported"); diff --git a/src/main/java/io/kamax/mxisd/registration/RegistrationManager.java b/src/main/java/io/kamax/mxisd/registration/RegistrationManager.java index 63ecbc9..b64613a 100644 --- a/src/main/java/io/kamax/mxisd/registration/RegistrationManager.java +++ b/src/main/java/io/kamax/mxisd/registration/RegistrationManager.java @@ -95,7 +95,7 @@ public class RegistrationManager { } } - public boolean allow(ThreePid tpid) { + public boolean isAllowed(ThreePid tpid) { return invMgr.hasInvite(tpid); } From 4d63bba2514359ea355b1407efaf8969e4413ac6 Mon Sep 17 00:00:00 2001 From: Max Dor Date: Sat, 16 Feb 2019 03:06:46 +0100 Subject: [PATCH 07/28] Add version in jar - Cli argument - In HTTP client - /version endpoint --- build.gradle | 8 ++++ src/main/java/io/kamax/mxisd/HttpMxisd.java | 8 +++- src/main/java/io/kamax/mxisd/Mxisd.java | 5 +- .../io/kamax/mxisd/MxisdStandaloneExec.java | 24 +++++++--- .../handler/status/StatusHandler.java | 2 +- .../handler/status/VersionHandler.java | 48 +++++++++++++++++++ 6 files changed, 86 insertions(+), 9 deletions(-) create mode 100644 src/main/java/io/kamax/mxisd/http/undertow/handler/status/VersionHandler.java diff --git a/build.gradle b/build.gradle index 2ac687b..916adec 100644 --- a/build.gradle +++ b/build.gradle @@ -152,6 +152,14 @@ dependencies { testCompile 'com.icegreen:greenmail:1.5.9' } +jar { + manifest { + attributes( + 'Implementation-Version': mxisdVersion() + ) + } +} + shadowJar { baseName = project.name classifier = null diff --git a/src/main/java/io/kamax/mxisd/HttpMxisd.java b/src/main/java/io/kamax/mxisd/HttpMxisd.java index 22683c8..d22ebc4 100644 --- a/src/main/java/io/kamax/mxisd/HttpMxisd.java +++ b/src/main/java/io/kamax/mxisd/HttpMxisd.java @@ -36,10 +36,13 @@ import io.kamax.mxisd.http.undertow.handler.profile.v1.InternalProfileHandler; import io.kamax.mxisd.http.undertow.handler.profile.v1.ProfileHandler; import io.kamax.mxisd.http.undertow.handler.register.v1.Register3pidRequestTokenHandler; import io.kamax.mxisd.http.undertow.handler.status.StatusHandler; +import io.kamax.mxisd.http.undertow.handler.status.VersionHandler; import io.undertow.Handlers; import io.undertow.Undertow; import io.undertow.server.HttpHandler; +import java.util.Objects; + public class HttpMxisd { // Core @@ -67,6 +70,7 @@ public class HttpMxisd { // Status endpoints .get(StatusHandler.Path, SaneHandler.around(new StatusHandler())) + .get(VersionHandler.Path, SaneHandler.around(new VersionHandler())) // Authentication endpoints .get(LoginHandler.Path, SaneHandler.around(new LoginGetHandler(m.getAuth(), m.getHttpClient()))) @@ -119,7 +123,9 @@ public class HttpMxisd { } public void stop() { - httpSrv.stop(); + // Because it might have never been initialized if an exception is thrown early + if (Objects.nonNull(httpSrv)) httpSrv.stop(); + m.stop(); } diff --git a/src/main/java/io/kamax/mxisd/Mxisd.java b/src/main/java/io/kamax/mxisd/Mxisd.java index 7c0f82a..00022c8 100644 --- a/src/main/java/io/kamax/mxisd/Mxisd.java +++ b/src/main/java/io/kamax/mxisd/Mxisd.java @@ -51,6 +51,7 @@ import io.kamax.mxisd.storage.crypto.Ed25519KeyManager; import io.kamax.mxisd.storage.crypto.KeyManager; import io.kamax.mxisd.storage.crypto.SignatureManager; import io.kamax.mxisd.storage.ormlite.OrmLiteSqlStorage; +import org.apache.commons.lang.StringUtils; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; @@ -58,6 +59,8 @@ import java.util.ServiceLoader; public class Mxisd { + public static final String Version = StringUtils.defaultIfBlank(Mxisd.class.getPackage().getImplementationVersion(), "UNKNOWN"); + private MxisdConfig cfg; private CloseableHttpClient httpClient; @@ -86,7 +89,7 @@ public class Mxisd { private void build() { httpClient = HttpClients.custom() - .setUserAgent("mxisd") + .setUserAgent("mxisd/" + Version) .setMaxConnPerRoute(Integer.MAX_VALUE) .setMaxConnTotal(Integer.MAX_VALUE) .build(); diff --git a/src/main/java/io/kamax/mxisd/MxisdStandaloneExec.java b/src/main/java/io/kamax/mxisd/MxisdStandaloneExec.java index e5375df..bf0f7f9 100644 --- a/src/main/java/io/kamax/mxisd/MxisdStandaloneExec.java +++ b/src/main/java/io/kamax/mxisd/MxisdStandaloneExec.java @@ -37,21 +37,33 @@ public class MxisdStandaloneExec { public static void main(String[] args) { try { - log.info("------------- mxisd starting -------------"); MxisdConfig cfg = null; - Iterator argsIt = Arrays.asList(args).iterator(); while (argsIt.hasNext()) { String arg = argsIt.next(); - if (StringUtils.equals("-c", arg)) { + if (StringUtils.equalsAny(arg, "-h", "--help", "-?", "--usage")) { + System.out.println("Available arguments:" + System.lineSeparator()); + System.out.println(" -h, --help Show this help message"); + System.out.println(" --version Print the version then exit"); + System.out.println(" -c, --config Set the configuration file location"); + System.out.println(" "); + System.exit(0); + } else if (StringUtils.equalsAny(arg, "-c", "--config")) { String cfgFile = argsIt.next(); cfg = YamlConfigLoader.loadFromFile(cfgFile); + } else if (StringUtils.equals("--version", arg)) { + System.out.println(Mxisd.Version); + System.exit(0); } else { - log.info("Invalid argument: {}", arg); + System.err.println("Invalid argument: " + arg); + System.err.println("Try '--help' for available arguments"); System.exit(1); } } + log.info("mxisd starting"); + log.info("Version: {}", Mxisd.Version); + if (Objects.isNull(cfg)) { cfg = YamlConfigLoader.tryLoadFromFile("mxisd.yaml").orElseGet(MxisdConfig::new); } @@ -59,11 +71,11 @@ public class MxisdStandaloneExec { HttpMxisd mxisd = new HttpMxisd(cfg); Runtime.getRuntime().addShutdownHook(new Thread(() -> { mxisd.stop(); - log.info("------------- mxisd stopped -------------"); + log.info("mxisd stopped"); })); mxisd.start(); - log.info("------------- mxisd started -------------"); + log.info("mxisd started"); } catch (ConfigurationException e) { log.error(e.getDetailedMessage()); log.error(e.getMessage()); diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/status/StatusHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/status/StatusHandler.java index a5dfa0e..4183081 100644 --- a/src/main/java/io/kamax/mxisd/http/undertow/handler/status/StatusHandler.java +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/status/StatusHandler.java @@ -26,7 +26,7 @@ import io.undertow.server.HttpServerExchange; public class StatusHandler extends BasicHttpHandler { - public static final String Path = "/_matrix/identity/status"; + public static final String Path = "/status"; @Override public void handleRequest(HttpServerExchange exchange) { diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/status/VersionHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/status/VersionHandler.java new file mode 100644 index 0000000..5118789 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/status/VersionHandler.java @@ -0,0 +1,48 @@ +/* + * 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.status; + +import com.google.gson.JsonObject; +import io.kamax.matrix.json.GsonUtil; +import io.kamax.mxisd.Mxisd; +import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; +import io.undertow.server.HttpServerExchange; + +public class VersionHandler extends BasicHttpHandler { + + public static final String Path = "/version"; + + private final String body; + + public VersionHandler() { + JsonObject server = new JsonObject(); + server.addProperty("name", "mxisd"); + server.addProperty("version", Mxisd.Version); + + body = GsonUtil.getPrettyForLog(GsonUtil.makeObj("server", server)); + } + + @Override + public void handleRequest(HttpServerExchange exchange) { + respondJson(exchange, body); + } + +} From 37ddd0e58866dcef96d8f63b59519fa105670328 Mon Sep 17 00:00:00 2001 From: Max Dor Date: Sun, 17 Feb 2019 03:22:48 +0100 Subject: [PATCH 08/28] Talk about server.name in the example config --- mxisd.example.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mxisd.example.yaml b/mxisd.example.yaml index 171a8a8..cc40217 100644 --- a/mxisd.example.yaml +++ b/mxisd.example.yaml @@ -14,6 +14,11 @@ # NOTE: in Synapse Homeserver, the Matrix domain is defined as 'server_name' in configuration file. # # This is used to build the various identifiers in all the features. +# +# If the hostname of the public URL used to reach your Matrix services is different from your Matrix domain, +# per example matrix.domain.tld vs domain.tld, then use the server.name configuration option. +# See the "Configure" section of the Getting Started guide for more info. +# matrix: domain: '' From 72a1794cc3fd802196862342f3607d580da18a29 Mon Sep 17 00:00:00 2001 From: Max Dor Date: Mon, 18 Feb 2019 23:08:50 +0100 Subject: [PATCH 09/28] Skeleton for 3PID registration policies (#130) --- src/main/java/io/kamax/mxisd/Mxisd.java | 2 +- .../io/kamax/mxisd/config/MxisdConfig.java | 10 + .../io/kamax/mxisd/config/RegisterConfig.java | 201 ++++++++++++++++++ .../registration/RegistrationManager.java | 58 ++++- 4 files changed, 261 insertions(+), 10 deletions(-) create mode 100644 src/main/java/io/kamax/mxisd/config/RegisterConfig.java diff --git a/src/main/java/io/kamax/mxisd/Mxisd.java b/src/main/java/io/kamax/mxisd/Mxisd.java index 00022c8..2410f70 100644 --- a/src/main/java/io/kamax/mxisd/Mxisd.java +++ b/src/main/java/io/kamax/mxisd/Mxisd.java @@ -115,7 +115,7 @@ public class Mxisd { invMgr = new InvitationManager(cfg, store, idStrategy, keyMgr, signMgr, fedDns, notifMgr, pMgr); authMgr = new AuthManager(cfg, AuthProviders.get(), idStrategy, invMgr, clientDns, httpClient); dirMgr = new DirectoryManager(cfg.getDirectory(), clientDns, httpClient, DirectoryProviders.get()); - regMgr = new RegistrationManager(httpClient, clientDns, idStrategy, invMgr); + regMgr = new RegistrationManager(cfg.getRegister(), httpClient, clientDns, invMgr); asHander = new AppSvcManager(cfg, store, pMgr, notifMgr, synapse); } diff --git a/src/main/java/io/kamax/mxisd/config/MxisdConfig.java b/src/main/java/io/kamax/mxisd/config/MxisdConfig.java index 787171e..3fb5ffa 100644 --- a/src/main/java/io/kamax/mxisd/config/MxisdConfig.java +++ b/src/main/java/io/kamax/mxisd/config/MxisdConfig.java @@ -97,6 +97,7 @@ public class MxisdConfig { private MemoryStoreConfig memory = new MemoryStoreConfig(); private NotificationConfig notification = new NotificationConfig(); private NetIqLdapConfig netiq = new NetIqLdapConfig(); + private RegisterConfig register = new RegisterConfig(); private ServerConfig server = new ServerConfig(); private SessionConfig session = new SessionConfig(); private StorageConfig storage = new StorageConfig(); @@ -219,6 +220,14 @@ public class MxisdConfig { this.netiq = netiq; } + public RegisterConfig getRegister() { + return register; + } + + public void setRegister(RegisterConfig register) { + this.register = register; + } + public ServerConfig getServer() { return server; } @@ -310,6 +319,7 @@ public class MxisdConfig { getMemory().build(); getNetiq().build(); getNotification().build(); + getRegister().build(); getRest().build(); getSession().build(); getServer().build(); diff --git a/src/main/java/io/kamax/mxisd/config/RegisterConfig.java b/src/main/java/io/kamax/mxisd/config/RegisterConfig.java new file mode 100644 index 0000000..515aab4 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/config/RegisterConfig.java @@ -0,0 +1,201 @@ +/* + * 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.config; + +import io.kamax.matrix.ThreePidMedium; +import io.kamax.matrix.json.GsonUtil; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.stream.Collectors; + +public class RegisterConfig { + + private static final Logger log = LoggerFactory.getLogger(RegisterConfig.class); + + public static class ThreepidPolicyPattern { + + private List blacklist = new ArrayList<>(); + private List whitelist = new ArrayList<>(); + + public List getBlacklist() { + return blacklist; + } + + public void setBlacklist(List blacklist) { + this.blacklist = blacklist; + } + + public List getWhitelist() { + return whitelist; + } + + public void setWhitelist(List whitelist) { + this.whitelist = whitelist; + } + + } + + public static class EmailPolicy extends ThreepidPolicy { + + private ThreepidPolicyPattern domain = new ThreepidPolicyPattern(); + + public ThreepidPolicyPattern getDomain() { + return domain; + } + + public void setDomain(ThreepidPolicyPattern domain) { + this.domain = domain; + } + + private List buildPatterns(List domains) { + log.info("Building email policy"); + return domains.stream().map(d -> { + if (StringUtils.startsWith(d, "*")) { + log.info("Found domain and subdomain policy"); + d = "(.*)" + d.substring(1); + } else if (StringUtils.startsWith(d, ".")) { + log.info("Found subdomain-only policy"); + d = "(.*)" + d; + } else { + log.info("Found domain-only policy"); + } + + return "([^@]+)@" + d.replace(".", "\\."); + }).collect(Collectors.toList()); + } + + @Override + public void build() { + if (Objects.isNull(getDomain())) { + return; + } + + if (Objects.nonNull(getDomain().getBlacklist())) { + if (Objects.isNull(getPattern().getBlacklist())) { + getPattern().setBlacklist(new ArrayList<>()); + } + + List domains = buildPatterns(getDomain().getBlacklist()); + getPattern().getBlacklist().addAll(domains); + } + + if (Objects.nonNull(getDomain().getWhitelist())) { + if (Objects.isNull(getPattern().getWhitelist())) { + getPattern().setWhitelist(new ArrayList<>()); + } + + List domains = buildPatterns(getDomain().getWhitelist()); + getPattern().getWhitelist().addAll(domains); + } + + setDomain(null); + } + + } + + public static class ThreepidPolicy { + + private ThreepidPolicyPattern pattern = new ThreepidPolicyPattern(); + + public ThreepidPolicyPattern getPattern() { + return pattern; + } + + public void setPattern(ThreepidPolicyPattern pattern) { + this.pattern = pattern; + } + + public void build() { + // no-op + } + + } + + public static class Policy { + + private boolean allowed; + private boolean invite = true; + private Map threepid = new HashMap<>(); + + public boolean isAllowed() { + return allowed; + } + + public void setAllowed(boolean allowed) { + this.allowed = allowed; + } + + public boolean forInvite() { + return invite; + } + + public void setInvite(boolean invite) { + this.invite = invite; + } + + public Map getThreepid() { + return threepid; + } + + public void setThreepid(Map threepid) { + this.threepid = threepid; + } + + } + + private Policy policy = new Policy(); + + public Policy getPolicy() { + return policy; + } + + public void setPolicy(Policy policy) { + this.policy = policy; + } + + public void build() { + log.info("--- Registration config ---"); + + log.info("Before Build"); + log.info(GsonUtil.getPrettyForLog(this)); + + new HashMap<>(getPolicy().getThreepid()).forEach((medium, policy) -> { + if (ThreePidMedium.Email.is(medium)) { + EmailPolicy pPolicy = GsonUtil.get().fromJson(GsonUtil.get().toJson(policy), EmailPolicy.class); + pPolicy.build(); + policy = GsonUtil.makeObj(pPolicy); + } else { + ThreepidPolicy pPolicy = GsonUtil.get().fromJson(GsonUtil.get().toJson(policy), ThreepidPolicy.class); + pPolicy.build(); + policy = GsonUtil.makeObj(pPolicy); + } + + getPolicy().getThreepid().put(medium, policy); + }); + + log.info("After Build"); + log.info(GsonUtil.getPrettyForLog(this)); + } + +} diff --git a/src/main/java/io/kamax/mxisd/registration/RegistrationManager.java b/src/main/java/io/kamax/mxisd/registration/RegistrationManager.java index b64613a..f98f80d 100644 --- a/src/main/java/io/kamax/mxisd/registration/RegistrationManager.java +++ b/src/main/java/io/kamax/mxisd/registration/RegistrationManager.java @@ -23,11 +23,11 @@ package io.kamax.mxisd.registration; import com.google.gson.JsonObject; import io.kamax.matrix.ThreePid; import io.kamax.matrix.json.GsonUtil; +import io.kamax.mxisd.config.RegisterConfig; import io.kamax.mxisd.dns.ClientDnsOverwrite; import io.kamax.mxisd.exception.NotImplementedException; import io.kamax.mxisd.exception.RemoteHomeServerException; import io.kamax.mxisd.invitation.InvitationManager; -import io.kamax.mxisd.lookup.strategy.LookupStrategy; import io.kamax.mxisd.util.RestClientUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.methods.CloseableHttpResponse; @@ -40,24 +40,23 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.URI; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class RegistrationManager { private static final Logger log = LoggerFactory.getLogger(RegistrationManager.class); + private final RegisterConfig cfg; private final CloseableHttpClient client; private final ClientDnsOverwrite dns; - private final LookupStrategy lookup; private final InvitationManager invMgr; - private Map sessions = new ConcurrentHashMap<>(); - - public RegistrationManager(CloseableHttpClient client, ClientDnsOverwrite dns, LookupStrategy lookup, InvitationManager invMgr) { + public RegistrationManager(RegisterConfig cfg, CloseableHttpClient client, ClientDnsOverwrite dns, InvitationManager invMgr) { + this.cfg = cfg; this.client = client; this.dns = dns; - this.lookup = lookup; this.invMgr = invMgr; } @@ -96,7 +95,48 @@ public class RegistrationManager { } public boolean isAllowed(ThreePid tpid) { - return invMgr.hasInvite(tpid); + // We check if the policy allows registration for invites, and if there is an invite for the 3PID + if (cfg.getPolicy().forInvite() && invMgr.hasInvite(tpid)) { + log.info("Registration allowed for pending invite"); + return true; + } + + // The following section deals with patterns which can either be built at startup time, or for each invite at runtime. + // Registration is a very rare occurrence relatively speaking, so we make the choice to build the patterns each time + // at runtime to save on RAM. + + Object policy = cfg.getPolicy().getThreepid().get(tpid.getMedium()); + if (Objects.nonNull(policy)) { + RegisterConfig.ThreepidPolicy tpidPolicy = GsonUtil.get().fromJson(GsonUtil.get().toJson(policy), RegisterConfig.ThreepidPolicy.class); + log.info("Found registration policy for {}", tpid.getMedium()); + + log.info("Processing pattern blacklist"); + for (String pattern : tpidPolicy.getPattern().getBlacklist()) { + log.info("Processing pattern {}", pattern); + + // We compile the pattern + Matcher m = Pattern.compile(pattern).matcher(tpid.getAddress()); + if (m.matches()) { // We only care about those who match... + log.info("Found matching blacklist entry, denying registration"); + return false; // ... and get denied as per blacklist + } + } + + log.info("Processing pattern whitelist"); + for (String pattern : tpidPolicy.getPattern().getWhitelist()) { + log.info("Processing pattern {}", pattern); + + // We compile the pattern + Matcher m = Pattern.compile(pattern).matcher(tpid.getAddress()); + if (m.matches()) { // We only care about those who match... + log.info("Found matching whitelist entry, allowing registration"); + return true; // ... and get accepted as per whitelist + } + } + } + + log.info("Returning default registration policy: {}", cfg.getPolicy().isAllowed()); + return cfg.getPolicy().isAllowed(); } } From 95ee3282815164aaac62d8143581415fce43f9aa Mon Sep 17 00:00:00 2001 From: Max Dor Date: Mon, 25 Feb 2019 14:06:32 +0100 Subject: [PATCH 10/28] Block custom internal endpoint that should never be called - Is not spec'd - Will not be spec'd - Is 100% internal as per its authors --- src/main/java/io/kamax/mxisd/HttpMxisd.java | 4 ++ .../undertow/handler/InternalInfoHandler.java | 50 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 src/main/java/io/kamax/mxisd/http/undertow/handler/InternalInfoHandler.java diff --git a/src/main/java/io/kamax/mxisd/HttpMxisd.java b/src/main/java/io/kamax/mxisd/HttpMxisd.java index d22ebc4..3c4c966 100644 --- a/src/main/java/io/kamax/mxisd/HttpMxisd.java +++ b/src/main/java/io/kamax/mxisd/HttpMxisd.java @@ -21,6 +21,7 @@ package io.kamax.mxisd; import io.kamax.mxisd.config.MxisdConfig; +import io.kamax.mxisd.http.undertow.handler.InternalInfoHandler; import io.kamax.mxisd.http.undertow.handler.OptionsHandler; import io.kamax.mxisd.http.undertow.handler.SaneHandler; import io.kamax.mxisd.http.undertow.handler.as.v1.AsNotFoundHandler; @@ -117,6 +118,9 @@ public class HttpMxisd { .put(AsTransactionHandler.Path, asTxnHandler) .put("/transactions/{" + AsTransactionHandler.ID + "}", asTxnHandler) // Legacy endpoint + // Banned endpoints + .get(InternalInfoHandler.Path, SaneHandler.around(new InternalInfoHandler())) + ).build(); httpSrv.start(); diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/InternalInfoHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/InternalInfoHandler.java new file mode 100644 index 0000000..973e1f3 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/InternalInfoHandler.java @@ -0,0 +1,50 @@ +/* + * 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; + +import io.undertow.server.HttpServerExchange; + +import java.util.concurrent.ThreadLocalRandom; + +public class InternalInfoHandler extends BasicHttpHandler { + + /* + * This endpoint should never be called as being entierly custom as per instructions of New Vector, + * the author of that endpoint. + * + * Used for the first time at https://github.com/matrix-org/synapse/pull/4681/files#diff-a73c645c44a17da6ab70f256da6b60afR41 + * + * Full context: https://matrix.to/#/!YkZelGRiqijtzXZODa:matrix.org/$15510967621328WMKVu:kamax.io?via=matrix.org + * Room name: #matrix-spec + * Room alias: #matrix-spec:matrix.org + */ + public static final String Path = "/_matrix/identity/api/{version}/internal-info"; + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + // We will return a random status code in all possible error codes + int type = ThreadLocalRandom.current().nextInt(4, 6) * 100; // Random 4 or 5, times 100 + int status = type + ThreadLocalRandom.current().nextInt(0, 100); // Random 0 to 99 + + respond(exchange, status, "M_FORBIDDEN", "This endpoint is under quarantine and possibly wrongfully labeled stable."); + } + +} From 96155c1876bf4f790d7d6d3e2f9c29a159d7f9b7 Mon Sep 17 00:00:00 2001 From: Max Dor Date: Fri, 1 Mar 2019 01:12:02 +0100 Subject: [PATCH 11/28] Improving logging --- src/main/java/io/kamax/mxisd/HttpMxisd.java | 6 ++++++ src/main/resources/simplelogger.properties | 2 ++ 2 files changed, 8 insertions(+) create mode 100644 src/main/resources/simplelogger.properties diff --git a/src/main/java/io/kamax/mxisd/HttpMxisd.java b/src/main/java/io/kamax/mxisd/HttpMxisd.java index 3c4c966..b6e34bc 100644 --- a/src/main/java/io/kamax/mxisd/HttpMxisd.java +++ b/src/main/java/io/kamax/mxisd/HttpMxisd.java @@ -52,6 +52,12 @@ public class HttpMxisd { // I/O private Undertow httpSrv; + static { + // Used in XNIO package, dependency of Undertow + // We switch to slf4j + System.setProperty("org.jboss.logging.provider", "slf4j"); + } + public HttpMxisd(MxisdConfig cfg) { m = new Mxisd(cfg); } diff --git a/src/main/resources/simplelogger.properties b/src/main/resources/simplelogger.properties new file mode 100644 index 0000000..b509437 --- /dev/null +++ b/src/main/resources/simplelogger.properties @@ -0,0 +1,2 @@ +org.slf4j.simpleLogger.logFile=System.out +org.slf4j.simpleLogger.log.org.xnio=warn From c3027898987a3a86fa12fd6b74229c26984d4b87 Mon Sep 17 00:00:00 2001 From: Max Dor Date: Fri, 1 Mar 2019 06:51:18 +0100 Subject: [PATCH 12/28] Add mechanism for 3PID invites expiration (#120) --- build.gradle | 2 +- .../kamax/mxisd/config/InvitationConfig.java | 44 ++++- .../mxisd/invitation/InvitationManager.java | 153 +++++++++++++++--- .../java/io/kamax/mxisd/storage/IStorage.java | 2 + .../storage/crypto/Ed25519KeyManager.java | 4 +- .../crypto/Ed25519SignatureManager.java | 2 - .../storage/ormlite/OrmLiteSqlStorage.java | 30 +++- .../dao/HistoricalThreePidInviteIO.java | 72 +++++++++ 8 files changed, 272 insertions(+), 37 deletions(-) create mode 100644 src/main/java/io/kamax/mxisd/storage/ormlite/dao/HistoricalThreePidInviteIO.java diff --git a/build.gradle b/build.gradle index 916adec..9d685b5 100644 --- a/build.gradle +++ b/build.gradle @@ -101,7 +101,7 @@ dependencies { compile 'com.j256.ormlite:ormlite-jdbc:5.0' // ed25519 handling - compile 'net.i2p.crypto:eddsa:0.3.0' + compile 'net.i2p.crypto:eddsa:0.1.0' // LDAP connector compile 'org.apache.directory.api:api-all:1.0.0' diff --git a/src/main/java/io/kamax/mxisd/config/InvitationConfig.java b/src/main/java/io/kamax/mxisd/config/InvitationConfig.java index d776e79..e819524 100644 --- a/src/main/java/io/kamax/mxisd/config/InvitationConfig.java +++ b/src/main/java/io/kamax/mxisd/config/InvitationConfig.java @@ -31,10 +31,42 @@ public class InvitationConfig { private static final Logger log = LoggerFactory.getLogger(InvitationConfig.class); + public static class Expiration { + + private Boolean enabled; + private long after; + private String resolveTo; + + public Boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public long getAfter() { + return after; + } + + public void setAfter(long after) { + this.after = after; + } + + public String getResolveTo() { + return resolveTo; + } + + public void setResolveTo(String resolveTo) { + this.resolveTo = resolveTo; + } + + } + public static class Resolution { private boolean recursive = true; - private long timer = 1; + private long timer = 5; public boolean isRecursive() { return recursive; @@ -80,9 +112,18 @@ public class InvitationConfig { } } + private Expiration expiration = new Expiration(); private Resolution resolution = new Resolution(); private Policies policy = new Policies(); + public Expiration getExpiration() { + return expiration; + } + + public void setExpiration(Expiration expiration) { + this.expiration = expiration; + } + public Resolution getResolution() { return resolution; } @@ -101,6 +142,7 @@ public class InvitationConfig { public void build() { log.info("--- Invite config ---"); + log.info("Expiration: {}", GsonUtil.get().toJson(getExpiration())); log.info("Resolution: {}", GsonUtil.get().toJson(getResolution())); log.info("Policies: {}", GsonUtil.get().toJson(getPolicy())); } diff --git a/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java b/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java index 521ac2d..f8004a7 100644 --- a/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java +++ b/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java @@ -31,6 +31,7 @@ import io.kamax.mxisd.config.MxisdConfig; import io.kamax.mxisd.config.ServerConfig; import io.kamax.mxisd.dns.FederationDnsOverwrite; import io.kamax.mxisd.exception.BadRequestException; +import io.kamax.mxisd.exception.ConfigurationException; import io.kamax.mxisd.exception.MappingAlreadyExistsException; import io.kamax.mxisd.exception.ObjectNotFoundException; import io.kamax.mxisd.lookup.SingleLookupReply; @@ -41,6 +42,7 @@ import io.kamax.mxisd.profile.ProfileManager; import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.crypto.*; import io.kamax.mxisd.storage.ormlite.dao.ThreePidInviteIO; +import org.apache.commons.codec.binary.Base64; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; @@ -63,6 +65,8 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.time.DateTimeException; +import java.time.Instant; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ForkJoinPool; @@ -70,7 +74,10 @@ import java.util.concurrent.TimeUnit; public class InvitationManager { - private transient final Logger log = LoggerFactory.getLogger(InvitationManager.class); + private static final Logger log = LoggerFactory.getLogger(InvitationManager.class); + private static final String CreatedAtPropertyKey = "created_at"; + + private final String defaultCreateTs = Long.toString(Instant.now().toEpochMilli()); private InvitationConfig cfg; private ServerConfig srvCfg; @@ -97,7 +104,7 @@ public class InvitationManager { NotificationManager notifMgr, ProfileManager profileMgr ) { - this.cfg = mxisdCfg.getInvite(); + this.cfg = requireValid(mxisdCfg); this.srvCfg = mxisdCfg.getServer(); this.storage = storage; this.lookupMgr = lookupMgr; @@ -110,6 +117,7 @@ public class InvitationManager { log.info("Loading saved invites"); Collection ioList = storage.getInvites(); ioList.forEach(io -> { + io.getProperties().putIfAbsent(CreatedAtPropertyKey, defaultCreateTs); log.info("Processing invite {}", GsonUtil.get().toJson(io)); ThreePidInvite invite = new ThreePidInvite( MatrixID.asAcceptable(io.getSender()), @@ -119,7 +127,7 @@ public class InvitationManager { io.getProperties() ); - ThreePidInviteReply reply = new ThreePidInviteReply(getId(invite), invite, io.getToken(), "", Collections.emptyList()); + ThreePidInviteReply reply = new ThreePidInviteReply(io.getId(), invite, io.getToken(), "", Collections.emptyList()); invitations.put(reply.getId(), reply); }); @@ -136,25 +144,64 @@ public class InvitationManager { log.info("Setting up invitation mapping refresh timer"); refreshTimer = new Timer(); - refreshTimer.scheduleAtFixedRate(new TimerTask() { - @Override - public void run() { - try { - lookupMappingsForInvites(); - } catch (Throwable t) { - log.error("Error when running background mapping refresh", t); - } - } - }, 5000L, TimeUnit.MILLISECONDS.convert(cfg.getResolution().getTimer(), TimeUnit.MINUTES)); + // We add a shutdown hook to cancel the hook and wait for pending resolutions Runtime.getRuntime().addShutdownHook(new Thread(() -> { refreshTimer.cancel(); ForkJoinPool.commonPool().awaitQuiescence(1, TimeUnit.MINUTES); })); + + // We set the refresh timer for background tasks + refreshTimer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + try { + doMaintenance(); + } catch (Throwable t) { + log.error("Error when running background maintenance", t); + } + } + }, 5000L, TimeUnit.MILLISECONDS.convert(cfg.getResolution().getTimer(), TimeUnit.MINUTES)); } - private String getId(IThreePidInvite invite) { - return invite.getSender().getDomain().toLowerCase() + invite.getMedium().toLowerCase() + invite.getAddress().toLowerCase(); + private InvitationConfig requireValid(MxisdConfig cfg) { + // This is not configured, we'll apply a default configuration + if (Objects.isNull(cfg.getInvite().getExpiration().isEnabled())) { + // We compute our own user, so it can be used if we bridge as well + String mxId = MatrixID.asAcceptable("_mxisd-expired_invite", cfg.getMatrix().getDomain()).getId(); + + // Enabled by default + cfg.getInvite().getExpiration().setEnabled(true); + + // We'll resolve to our computed User ID + cfg.getInvite().getExpiration().setResolveTo(mxId); + + // One calendar week (60min/1h * 24 = 1d * 7 = 1w) + cfg.getInvite().getExpiration().setAfter(60 * 24 * 7); + } + + if (cfg.getInvite().getExpiration().isEnabled()) { + if (cfg.getInvite().getExpiration().getAfter() < 1) { + throw new ConfigurationException("Invitation expiration delay must be greater or equal to 1"); + } + + if (StringUtils.isBlank(cfg.getInvite().getExpiration().getResolveTo())) { + throw new ConfigurationException("Invitation expiration resolution target cannot be empty/blank"); + } + + try { + MatrixID.asAcceptable(cfg.getInvite().getExpiration().getResolveTo()); + } catch (IllegalArgumentException e) { + throw new ConfigurationException("Invitation expiration resolution target is not a valid Matrix ID: " + e.getMessage()); + } + } + + return cfg.getInvite(); + } + + private String computeId(IThreePidInvite invite) { + String rawId = invite.getSender().getDomain().toLowerCase() + invite.getMedium().toLowerCase() + invite.getAddress().toLowerCase(); + return Base64.encodeBase64URLSafeString(rawId.getBytes(StandardCharsets.UTF_8)); } private String getIdForLog(IThreePidInviteReply reply) { @@ -248,7 +295,7 @@ public class InvitationManager { throw new BadRequestException("Medium type " + invitation.getMedium() + " is not supported"); } - String invId = getId(invitation); + String invId = computeId(invitation); log.info("Handling invite for {}:{} from {} in room {}", invitation.getMedium(), invitation.getAddress(), invitation.getSender(), invitation.getRoomId()); IThreePidInviteReply reply = invitations.get(invId); if (reply != null) { @@ -276,6 +323,7 @@ public class InvitationManager { String pPubKey = keyMgr.getPublicKeyBase64(pKeyId); String ePubKey = keyMgr.getPublicKeyBase64(eKeyId); + invitation.getProperties().put(CreatedAtPropertyKey, Long.toString(Instant.now().toEpochMilli())); invitation.getProperties().put("p_key_algo", pKeyId.getAlgorithm()); invitation.getProperties().put("p_key_serial", pKeyId.getSerial()); invitation.getProperties().put("p_key_public", pPubKey); @@ -312,6 +360,58 @@ public class InvitationManager { return false; } + private void removeInvite(IThreePidInviteReply reply) { + invitations.remove(reply.getId()); + storage.deleteInvite(reply.getId()); + } + + /** + * Trigger the periodic maintenance tasks + */ + public void doMaintenance() { + lookupMappingsForInvites(); + expireInvites(); + } + + public void expireInvites() { + log.debug("Invite expiration: started"); + + if (!cfg.getExpiration().isEnabled()) { + log.debug("Invite expiration is disabled, skipping"); + return; + } + + if (invitations.isEmpty()) { + log.debug("No invite to expired, skipping"); + return; + } + + String targetMxid = cfg.getExpiration().getResolveTo(); + for (IThreePidInviteReply reply : invitations.values()) { + log.debug("Processing invite {}", reply.getId()); + + String tsRaw = reply.getInvite().getProperties().computeIfAbsent(CreatedAtPropertyKey, k -> defaultCreateTs); + try { + Instant ts = Instant.ofEpochMilli(Long.parseLong(tsRaw)); + Instant targetTs = ts.plusSeconds(cfg.getExpiration().getAfter() * 60); + Instant now = Instant.now(); + log.debug("Invite {} - Created at {} - Expire at {} - Current time is {}", reply.getId(), ts, targetTs, now); + if (targetTs.isBefore(Instant.now())) { + log.debug("Invite {} has not expired yet, skipping", reply.getId()); + continue; + } + + log.info("Invite {} has expired at TS {} - Expiring and resolving to {}", targetTs, targetMxid); + publishMapping(reply, targetMxid); + } catch (NumberFormatException | DateTimeException e) { + log.warn("Invite {} has an invalid creation TS, setting to default value of {}", reply.getId(), defaultCreateTs); + reply.getInvite().getProperties().put(CreatedAtPropertyKey, defaultCreateTs); + } + } + + log.debug("Invite expiration: finished"); + } + public void lookupMappingsForInvites() { if (!invitations.isEmpty()) { log.info("Checking for existing mapping for pending invites"); @@ -391,25 +491,32 @@ public class InvitationManager { StringEntity entity = new StringEntity(content.toString(), StandardCharsets.UTF_8); entity.setContentType("application/json"); req.setEntity(entity); + + Instant resolvedAt = Instant.now(); + boolean couldPublish = false; try { log.info("Posting onBind event to {}", req.getURI()); CloseableHttpResponse response = client.execute(req); int statusCode = response.getStatusLine().getStatusCode(); log.info("Answer code: {}", statusCode); if (statusCode >= 300 && statusCode != 403) { - log.warn("Answer body: {}", IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8)); + log.info("Answer body: {}", IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8)); + log.warn("HS returned an error. Invite can be found in historical storage for manual re-processing"); } else { + couldPublish = true; if (statusCode == 403) { - log.info("Invite was obsolete"); + log.info("Invite is obsolete or no longer under our control"); } - - invitations.remove(getId(reply.getInvite())); - storage.deleteInvite(reply.getId()); - log.info("Removed invite from internal store"); } response.close(); } catch (IOException e) { log.warn("Unable to tell HS {} about invite being mapped", domain, e); + } finally { + synchronized (this) { + storage.insertHistoricalInvite(reply, mxid, resolvedAt, couldPublish); + removeInvite(reply); + log.info("Moved invite {} to historical table", reply.getId()); + } } }).start(); } @@ -425,7 +532,7 @@ public class InvitationManager { @Override public void run() { try { - log.info("Searching for mapping created since invite {} was created", getIdForLog(reply)); + log.info("Searching for mapping created after invite {} was created", getIdForLog(reply)); Optional result = lookup3pid(reply.getInvite().getMedium(), reply.getInvite().getAddress()); if (result.isPresent()) { SingleLookupReply lookup = result.get(); diff --git a/src/main/java/io/kamax/mxisd/storage/IStorage.java b/src/main/java/io/kamax/mxisd/storage/IStorage.java index 80d35c4..ff1bbbe 100644 --- a/src/main/java/io/kamax/mxisd/storage/IStorage.java +++ b/src/main/java/io/kamax/mxisd/storage/IStorage.java @@ -38,6 +38,8 @@ public interface IStorage { void deleteInvite(String id); + void insertHistoricalInvite(IThreePidInviteReply data, String resolvedTo, Instant resolvedAt, boolean couldPublish); + Optional getThreePidSession(String sid); Optional findThreePidSession(ThreePid tpid, String secret); diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/Ed25519KeyManager.java b/src/main/java/io/kamax/mxisd/storage/crypto/Ed25519KeyManager.java index d70eb73..2b4dae5 100644 --- a/src/main/java/io/kamax/mxisd/storage/crypto/Ed25519KeyManager.java +++ b/src/main/java/io/kamax/mxisd/storage/crypto/Ed25519KeyManager.java @@ -47,7 +47,7 @@ public class Ed25519KeyManager implements KeyManager { private final KeyStore store; public Ed25519KeyManager(KeyStore store) { - this.keySpecs = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519); + this.keySpecs = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.CURVE_ED25519_SHA512); this.store = store; if (!store.getCurrentKey().isPresent()) { @@ -106,7 +106,7 @@ public class Ed25519KeyManager implements KeyManager { } public EdDSAPrivateKeySpec getPrivateKeySpecs(KeyIdentifier id) { - return new EdDSAPrivateKeySpec(java.util.Base64.getDecoder().decode(getKey(id).getPrivateKeyBase64()), keySpecs); + return new EdDSAPrivateKeySpec(Base64.decodeBase64(getKey(id).getPrivateKeyBase64()), keySpecs); } public EdDSAPrivateKey getPrivateKey(KeyIdentifier id) { diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/Ed25519SignatureManager.java b/src/main/java/io/kamax/mxisd/storage/crypto/Ed25519SignatureManager.java index c5b0d50..cbb1f1f 100644 --- a/src/main/java/io/kamax/mxisd/storage/crypto/Ed25519SignatureManager.java +++ b/src/main/java/io/kamax/mxisd/storage/crypto/Ed25519SignatureManager.java @@ -43,7 +43,6 @@ public class Ed25519SignatureManager implements SignatureManager { Signature sign = sign(message); JsonObject keySignature = new JsonObject(); - // FIXME should create a signing key object what would give this ed and index values keySignature.addProperty(sign.getKey().getAlgorithm() + ":" + sign.getKey().getSerial(), sign.getSignature()); JsonObject signature = new JsonObject(); signature.add(domain, keySignature); @@ -53,7 +52,6 @@ public class Ed25519SignatureManager implements SignatureManager { @Override public Signature sign(JsonObject obj) { - return sign(MatrixJson.encodeCanonical(obj)); } diff --git a/src/main/java/io/kamax/mxisd/storage/ormlite/OrmLiteSqlStorage.java b/src/main/java/io/kamax/mxisd/storage/ormlite/OrmLiteSqlStorage.java index 0149a7b..31a956b 100644 --- a/src/main/java/io/kamax/mxisd/storage/ormlite/OrmLiteSqlStorage.java +++ b/src/main/java/io/kamax/mxisd/storage/ormlite/OrmLiteSqlStorage.java @@ -34,24 +34,18 @@ import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.dao.IThreePidSessionDao; import io.kamax.mxisd.storage.ormlite.dao.ASTransactionDao; +import io.kamax.mxisd.storage.ormlite.dao.HistoricalThreePidInviteIO; import io.kamax.mxisd.storage.ormlite.dao.ThreePidInviteIO; import io.kamax.mxisd.storage.ormlite.dao.ThreePidSessionDao; import org.apache.commons.lang.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.IOException; import java.sql.SQLException; import java.time.Instant; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Optional; +import java.util.*; public class OrmLiteSqlStorage implements IStorage { - private transient final Logger log = LoggerFactory.getLogger(OrmLiteSqlStorage.class); - @FunctionalInterface private interface Getter { @@ -67,6 +61,7 @@ public class OrmLiteSqlStorage implements IStorage { } private Dao invDao; + private Dao expInvDao; private Dao sessionDao; private Dao asTxnDao; @@ -86,6 +81,7 @@ public class OrmLiteSqlStorage implements IStorage { withCatcher(() -> { ConnectionSource connPool = new JdbcConnectionSource("jdbc:" + backend + ":" + path); invDao = createDaoAndTable(connPool, ThreePidInviteIO.class); + expInvDao = createDaoAndTable(connPool, HistoricalThreePidInviteIO.class); sessionDao = createDaoAndTable(connPool, ThreePidSessionDao.class); asTxnDao = createDaoAndTable(connPool, ASTransactionDao.class); }); @@ -150,6 +146,24 @@ public class OrmLiteSqlStorage implements IStorage { }); } + @Override + public void insertHistoricalInvite(IThreePidInviteReply data, String resolvedTo, Instant resolvedAt, boolean couldPublish) { + withCatcher(() -> { + HistoricalThreePidInviteIO io = new HistoricalThreePidInviteIO(data, resolvedTo, resolvedAt, couldPublish); + int updated = expInvDao.create(io); + if (updated != 1) { + throw new RuntimeException("Unexpected row count after DB action: " + updated); + } + + // Ugly, but it avoids touching the structure of the historical parent class + // and avoid any possible regression at this point. + updated = expInvDao.updateId(io, UUID.randomUUID().toString().replace("-", "")); + if (updated != 1) { + throw new RuntimeException("Unexpected row count after DB action: " + updated); + } + }); + } + @Override public Optional getThreePidSession(String sid) { return withCatcher(() -> Optional.ofNullable(sessionDao.queryForId(sid))); diff --git a/src/main/java/io/kamax/mxisd/storage/ormlite/dao/HistoricalThreePidInviteIO.java b/src/main/java/io/kamax/mxisd/storage/ormlite/dao/HistoricalThreePidInviteIO.java new file mode 100644 index 0000000..57935c7 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/storage/ormlite/dao/HistoricalThreePidInviteIO.java @@ -0,0 +1,72 @@ +/* + * 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.storage.ormlite.dao; + +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; +import io.kamax.mxisd.invitation.IThreePidInviteReply; + +import java.time.Instant; + +@DatabaseTable(tableName = "invite_3pid_history") +public class HistoricalThreePidInviteIO extends ThreePidInviteIO { + + @DatabaseField(canBeNull = false) + private String resolvedTo; + + @DatabaseField(canBeNull = false) + private long resolvedAt; + + @DatabaseField(canBeNull = false) + private boolean couldPublish; + + @DatabaseField(canBeNull = false) + private long publishAttempts = 1; // Placeholder for retry mechanism, if ever implemented + + public HistoricalThreePidInviteIO() { + // Needed for ORMLite + } + + public HistoricalThreePidInviteIO(IThreePidInviteReply data, String resolvedTo, Instant resolvedAt, boolean couldPublish) { + super(data); + + this.resolvedTo = resolvedTo; + this.resolvedAt = resolvedAt.toEpochMilli(); + this.couldPublish = couldPublish; + } + + public String getResolvedTo() { + return resolvedTo; + } + + public Instant getResolvedAt() { + return Instant.ofEpochMilli(resolvedAt); + } + + public boolean isCouldPublish() { + return couldPublish; + } + + public long getPublishAttempts() { + return publishAttempts; + } + +} From 93bd7354c2fdb819bbf5a4c22b3e0ee0787fa8b8 Mon Sep 17 00:00:00 2001 From: Max Dor Date: Fri, 1 Mar 2019 12:42:09 +0100 Subject: [PATCH 13/28] Improve Authentication doc --- docs/features/authentication.md | 50 ++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/docs/features/authentication.md b/docs/features/authentication.md index abce66a..f3d7998 100644 --- a/docs/features/authentication.md +++ b/docs/features/authentication.md @@ -21,7 +21,7 @@ It allows to use Identity stores configured in mxisd to authenticate users on yo Authentication is divided into two parts: - [Basic](#basic): authenticate with a regular username. -- [Advanced](#advanced): same as basic with extra ability to authenticate using a 3PID. +- [Advanced](#advanced): same as basic with extra abilities like authenticate using a 3PID or do username rewrite. ## Basic Authentication by username is possible by linking synapse and mxisd together using a specific module for synapse, also @@ -145,7 +145,49 @@ Your VirtualHost should now look similar to: ``` +##### nginx + +The specific configuration to add under the relevant `server`: + +```nginx +location /_matrix/client/r0/login { + proxy_pass http://localhost:8090; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; +} +``` + +Your `server` section should now look similar to: + +```nginx +server { + listen 443 ssl; + server_name matrix.example.org; + + # ... + + location /_matrix/client/r0/login { + proxy_pass http://localhost:8090; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + } + + location /_matrix/identity { + proxy_pass http://localhost:8090/_matrix/identity; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + } + + location /_matrix { + proxy_pass http://localhost:8008/_matrix; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + } +} +``` + #### DNS Overwrite + Just like you need to configure a reverse proxy to send client requests to mxisd, you also need to configure mxisd with the internal IP of the Homeserver so it can talk to it directly to integrate its directory search. @@ -165,6 +207,12 @@ In case the hostname is the same as your Matrix domain and `server.name` is not `value` is the base internal URL of the Homeserver, without any `/_matrix/..` or trailing `/`. +### Optional features + +The following features are available after you have a working Advanced setup: + +- Username rewrite: Allows you to rewrite the username of a regular login/pass authentication to a 3PID, that then gets resolved using the regular lookup process. Most common use case is to allow login with numerical usernames on synapse, which is not possible out of the box. + #### Username rewrite In mxisd config: ```yaml From dfedde0df674e4181da57aa884f4dca4d4aa2a3a Mon Sep 17 00:00:00 2001 From: Max Dor Date: Fri, 1 Mar 2019 15:16:19 +0100 Subject: [PATCH 14/28] Improve crypto - Re-organize packages to be consistent - Add Key store tests --- src/main/java/io/kamax/mxisd/Mxisd.java | 6 +- .../io/kamax/mxisd/crypto/CryptoFactory.java | 6 +- .../{storage => }/crypto/GenericKey.java | 2 +- .../crypto/GenericKeyIdentifier.java | 26 +++- .../kamax/mxisd/{storage => }/crypto/Key.java | 2 +- .../{storage => }/crypto/KeyAlgorithm.java | 2 +- .../{storage => }/crypto/KeyIdentifier.java | 2 +- .../{storage => }/crypto/KeyManager.java | 2 +- .../mxisd/{storage => }/crypto/KeyType.java | 2 +- .../crypto/RegularKeyIdentifier.java | 2 +- .../mxisd/{storage => }/crypto/Signature.java | 2 +- .../crypto/SignatureManager.java | 17 ++- .../crypto => crypto/ed25519}/Ed25519Key.java | 7 +- .../ed25519}/Ed25519KeyManager.java | 24 ++-- .../ed25519/Ed25519RegularKeyIdentifier.java} | 9 +- .../ed25519}/Ed25519SignatureManager.java | 5 +- .../v1/EphemeralKeyIsValidHandler.java | 4 +- .../handler/identity/v1/KeyGetHandler.java | 6 +- .../identity/v1/RegularKeyIsValidHandler.java | 4 +- .../identity/v1/SignEd25519Handler.java | 2 +- .../identity/v1/SingleLookupHandler.java | 2 +- .../identity/v1/StoreInviteHandler.java | 2 +- .../mxisd/invitation/InvitationManager.java | 2 +- .../mxisd/storage/crypto/FileKeyJson.java | 2 + .../mxisd/storage/crypto/FileKeyStore.java | 53 +++++--- .../kamax/mxisd/storage/crypto/KeyStore.java | 7 +- .../mxisd/storage/crypto/MemoryKeyStore.java | 22 +-- .../test/{storage => }/crypto/KeyTest.java | 2 +- .../crypto/SignatureManagerTest.java | 13 +- .../test/storage/crypto/FileKeyStoreTest.java | 42 ++++++ .../test/storage/crypto/KeyStoreTest.java | 128 ++++++++++++++++++ .../storage/crypto/MemoryKeyStoreTest.java | 33 +++++ 32 files changed, 362 insertions(+), 78 deletions(-) rename src/main/java/io/kamax/mxisd/{storage => }/crypto/GenericKey.java (97%) rename src/main/java/io/kamax/mxisd/{storage => }/crypto/GenericKeyIdentifier.java (65%) rename src/main/java/io/kamax/mxisd/{storage => }/crypto/Key.java (96%) rename src/main/java/io/kamax/mxisd/{storage => }/crypto/KeyAlgorithm.java (95%) rename src/main/java/io/kamax/mxisd/{storage => }/crypto/KeyIdentifier.java (97%) rename src/main/java/io/kamax/mxisd/{storage => }/crypto/KeyManager.java (96%) rename src/main/java/io/kamax/mxisd/{storage => }/crypto/KeyType.java (96%) rename src/main/java/io/kamax/mxisd/{storage => }/crypto/RegularKeyIdentifier.java (96%) rename src/main/java/io/kamax/mxisd/{storage => }/crypto/Signature.java (95%) rename src/main/java/io/kamax/mxisd/{storage => }/crypto/SignatureManager.java (73%) rename src/main/java/io/kamax/mxisd/{storage/crypto => crypto/ed25519}/Ed25519Key.java (86%) rename src/main/java/io/kamax/mxisd/{storage/crypto => crypto/ed25519}/Ed25519KeyManager.java (86%) rename src/main/java/io/kamax/mxisd/{storage/crypto/Ed2219RegularKeyIdentifier.java => crypto/ed25519/Ed25519RegularKeyIdentifier.java} (76%) rename src/main/java/io/kamax/mxisd/{storage/crypto => crypto/ed25519}/Ed25519SignatureManager.java (94%) rename src/test/java/io/kamax/mxisd/test/{storage => }/crypto/KeyTest.java (96%) rename src/test/java/io/kamax/mxisd/test/{storage => }/crypto/SignatureManagerTest.java (86%) create mode 100644 src/test/java/io/kamax/mxisd/test/storage/crypto/FileKeyStoreTest.java create mode 100644 src/test/java/io/kamax/mxisd/test/storage/crypto/KeyStoreTest.java create mode 100644 src/test/java/io/kamax/mxisd/test/storage/crypto/MemoryKeyStoreTest.java diff --git a/src/main/java/io/kamax/mxisd/Mxisd.java b/src/main/java/io/kamax/mxisd/Mxisd.java index 2410f70..6eb47ed 100644 --- a/src/main/java/io/kamax/mxisd/Mxisd.java +++ b/src/main/java/io/kamax/mxisd/Mxisd.java @@ -27,6 +27,9 @@ import io.kamax.mxisd.backend.IdentityStoreSupplier; import io.kamax.mxisd.backend.sql.synapse.Synapse; import io.kamax.mxisd.config.MxisdConfig; import io.kamax.mxisd.crypto.CryptoFactory; +import io.kamax.mxisd.crypto.KeyManager; +import io.kamax.mxisd.crypto.SignatureManager; +import io.kamax.mxisd.crypto.ed25519.Ed25519KeyManager; import io.kamax.mxisd.directory.DirectoryManager; import io.kamax.mxisd.directory.DirectoryProviders; import io.kamax.mxisd.dns.ClientDnsOverwrite; @@ -47,9 +50,6 @@ import io.kamax.mxisd.profile.ProfileProviders; import io.kamax.mxisd.registration.RegistrationManager; import io.kamax.mxisd.session.SessionManager; import io.kamax.mxisd.storage.IStorage; -import io.kamax.mxisd.storage.crypto.Ed25519KeyManager; -import io.kamax.mxisd.storage.crypto.KeyManager; -import io.kamax.mxisd.storage.crypto.SignatureManager; import io.kamax.mxisd.storage.ormlite.OrmLiteSqlStorage; import org.apache.commons.lang.StringUtils; import org.apache.http.impl.client.CloseableHttpClient; diff --git a/src/main/java/io/kamax/mxisd/crypto/CryptoFactory.java b/src/main/java/io/kamax/mxisd/crypto/CryptoFactory.java index 38f1263..253e978 100644 --- a/src/main/java/io/kamax/mxisd/crypto/CryptoFactory.java +++ b/src/main/java/io/kamax/mxisd/crypto/CryptoFactory.java @@ -21,7 +21,11 @@ package io.kamax.mxisd.crypto; import io.kamax.mxisd.config.KeyConfig; -import io.kamax.mxisd.storage.crypto.*; +import io.kamax.mxisd.crypto.ed25519.Ed25519KeyManager; +import io.kamax.mxisd.crypto.ed25519.Ed25519SignatureManager; +import io.kamax.mxisd.storage.crypto.FileKeyStore; +import io.kamax.mxisd.storage.crypto.KeyStore; +import io.kamax.mxisd.storage.crypto.MemoryKeyStore; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/GenericKey.java b/src/main/java/io/kamax/mxisd/crypto/GenericKey.java similarity index 97% rename from src/main/java/io/kamax/mxisd/storage/crypto/GenericKey.java rename to src/main/java/io/kamax/mxisd/crypto/GenericKey.java index 7d59079..685ac2d 100644 --- a/src/main/java/io/kamax/mxisd/storage/crypto/GenericKey.java +++ b/src/main/java/io/kamax/mxisd/crypto/GenericKey.java @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.storage.crypto; +package io.kamax.mxisd.crypto; public class GenericKey implements Key { diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/GenericKeyIdentifier.java b/src/main/java/io/kamax/mxisd/crypto/GenericKeyIdentifier.java similarity index 65% rename from src/main/java/io/kamax/mxisd/storage/crypto/GenericKeyIdentifier.java rename to src/main/java/io/kamax/mxisd/crypto/GenericKeyIdentifier.java index 0390eda..9dda144 100644 --- a/src/main/java/io/kamax/mxisd/storage/crypto/GenericKeyIdentifier.java +++ b/src/main/java/io/kamax/mxisd/crypto/GenericKeyIdentifier.java @@ -18,7 +18,11 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.storage.crypto; +package io.kamax.mxisd.crypto; + +import org.apache.commons.lang3.StringUtils; + +import java.util.Objects; public class GenericKeyIdentifier implements KeyIdentifier { @@ -31,7 +35,11 @@ public class GenericKeyIdentifier implements KeyIdentifier { } public GenericKeyIdentifier(KeyType type, String algo, String serial) { - this.type = type; + if (StringUtils.isAnyBlank(algo, serial)) { + throw new IllegalArgumentException("Aglorith and/or Serial cannot be blank"); + } + + this.type = Objects.requireNonNull(type); this.algo = algo; this.serial = serial; } @@ -51,4 +59,18 @@ public class GenericKeyIdentifier implements KeyIdentifier { return serial; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof GenericKeyIdentifier)) return false; + GenericKeyIdentifier that = (GenericKeyIdentifier) o; + return type == that.type && + algo.equals(that.algo) && + serial.equals(that.serial); + } + + @Override + public int hashCode() { + return Objects.hash(type, algo, serial); + } } diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/Key.java b/src/main/java/io/kamax/mxisd/crypto/Key.java similarity index 96% rename from src/main/java/io/kamax/mxisd/storage/crypto/Key.java rename to src/main/java/io/kamax/mxisd/crypto/Key.java index 9a98917..628e237 100644 --- a/src/main/java/io/kamax/mxisd/storage/crypto/Key.java +++ b/src/main/java/io/kamax/mxisd/crypto/Key.java @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.storage.crypto; +package io.kamax.mxisd.crypto; /** * A signing key diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/KeyAlgorithm.java b/src/main/java/io/kamax/mxisd/crypto/KeyAlgorithm.java similarity index 95% rename from src/main/java/io/kamax/mxisd/storage/crypto/KeyAlgorithm.java rename to src/main/java/io/kamax/mxisd/crypto/KeyAlgorithm.java index bb453ee..4e63d35 100644 --- a/src/main/java/io/kamax/mxisd/storage/crypto/KeyAlgorithm.java +++ b/src/main/java/io/kamax/mxisd/crypto/KeyAlgorithm.java @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.storage.crypto; +package io.kamax.mxisd.crypto; public interface KeyAlgorithm { diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/KeyIdentifier.java b/src/main/java/io/kamax/mxisd/crypto/KeyIdentifier.java similarity index 97% rename from src/main/java/io/kamax/mxisd/storage/crypto/KeyIdentifier.java rename to src/main/java/io/kamax/mxisd/crypto/KeyIdentifier.java index db28f25..1954d06 100644 --- a/src/main/java/io/kamax/mxisd/storage/crypto/KeyIdentifier.java +++ b/src/main/java/io/kamax/mxisd/crypto/KeyIdentifier.java @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.storage.crypto; +package io.kamax.mxisd.crypto; /** * Identifying data for a given Key. diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/KeyManager.java b/src/main/java/io/kamax/mxisd/crypto/KeyManager.java similarity index 96% rename from src/main/java/io/kamax/mxisd/storage/crypto/KeyManager.java rename to src/main/java/io/kamax/mxisd/crypto/KeyManager.java index 68a2cbc..a36f70b 100644 --- a/src/main/java/io/kamax/mxisd/storage/crypto/KeyManager.java +++ b/src/main/java/io/kamax/mxisd/crypto/KeyManager.java @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.storage.crypto; +package io.kamax.mxisd.crypto; import java.util.List; diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/KeyType.java b/src/main/java/io/kamax/mxisd/crypto/KeyType.java similarity index 96% rename from src/main/java/io/kamax/mxisd/storage/crypto/KeyType.java rename to src/main/java/io/kamax/mxisd/crypto/KeyType.java index 84565f6..2b63ba2 100644 --- a/src/main/java/io/kamax/mxisd/storage/crypto/KeyType.java +++ b/src/main/java/io/kamax/mxisd/crypto/KeyType.java @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.storage.crypto; +package io.kamax.mxisd.crypto; /** * Types of keys used by an Identity server. diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/RegularKeyIdentifier.java b/src/main/java/io/kamax/mxisd/crypto/RegularKeyIdentifier.java similarity index 96% rename from src/main/java/io/kamax/mxisd/storage/crypto/RegularKeyIdentifier.java rename to src/main/java/io/kamax/mxisd/crypto/RegularKeyIdentifier.java index b1b8721..3990587 100644 --- a/src/main/java/io/kamax/mxisd/storage/crypto/RegularKeyIdentifier.java +++ b/src/main/java/io/kamax/mxisd/crypto/RegularKeyIdentifier.java @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.storage.crypto; +package io.kamax.mxisd.crypto; public class RegularKeyIdentifier extends GenericKeyIdentifier { diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/Signature.java b/src/main/java/io/kamax/mxisd/crypto/Signature.java similarity index 95% rename from src/main/java/io/kamax/mxisd/storage/crypto/Signature.java rename to src/main/java/io/kamax/mxisd/crypto/Signature.java index 9174449..08d5887 100644 --- a/src/main/java/io/kamax/mxisd/storage/crypto/Signature.java +++ b/src/main/java/io/kamax/mxisd/crypto/Signature.java @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.storage.crypto; +package io.kamax.mxisd.crypto; public interface Signature { diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/SignatureManager.java b/src/main/java/io/kamax/mxisd/crypto/SignatureManager.java similarity index 73% rename from src/main/java/io/kamax/mxisd/storage/crypto/SignatureManager.java rename to src/main/java/io/kamax/mxisd/crypto/SignatureManager.java index 316cb2c..bcfdcfa 100644 --- a/src/main/java/io/kamax/mxisd/storage/crypto/SignatureManager.java +++ b/src/main/java/io/kamax/mxisd/crypto/SignatureManager.java @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.storage.crypto; +package io.kamax.mxisd.crypto; import com.google.gson.JsonObject; @@ -26,10 +26,17 @@ import java.nio.charset.StandardCharsets; public interface SignatureManager { + /** + * Sign the message and produce a signatures object that can directly be added to the object being signed. + * + * @param domain The domain under which the signature should be added + * @param message The message to sign + * @return The signatures object + */ JsonObject signMessageGson(String domain, String message); /** - * Sign the canonical form of a JSON object + * Sign the canonical form of a JSON object. * * @param obj The JSON object to canonicalize and sign * @return The signature @@ -37,17 +44,17 @@ public interface SignatureManager { Signature sign(JsonObject obj); /** - * Sign the message, using UTF-8 as decoding character set + * Sign the message, using UTF-8 as decoding character set. * * @param message The UTF-8 encoded message - * @return + * @return The signature */ default Signature sign(String message) { return sign(message.getBytes(StandardCharsets.UTF_8)); } /** - * Sign the data + * Sign the data. * * @param data The data to sign * @return The signature diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/Ed25519Key.java b/src/main/java/io/kamax/mxisd/crypto/ed25519/Ed25519Key.java similarity index 86% rename from src/main/java/io/kamax/mxisd/storage/crypto/Ed25519Key.java rename to src/main/java/io/kamax/mxisd/crypto/ed25519/Ed25519Key.java index af31552..1416101 100644 --- a/src/main/java/io/kamax/mxisd/storage/crypto/Ed25519Key.java +++ b/src/main/java/io/kamax/mxisd/crypto/ed25519/Ed25519Key.java @@ -18,7 +18,12 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.storage.crypto; +package io.kamax.mxisd.crypto.ed25519; + +import io.kamax.mxisd.crypto.GenericKeyIdentifier; +import io.kamax.mxisd.crypto.Key; +import io.kamax.mxisd.crypto.KeyAlgorithm; +import io.kamax.mxisd.crypto.KeyIdentifier; public class Ed25519Key implements Key { diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/Ed25519KeyManager.java b/src/main/java/io/kamax/mxisd/crypto/ed25519/Ed25519KeyManager.java similarity index 86% rename from src/main/java/io/kamax/mxisd/storage/crypto/Ed25519KeyManager.java rename to src/main/java/io/kamax/mxisd/crypto/ed25519/Ed25519KeyManager.java index 2b4dae5..ac64475 100644 --- a/src/main/java/io/kamax/mxisd/storage/crypto/Ed25519KeyManager.java +++ b/src/main/java/io/kamax/mxisd/crypto/ed25519/Ed25519KeyManager.java @@ -18,9 +18,11 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.storage.crypto; +package io.kamax.mxisd.crypto.ed25519; import io.kamax.matrix.codec.MxBase64; +import io.kamax.mxisd.crypto.*; +import io.kamax.mxisd.storage.crypto.KeyStore; import net.i2p.crypto.eddsa.EdDSAPrivateKey; import net.i2p.crypto.eddsa.EdDSAPublicKey; import net.i2p.crypto.eddsa.KeyPairGenerator; @@ -38,6 +40,7 @@ import java.nio.ByteBuffer; import java.security.KeyPair; import java.time.Instant; import java.util.List; +import java.util.stream.Collectors; public class Ed25519KeyManager implements KeyManager { @@ -51,7 +54,12 @@ public class Ed25519KeyManager implements KeyManager { this.store = store; if (!store.getCurrentKey().isPresent()) { - List keys = store.list(KeyType.Regular); + List keys = store.list(KeyType.Regular).stream() + .map(this::getKey) + .filter(Key::isValid) + .map(Key::getId) + .collect(Collectors.toList()); + if (keys.isEmpty()) { keys.add(generateKey(KeyType.Regular)); } @@ -60,17 +68,17 @@ public class Ed25519KeyManager implements KeyManager { } } - protected String generateId() { + private String generateId() { ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES); buffer.putLong(Instant.now().toEpochMilli() - 1546297200000L); // TS since 2019-01-01T00:00:00Z to keep IDs short return Base64.encodeBase64URLSafeString(buffer.array()) + RandomStringUtils.randomAlphanumeric(1); } - protected String getPrivateKeyBase64(EdDSAPrivateKey key) { + private String getPrivateKeyBase64(EdDSAPrivateKey key) { return MxBase64.encode(key.getSeed()); } - public EdDSAParameterSpec getKeySpecs() { + EdDSAParameterSpec getKeySpecs() { return keySpecs; } @@ -105,15 +113,15 @@ public class Ed25519KeyManager implements KeyManager { return store.get(id); } - public EdDSAPrivateKeySpec getPrivateKeySpecs(KeyIdentifier id) { + private EdDSAPrivateKeySpec getPrivateKeySpecs(KeyIdentifier id) { return new EdDSAPrivateKeySpec(Base64.decodeBase64(getKey(id).getPrivateKeyBase64()), keySpecs); } - public EdDSAPrivateKey getPrivateKey(KeyIdentifier id) { + EdDSAPrivateKey getPrivateKey(KeyIdentifier id) { return new EdDSAPrivateKey(getPrivateKeySpecs(id)); } - public EdDSAPublicKey getPublicKey(KeyIdentifier id) { + private EdDSAPublicKey getPublicKey(KeyIdentifier id) { EdDSAPrivateKeySpec privKeySpec = getPrivateKeySpecs(id); EdDSAPublicKeySpec pubKeySpec = new EdDSAPublicKeySpec(privKeySpec.getA(), keySpecs); return new EdDSAPublicKey(pubKeySpec); diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/Ed2219RegularKeyIdentifier.java b/src/main/java/io/kamax/mxisd/crypto/ed25519/Ed25519RegularKeyIdentifier.java similarity index 76% rename from src/main/java/io/kamax/mxisd/storage/crypto/Ed2219RegularKeyIdentifier.java rename to src/main/java/io/kamax/mxisd/crypto/ed25519/Ed25519RegularKeyIdentifier.java index 091728a..e0d5856 100644 --- a/src/main/java/io/kamax/mxisd/storage/crypto/Ed2219RegularKeyIdentifier.java +++ b/src/main/java/io/kamax/mxisd/crypto/ed25519/Ed25519RegularKeyIdentifier.java @@ -18,11 +18,14 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.storage.crypto; +package io.kamax.mxisd.crypto.ed25519; -public class Ed2219RegularKeyIdentifier extends RegularKeyIdentifier { +import io.kamax.mxisd.crypto.KeyAlgorithm; +import io.kamax.mxisd.crypto.RegularKeyIdentifier; - public Ed2219RegularKeyIdentifier(String serial) { +public class Ed25519RegularKeyIdentifier extends RegularKeyIdentifier { + + public Ed25519RegularKeyIdentifier(String serial) { super(KeyAlgorithm.Ed25519, serial); } diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/Ed25519SignatureManager.java b/src/main/java/io/kamax/mxisd/crypto/ed25519/Ed25519SignatureManager.java similarity index 94% rename from src/main/java/io/kamax/mxisd/storage/crypto/Ed25519SignatureManager.java rename to src/main/java/io/kamax/mxisd/crypto/ed25519/Ed25519SignatureManager.java index cbb1f1f..dab5a99 100644 --- a/src/main/java/io/kamax/mxisd/storage/crypto/Ed25519SignatureManager.java +++ b/src/main/java/io/kamax/mxisd/crypto/ed25519/Ed25519SignatureManager.java @@ -18,11 +18,14 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.storage.crypto; +package io.kamax.mxisd.crypto.ed25519; import com.google.gson.JsonObject; import io.kamax.matrix.codec.MxBase64; import io.kamax.matrix.json.MatrixJson; +import io.kamax.mxisd.crypto.KeyIdentifier; +import io.kamax.mxisd.crypto.Signature; +import io.kamax.mxisd.crypto.SignatureManager; import net.i2p.crypto.eddsa.EdDSAEngine; import java.security.InvalidKeyException; diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/EphemeralKeyIsValidHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/EphemeralKeyIsValidHandler.java index d39ccc1..0f70013 100644 --- a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/EphemeralKeyIsValidHandler.java +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/EphemeralKeyIsValidHandler.java @@ -20,9 +20,9 @@ package io.kamax.mxisd.http.undertow.handler.identity.v1; +import io.kamax.mxisd.crypto.KeyManager; +import io.kamax.mxisd.crypto.KeyType; import io.kamax.mxisd.http.IsAPIv1; -import io.kamax.mxisd.storage.crypto.KeyManager; -import io.kamax.mxisd.storage.crypto.KeyType; import io.undertow.server.HttpServerExchange; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/KeyGetHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/KeyGetHandler.java index 4a1ac69..8b1de81 100644 --- a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/KeyGetHandler.java +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/KeyGetHandler.java @@ -21,11 +21,11 @@ package io.kamax.mxisd.http.undertow.handler.identity.v1; import com.google.gson.JsonObject; +import io.kamax.mxisd.crypto.GenericKeyIdentifier; +import io.kamax.mxisd.crypto.KeyManager; +import io.kamax.mxisd.crypto.KeyType; import io.kamax.mxisd.http.IsAPIv1; import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; -import io.kamax.mxisd.storage.crypto.GenericKeyIdentifier; -import io.kamax.mxisd.storage.crypto.KeyManager; -import io.kamax.mxisd.storage.crypto.KeyType; import io.undertow.server.HttpServerExchange; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/RegularKeyIsValidHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/RegularKeyIsValidHandler.java index f181ed9..6b5167c 100644 --- a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/RegularKeyIsValidHandler.java +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/RegularKeyIsValidHandler.java @@ -20,9 +20,9 @@ package io.kamax.mxisd.http.undertow.handler.identity.v1; +import io.kamax.mxisd.crypto.KeyManager; +import io.kamax.mxisd.crypto.KeyType; import io.kamax.mxisd.http.IsAPIv1; -import io.kamax.mxisd.storage.crypto.KeyManager; -import io.kamax.mxisd.storage.crypto.KeyType; import io.undertow.server.HttpServerExchange; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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 index f76e7f5..8d6dc7e 100644 --- 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 @@ -26,11 +26,11 @@ 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.crypto.SignatureManager; 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; diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/SingleLookupHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/SingleLookupHandler.java index 8702b5d..d81d38c 100644 --- a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/SingleLookupHandler.java +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/SingleLookupHandler.java @@ -26,12 +26,12 @@ import io.kamax.matrix.json.GsonUtil; import io.kamax.matrix.json.MatrixJson; import io.kamax.mxisd.config.MxisdConfig; import io.kamax.mxisd.config.ServerConfig; +import io.kamax.mxisd.crypto.SignatureManager; import io.kamax.mxisd.http.IsAPIv1; import io.kamax.mxisd.http.io.identity.SingeLookupReplyJson; import io.kamax.mxisd.lookup.SingleLookupReply; import io.kamax.mxisd.lookup.SingleLookupRequest; import io.kamax.mxisd.lookup.strategy.LookupStrategy; -import io.kamax.mxisd.storage.crypto.SignatureManager; import io.undertow.server.HttpServerExchange; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/StoreInviteHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/StoreInviteHandler.java index 1d22ab4..11996a0 100644 --- a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/StoreInviteHandler.java +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/StoreInviteHandler.java @@ -26,6 +26,7 @@ import io.kamax.matrix.MatrixID; import io.kamax.matrix._MatrixID; import io.kamax.matrix.json.GsonUtil; import io.kamax.mxisd.config.ServerConfig; +import io.kamax.mxisd.crypto.KeyManager; import io.kamax.mxisd.exception.BadRequestException; import io.kamax.mxisd.http.IsAPIv1; import io.kamax.mxisd.http.io.identity.StoreInviteRequest; @@ -35,7 +36,6 @@ import io.kamax.mxisd.invitation.IThreePidInvite; import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.invitation.InvitationManager; import io.kamax.mxisd.invitation.ThreePidInvite; -import io.kamax.mxisd.storage.crypto.KeyManager; import io.undertow.server.HttpServerExchange; import io.undertow.util.QueryParameterUtils; import org.apache.commons.lang3.StringUtils; diff --git a/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java b/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java index f8004a7..362f867 100644 --- a/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java +++ b/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java @@ -29,6 +29,7 @@ import io.kamax.matrix.json.GsonUtil; import io.kamax.mxisd.config.InvitationConfig; import io.kamax.mxisd.config.MxisdConfig; import io.kamax.mxisd.config.ServerConfig; +import io.kamax.mxisd.crypto.*; import io.kamax.mxisd.dns.FederationDnsOverwrite; import io.kamax.mxisd.exception.BadRequestException; import io.kamax.mxisd.exception.ConfigurationException; @@ -40,7 +41,6 @@ import io.kamax.mxisd.lookup.strategy.LookupStrategy; import io.kamax.mxisd.notification.NotificationManager; import io.kamax.mxisd.profile.ProfileManager; import io.kamax.mxisd.storage.IStorage; -import io.kamax.mxisd.storage.crypto.*; import io.kamax.mxisd.storage.ormlite.dao.ThreePidInviteIO; import org.apache.commons.codec.binary.Base64; import org.apache.commons.io.IOUtils; diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/FileKeyJson.java b/src/main/java/io/kamax/mxisd/storage/crypto/FileKeyJson.java index 15164e9..5938a9d 100644 --- a/src/main/java/io/kamax/mxisd/storage/crypto/FileKeyJson.java +++ b/src/main/java/io/kamax/mxisd/storage/crypto/FileKeyJson.java @@ -20,6 +20,8 @@ package io.kamax.mxisd.storage.crypto; +import io.kamax.mxisd.crypto.Key; + public class FileKeyJson { public static FileKeyJson get(Key key) { diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/FileKeyStore.java b/src/main/java/io/kamax/mxisd/storage/crypto/FileKeyStore.java index 025353d..7ced08b 100644 --- a/src/main/java/io/kamax/mxisd/storage/crypto/FileKeyStore.java +++ b/src/main/java/io/kamax/mxisd/storage/crypto/FileKeyStore.java @@ -23,6 +23,7 @@ package io.kamax.mxisd.storage.crypto; import com.google.gson.JsonObject; import io.kamax.matrix.crypto.KeyFileStore; import io.kamax.matrix.json.GsonUtil; +import io.kamax.mxisd.crypto.*; import io.kamax.mxisd.exception.ObjectNotFoundException; import org.apache.commons.codec.binary.Base64; import org.apache.commons.io.FileUtils; @@ -54,31 +55,39 @@ public class FileKeyStore implements KeyStore { base = new File(path).getAbsoluteFile().toString(); File f = new File(base); - if (f.exists() && f.isFile()) { + if (!f.exists()) { try { - log.info("Found old key store format at {}, migrating...", base); - File oldStorePath = new File(f.toString() + ".backup-before-migration"); - FileUtils.moveFile(f, oldStorePath); FileUtils.forceMkdir(f); - - - String privKey = new KeyFileStore(oldStorePath.toString()).load().orElse(""); - if (StringUtils.isBlank(privKey)) { - log.info("Empty file, nothing to migrate"); - } else { - // We ensure this is valid Base64 data before migrating - Base64.decodeBase64(privKey); - - // We store the new key - add(new GenericKey(new GenericKeyIdentifier(KeyType.Regular, KeyAlgorithm.Ed25519, "0"), true, privKey)); - - log.info("Store migrated to new directory format"); - } } catch (IOException e) { - throw new RuntimeException("Unable to migrate store from old single file format to new directory format", e); + throw new RuntimeException("Unable to create key store"); } } else { - log.info("Key store is already in directory format"); + if (f.isFile()) { + try { + log.info("Found old key store format at {}, migrating...", base); + File oldStorePath = new File(f.toString() + ".backup-before-migration"); + FileUtils.moveFile(f, oldStorePath); + FileUtils.forceMkdir(f); + + + String privKey = new KeyFileStore(oldStorePath.toString()).load().orElse(""); + if (StringUtils.isBlank(privKey)) { + log.info("Empty file, nothing to migrate"); + } else { + // We ensure this is valid Base64 data before migrating + Base64.decodeBase64(privKey); + + // We store the new key + add(new GenericKey(new GenericKeyIdentifier(KeyType.Regular, KeyAlgorithm.Ed25519, "0"), true, privKey)); + + log.info("Store migrated to new directory format"); + } + } catch (IOException e) { + throw new RuntimeException("Unable to migrate store from old single file format to new directory format", e); + } + } else { + log.info("Key store is already in directory format"); + } } if (!f.isDirectory()) { @@ -207,6 +216,10 @@ public class FileKeyStore implements KeyStore { @Override public void setCurrentKey(KeyIdentifier id) throws IllegalArgumentException { + if (!has(id)) { + throw new IllegalArgumentException("Key " + id.getType() + ":" + id.getAlgorithm() + ":" + id.getSerial() + " is not known to the store"); + } + JsonObject json = new JsonObject(); json.addProperty("type", id.getType().name()); json.addProperty("algo", id.getAlgorithm()); diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/KeyStore.java b/src/main/java/io/kamax/mxisd/storage/crypto/KeyStore.java index 72c24b3..23c7277 100644 --- a/src/main/java/io/kamax/mxisd/storage/crypto/KeyStore.java +++ b/src/main/java/io/kamax/mxisd/storage/crypto/KeyStore.java @@ -20,6 +20,9 @@ package io.kamax.mxisd.storage.crypto; +import io.kamax.mxisd.crypto.Key; +import io.kamax.mxisd.crypto.KeyIdentifier; +import io.kamax.mxisd.crypto.KeyType; import io.kamax.mxisd.exception.ObjectNotFoundException; import java.util.List; @@ -84,9 +87,9 @@ public interface KeyStore { * Store the information of which key is the current signing key * * @param id The key identifier - * @throws ObjectNotFoundException If the key is not known to the store + * @throws IllegalArgumentException If the key is not known to the store */ - void setCurrentKey(KeyIdentifier id) throws ObjectNotFoundException; + void setCurrentKey(KeyIdentifier id) throws IllegalArgumentException; /** * Retrieve the previously stored information of which key is the current signing key, if any diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/MemoryKeyStore.java b/src/main/java/io/kamax/mxisd/storage/crypto/MemoryKeyStore.java index d2d8419..f67df68 100644 --- a/src/main/java/io/kamax/mxisd/storage/crypto/MemoryKeyStore.java +++ b/src/main/java/io/kamax/mxisd/storage/crypto/MemoryKeyStore.java @@ -20,18 +20,18 @@ package io.kamax.mxisd.storage.crypto; +import io.kamax.mxisd.crypto.*; import io.kamax.mxisd.exception.ObjectNotFoundException; -import org.apache.commons.lang3.StringUtils; import java.util.*; import java.util.concurrent.ConcurrentHashMap; public class MemoryKeyStore implements KeyStore { - private Map>> keys = new ConcurrentHashMap<>(); + private Map>> keys = new ConcurrentHashMap<>(); private KeyIdentifier current; - private Map getMap(KeyType type, String algo) { + private Map getMap(KeyType type, String algo) { return keys.computeIfAbsent(type, k -> new ConcurrentHashMap<>()).computeIfAbsent(algo, k -> new ConcurrentHashMap<>()); } @@ -56,23 +56,23 @@ public class MemoryKeyStore implements KeyStore { @Override public Key get(KeyIdentifier id) throws ObjectNotFoundException { - String data = getMap(id.getType(), id.getAlgorithm()).get(id.getSerial()); + FileKeyJson data = getMap(id.getType(), id.getAlgorithm()).get(id.getSerial()); if (Objects.isNull(data)) { throw new ObjectNotFoundException("Key", id.getType() + ":" + id.getAlgorithm() + ":" + id.getSerial()); } - return new GenericKey(new GenericKeyIdentifier(id), StringUtils.isEmpty(data), data); + return new GenericKey(new GenericKeyIdentifier(id), data.isValid(), data.getKey()); } private void set(Key key) { - String data = key.isValid() ? key.getPrivateKeyBase64() : ""; + FileKeyJson data = FileKeyJson.get(key); getMap(key.getId().getType(), key.getId().getAlgorithm()).put(key.getId().getSerial(), data); } @Override public void add(Key key) throws IllegalStateException { if (has(key.getId())) { - throw new IllegalStateException(); + throw new IllegalStateException("Key " + key.getId().getId() + " already exists"); } set(key); @@ -89,13 +89,17 @@ public class MemoryKeyStore implements KeyStore { @Override public void delete(KeyIdentifier id) throws ObjectNotFoundException { + if (!has(id)) { + throw new ObjectNotFoundException("Key", id.getType() + ":" + id.getAlgorithm() + ":" + id.getSerial()); + } + keys.computeIfAbsent(id.getType(), k -> new ConcurrentHashMap<>()).computeIfAbsent(id.getAlgorithm(), k -> new ConcurrentHashMap<>()).remove(id.getSerial()); } @Override - public void setCurrentKey(KeyIdentifier id) throws ObjectNotFoundException { + public void setCurrentKey(KeyIdentifier id) throws IllegalArgumentException { if (!has(id)) { - throw new ObjectNotFoundException("Key", id.getType() + ":" + id.getAlgorithm() + ":" + id.getSerial()); + throw new IllegalArgumentException("Key " + id.getType() + ":" + id.getAlgorithm() + ":" + id.getSerial() + " is not known to the store"); } current = id; diff --git a/src/test/java/io/kamax/mxisd/test/storage/crypto/KeyTest.java b/src/test/java/io/kamax/mxisd/test/crypto/KeyTest.java similarity index 96% rename from src/test/java/io/kamax/mxisd/test/storage/crypto/KeyTest.java rename to src/test/java/io/kamax/mxisd/test/crypto/KeyTest.java index 4c5a365..aa0fd34 100644 --- a/src/test/java/io/kamax/mxisd/test/storage/crypto/KeyTest.java +++ b/src/test/java/io/kamax/mxisd/test/crypto/KeyTest.java @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.test.storage.crypto; +package io.kamax.mxisd.test.crypto; public class KeyTest { diff --git a/src/test/java/io/kamax/mxisd/test/storage/crypto/SignatureManagerTest.java b/src/test/java/io/kamax/mxisd/test/crypto/SignatureManagerTest.java similarity index 86% rename from src/test/java/io/kamax/mxisd/test/storage/crypto/SignatureManagerTest.java rename to src/test/java/io/kamax/mxisd/test/crypto/SignatureManagerTest.java index 9c4a3ac..06d2fed 100644 --- a/src/test/java/io/kamax/mxisd/test/storage/crypto/SignatureManagerTest.java +++ b/src/test/java/io/kamax/mxisd/test/crypto/SignatureManagerTest.java @@ -18,12 +18,19 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.test.storage.crypto; +package io.kamax.mxisd.test.crypto; import com.google.gson.JsonObject; import io.kamax.matrix.json.GsonUtil; import io.kamax.matrix.json.MatrixJson; -import io.kamax.mxisd.storage.crypto.*; +import io.kamax.mxisd.crypto.Signature; +import io.kamax.mxisd.crypto.SignatureManager; +import io.kamax.mxisd.crypto.ed25519.Ed25519Key; +import io.kamax.mxisd.crypto.ed25519.Ed25519KeyManager; +import io.kamax.mxisd.crypto.ed25519.Ed25519RegularKeyIdentifier; +import io.kamax.mxisd.crypto.ed25519.Ed25519SignatureManager; +import io.kamax.mxisd.storage.crypto.KeyStore; +import io.kamax.mxisd.storage.crypto.MemoryKeyStore; import org.junit.BeforeClass; import org.junit.Test; @@ -36,7 +43,7 @@ public class SignatureManagerTest { private static SignatureManager signMgr; private static SignatureManager build(String keySeed) { - Ed25519Key key = new Ed25519Key(new Ed2219RegularKeyIdentifier("0"), keySeed); + Ed25519Key key = new Ed25519Key(new Ed25519RegularKeyIdentifier("0"), keySeed); KeyStore store = new MemoryKeyStore(); store.add(key); diff --git a/src/test/java/io/kamax/mxisd/test/storage/crypto/FileKeyStoreTest.java b/src/test/java/io/kamax/mxisd/test/storage/crypto/FileKeyStoreTest.java new file mode 100644 index 0000000..fdfed23 --- /dev/null +++ b/src/test/java/io/kamax/mxisd/test/storage/crypto/FileKeyStoreTest.java @@ -0,0 +1,42 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * 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.test.storage.crypto; + +import io.kamax.mxisd.storage.crypto.FileKeyStore; +import io.kamax.mxisd.storage.crypto.KeyStore; +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.util.UUID; + +public class FileKeyStoreTest extends KeyStoreTest { + + @Override + public KeyStore create() throws IOException { + String path = FileUtils.getTempDirectoryPath() + + "/mxisd-test-key-store-" + + UUID.randomUUID().toString().replace("-", ""); + FileUtils.forceDeleteOnExit(new File(path)); + return new FileKeyStore(path); + } + +} diff --git a/src/test/java/io/kamax/mxisd/test/storage/crypto/KeyStoreTest.java b/src/test/java/io/kamax/mxisd/test/storage/crypto/KeyStoreTest.java new file mode 100644 index 0000000..dd64dcf --- /dev/null +++ b/src/test/java/io/kamax/mxisd/test/storage/crypto/KeyStoreTest.java @@ -0,0 +1,128 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * 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.test.storage.crypto; + +import io.kamax.mxisd.crypto.*; +import io.kamax.mxisd.exception.ObjectNotFoundException; +import io.kamax.mxisd.storage.crypto.KeyStore; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.Before; +import org.junit.Test; + +import java.util.Optional; + +import static org.junit.Assert.*; + +public abstract class KeyStoreTest { + + private KeyStore store; + + public abstract KeyStore create() throws Exception; + + private Key generateRandomKey() { + KeyIdentifier keyId = new GenericKeyIdentifier(KeyType.Regular, "algo", RandomStringUtils.randomAlphanumeric(6)); + return new GenericKey(keyId, true, RandomStringUtils.randomAlphanumeric(48)); + } + + @Before + public void before() throws Exception { + store = create(); + } + + @Test + public void isEmptyAfterCreate() { + assertTrue(store.list().isEmpty()); + assertFalse(store.getCurrentKey().isPresent()); + } + + @Test + public void add() { + Key key = generateRandomKey(); + KeyIdentifier keyId = key.getId(); + + store.add(key); + + Key keyFromStore = store.get(keyId); + assertEquals(key.getId(), keyFromStore.getId()); + assertEquals(key.getPrivateKeyBase64(), keyFromStore.getPrivateKeyBase64()); + assertEquals(key.isValid(), keyFromStore.isValid()); + + assertTrue(store.list().contains(keyId)); + assertTrue(store.list(keyId.getType()).contains(keyId)); + } + + @Test(expected = IllegalStateException.class) + public void addDuplicate() { + Key key = generateRandomKey(); + store.add(key); + store.add(key); + } + + @Test + public void update() { + Key key = generateRandomKey(); + store.add(key); + + Key keyUpdated = new GenericKey(key.getId(), !key.isValid(), key.getPrivateKeyBase64()); + store.update(keyUpdated); + + Key keyFromStore = store.get(key.getId()); + assertEquals(key.getId(), keyFromStore.getId()); + assertEquals(key.getPrivateKeyBase64(), keyFromStore.getPrivateKeyBase64()); + assertEquals(key.isValid(), !keyFromStore.isValid()); + } + + @Test(expected = ObjectNotFoundException.class) + public void updateNonExisting() { + store.update(generateRandomKey()); + } + + @Test + public void delete() { + Key key = generateRandomKey(); + store.add(key); + + store.delete(key.getId()); + assertFalse(store.list().contains(key.getId())); + assertFalse(store.list(key.getId().getType()).contains(key.getId())); + } + + @Test(expected = ObjectNotFoundException.class) + public void deleteNonExisting() { + store.delete(generateRandomKey().getId()); + } + + @Test + public void setCurrentKey() { + Key key = generateRandomKey(); + store.add(key); + store.setCurrentKey(key.getId()); + Optional currentKey = store.getCurrentKey(); + assertTrue(currentKey.isPresent()); + assertEquals(currentKey.get(), key.getId()); + } + + @Test(expected = IllegalArgumentException.class) + public void setCurrentKeyNonExisting() { + store.setCurrentKey(generateRandomKey().getId()); + } + +} diff --git a/src/test/java/io/kamax/mxisd/test/storage/crypto/MemoryKeyStoreTest.java b/src/test/java/io/kamax/mxisd/test/storage/crypto/MemoryKeyStoreTest.java new file mode 100644 index 0000000..af918da --- /dev/null +++ b/src/test/java/io/kamax/mxisd/test/storage/crypto/MemoryKeyStoreTest.java @@ -0,0 +1,33 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * 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.test.storage.crypto; + +import io.kamax.mxisd.storage.crypto.KeyStore; +import io.kamax.mxisd.storage.crypto.MemoryKeyStore; + +public class MemoryKeyStoreTest extends KeyStoreTest { + + @Override + public KeyStore create() { + return new MemoryKeyStore(); + } + +} From 1307e3aa4373abcc4033c933b450b9eb31b9a4c8 Mon Sep 17 00:00:00 2001 From: Max Dor Date: Fri, 1 Mar 2019 15:18:47 +0100 Subject: [PATCH 15/28] Add missing javadoc --- src/main/java/io/kamax/mxisd/storage/crypto/KeyStore.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/KeyStore.java b/src/main/java/io/kamax/mxisd/storage/crypto/KeyStore.java index 23c7277..1dd73b9 100644 --- a/src/main/java/io/kamax/mxisd/storage/crypto/KeyStore.java +++ b/src/main/java/io/kamax/mxisd/storage/crypto/KeyStore.java @@ -73,6 +73,12 @@ public interface KeyStore { */ void add(Key key) throws IllegalStateException; + /** + * Update key properties in the store + * + * @param key They key to update. getId() will be used to identify the key to update + * @throws ObjectNotFoundException If no key is found for that identifier + */ void update(Key key) throws ObjectNotFoundException; /** From d5f913705662ef9b771cb5e03588b76465fd7d7e Mon Sep 17 00:00:00 2001 From: Max Dor Date: Fri, 1 Mar 2019 15:58:37 +0100 Subject: [PATCH 16/28] split into app svc processor --- .../java/io/kamax/mxisd/as/AppSvcManager.java | 66 ++--------- .../io/kamax/mxisd/as/EventTypeProcessor.java | 30 +++++ .../kamax/mxisd/as/MembershipProcessor.java | 111 ++++++++++++++++++ 3 files changed, 151 insertions(+), 56 deletions(-) create mode 100644 src/main/java/io/kamax/mxisd/as/EventTypeProcessor.java create mode 100644 src/main/java/io/kamax/mxisd/as/MembershipProcessor.java diff --git a/src/main/java/io/kamax/mxisd/as/AppSvcManager.java b/src/main/java/io/kamax/mxisd/as/AppSvcManager.java index 1dedac5..403beef 100644 --- a/src/main/java/io/kamax/mxisd/as/AppSvcManager.java +++ b/src/main/java/io/kamax/mxisd/as/AppSvcManager.java @@ -22,9 +22,7 @@ package io.kamax.mxisd.as; import com.google.gson.JsonObject; import io.kamax.matrix.MatrixID; -import io.kamax.matrix.ThreePidMedium; import io.kamax.matrix._MatrixID; -import io.kamax.matrix._ThreePid; import io.kamax.matrix.event.EventKey; import io.kamax.matrix.json.GsonUtil; import io.kamax.mxisd.backend.sql.synapse.Synapse; @@ -37,7 +35,7 @@ import io.kamax.mxisd.profile.ProfileManager; import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.ormlite.dao.ASTransactionDao; import io.kamax.mxisd.util.GsonParser; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,7 +44,6 @@ import java.time.Instant; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; public class AppSvcManager { @@ -56,21 +53,18 @@ public class AppSvcManager { private MatrixConfig cfg; private IStorage store; - private ProfileManager profiler; - private NotificationManager notif; - private Synapse synapse; + private Map processors = new HashMap<>(); private Map> transactionsInProgress; public AppSvcManager(MxisdConfig cfg, IStorage store, ProfileManager profiler, NotificationManager notif, Synapse synapse) { this.cfg = cfg.getMatrix(); this.store = store; - this.profiler = profiler; - this.notif = notif; - this.synapse = synapse; parser = new GsonParser(); transactionsInProgress = new ConcurrentHashMap<>(); + + processors.put("m.room.member", new MembershipProcessor(cfg.getMatrix(), profiler, notif, synapse)); } public AppSvcManager withToken(String token) { @@ -139,7 +133,7 @@ public class AppSvcManager { return future; } - public void processTransaction(List eventsJson) { + private void processTransaction(List eventsJson) { log.info("Processing transaction events: start"); eventsJson.forEach(ev -> { @@ -165,54 +159,14 @@ public class AppSvcManager { _MatrixID sender = MatrixID.asAcceptable(senderId); log.debug("Sender: {}", senderId); - if (!StringUtils.equals("m.room.member", GsonUtil.getStringOrNull(ev, "type"))) { - log.debug("This is not a room membership event, skipping"); + String evType = StringUtils.defaultIfBlank(EventKey.Type.getStringOrNull(ev), ""); + EventTypeProcessor p = processors.get(evType); + if (Objects.isNull(p)) { + log.debug("No event processor for type {}, skipping", evType); return; } - if (!StringUtils.equals("invite", GsonUtil.getStringOrNull(ev, "membership"))) { - log.debug("This is not an invite event, skipping"); - return; - } - - String inviteeId = EventKey.StateKey.getStringOrNull(ev); - if (StringUtils.isBlank(inviteeId)) { - log.warn("Invalid event: No invitee ID, skipping"); - return; - } - - _MatrixID invitee = MatrixID.asAcceptable(inviteeId); - if (!StringUtils.equals(invitee.getDomain(), cfg.getDomain())) { - log.debug("Ignoring invite for {}: not a local user"); - return; - } - - log.info("Got invite from {} to {}", senderId, inviteeId); - - boolean wasSent = false; - List<_ThreePid> tpids = profiler.getThreepids(invitee).stream() - .filter(tpid -> ThreePidMedium.Email.is(tpid.getMedium())) - .collect(Collectors.toList()); - log.info("Found {} email(s) in identity store for {}", tpids.size(), inviteeId); - - for (_ThreePid tpid : tpids) { - log.info("Found Email to notify about room invitation: {}", tpid.getAddress()); - Map properties = new HashMap<>(); - profiler.getDisplayName(sender).ifPresent(name -> properties.put("sender_display_name", name)); - try { - synapse.getRoomName(roomId).ifPresent(name -> properties.put("room_name", name)); - } catch (RuntimeException e) { - log.warn("Could not fetch room name", e); - log.info("Unable to fetch room name: Did you integrate your Homeserver as documented?"); - } - - IMatrixIdInvite inv = new MatrixIdInvite(roomId, sender, invitee, tpid.getMedium(), tpid.getAddress(), properties); - notif.sendForInvite(inv); - log.info("Notification for invite of {} sent to {}", inviteeId, tpid.getAddress()); - wasSent = true; - } - - log.info("Was notification sent? {}", wasSent); + p.process(ev, sender, roomId); log.debug("Event {}: processing end", evId); }); diff --git a/src/main/java/io/kamax/mxisd/as/EventTypeProcessor.java b/src/main/java/io/kamax/mxisd/as/EventTypeProcessor.java new file mode 100644 index 0000000..0d55602 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/as/EventTypeProcessor.java @@ -0,0 +1,30 @@ +/* + * 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.as; + +import com.google.gson.JsonObject; +import io.kamax.matrix._MatrixID; + +public interface EventTypeProcessor { + + void process(JsonObject ev, _MatrixID sender, String roomId); + +} diff --git a/src/main/java/io/kamax/mxisd/as/MembershipProcessor.java b/src/main/java/io/kamax/mxisd/as/MembershipProcessor.java new file mode 100644 index 0000000..c0d8643 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/as/MembershipProcessor.java @@ -0,0 +1,111 @@ +/* + * 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.as; + +import com.google.gson.JsonObject; +import io.kamax.matrix.MatrixID; +import io.kamax.matrix.ThreePidMedium; +import io.kamax.matrix._MatrixID; +import io.kamax.matrix._ThreePid; +import io.kamax.matrix.event.EventKey; +import io.kamax.matrix.json.GsonUtil; +import io.kamax.mxisd.backend.sql.synapse.Synapse; +import io.kamax.mxisd.config.MatrixConfig; +import io.kamax.mxisd.notification.NotificationManager; +import io.kamax.mxisd.profile.ProfileManager; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class MembershipProcessor implements EventTypeProcessor { + + private final static Logger log = LoggerFactory.getLogger(MembershipProcessor.class); + + private final MatrixConfig cfg; + private ProfileManager profiler; + private NotificationManager notif; + private Synapse synapse; + + public MembershipProcessor(MatrixConfig cfg, ProfileManager profiler, NotificationManager notif, Synapse synapse) { + this.cfg = cfg; + this.profiler = profiler; + this.notif = notif; + this.synapse = synapse; + } + + @Override + public void process(JsonObject ev, _MatrixID sender, String roomId) { + JsonObject content = EventKey.Content.findObj(ev).orElseGet(() -> { + log.debug("No content found, falling back to full object"); + return ev; + }); + + if (!StringUtils.equals("invite", GsonUtil.getStringOrNull(content, "membership"))) { + log.debug("This is not an invite event, skipping"); + return; + } + + String inviteeId = EventKey.StateKey.getStringOrNull(ev); + if (StringUtils.isBlank(inviteeId)) { + log.warn("Invalid event: No invitee ID, skipping"); + return; + } + + _MatrixID invitee = MatrixID.asAcceptable(inviteeId); + if (!StringUtils.equals(invitee.getDomain(), cfg.getDomain())) { + log.debug("Ignoring invite for {}: not a local user"); + return; + } + + log.info("Got invite from {} to {}", sender.getId(), inviteeId); + + boolean wasSent = false; + List<_ThreePid> tpids = profiler.getThreepids(invitee).stream() + .filter(tpid -> ThreePidMedium.Email.is(tpid.getMedium())) + .collect(Collectors.toList()); + log.info("Found {} email(s) in identity store for {}", tpids.size(), inviteeId); + + for (_ThreePid tpid : tpids) { + log.info("Found Email to notify about room invitation: {}", tpid.getAddress()); + Map properties = new HashMap<>(); + profiler.getDisplayName(sender).ifPresent(name -> properties.put("sender_display_name", name)); + try { + synapse.getRoomName(roomId).ifPresent(name -> properties.put("room_name", name)); + } catch (RuntimeException e) { + log.warn("Could not fetch room name", e); + log.info("Unable to fetch room name: Did you integrate your Homeserver as documented?"); + } + + IMatrixIdInvite inv = new MatrixIdInvite(roomId, sender, invitee, tpid.getMedium(), tpid.getAddress(), properties); + notif.sendForInvite(inv); + log.info("Notification for invite of {} sent to {}", inviteeId, tpid.getAddress()); + wasSent = true; + } + + log.info("Was notification sent? {}", wasSent); + } + +} From de92e98f7d28b3f8443495b6d7f85de5ba4b6fe2 Mon Sep 17 00:00:00 2001 From: Max Dor Date: Fri, 1 Mar 2019 17:51:33 +0100 Subject: [PATCH 17/28] Save work in progress --- .../java/io/kamax/mxisd/as/AppSvcManager.java | 27 +++ .../kamax/mxisd/as/MembershipProcessor.java | 3 +- .../mxisd/as/SynapseRegistrationYaml.java | 168 ++++++++++++++++++ .../io/kamax/mxisd/config/ListenerConfig.java | 75 +++++++- 4 files changed, 269 insertions(+), 4 deletions(-) create mode 100644 src/main/java/io/kamax/mxisd/as/SynapseRegistrationYaml.java diff --git a/src/main/java/io/kamax/mxisd/as/AppSvcManager.java b/src/main/java/io/kamax/mxisd/as/AppSvcManager.java index 403beef..79c7a01 100644 --- a/src/main/java/io/kamax/mxisd/as/AppSvcManager.java +++ b/src/main/java/io/kamax/mxisd/as/AppSvcManager.java @@ -35,11 +35,18 @@ import io.kamax.mxisd.profile.ProfileManager; import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.ormlite.dao.ASTransactionDao; import io.kamax.mxisd.util.GsonParser; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.introspector.BeanAccess; +import org.yaml.snakeyaml.representer.Representer; +import java.io.FileOutputStream; +import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.*; import java.util.concurrent.CompletableFuture; @@ -65,6 +72,26 @@ public class AppSvcManager { transactionsInProgress = new ConcurrentHashMap<>(); processors.put("m.room.member", new MembershipProcessor(cfg.getMatrix(), profiler, notif, synapse)); + + processConfig(); + } + + private void processConfig() { + String synapseRegFile = cfg.getListener().getSynapse().getRegistrationFile(); + if (StringUtils.isNotBlank(synapseRegFile)) { + SynapseRegistrationYaml syncCfg = SynapseRegistrationYaml.parse(cfg.getListener()); + + Representer rep = new Representer(); + rep.getPropertyUtils().setBeanAccess(BeanAccess.FIELD); + Yaml yaml = new Yaml(rep); + String synCfgRaw = yaml.dump(syncCfg); + + try { + IOUtils.write(synCfgRaw, new FileOutputStream(synapseRegFile), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException("Unable to write synapse appservice registration file", e); + } + } } public AppSvcManager withToken(String token) { diff --git a/src/main/java/io/kamax/mxisd/as/MembershipProcessor.java b/src/main/java/io/kamax/mxisd/as/MembershipProcessor.java index c0d8643..2e9b3cf 100644 --- a/src/main/java/io/kamax/mxisd/as/MembershipProcessor.java +++ b/src/main/java/io/kamax/mxisd/as/MembershipProcessor.java @@ -26,7 +26,6 @@ import io.kamax.matrix.ThreePidMedium; import io.kamax.matrix._MatrixID; import io.kamax.matrix._ThreePid; import io.kamax.matrix.event.EventKey; -import io.kamax.matrix.json.GsonUtil; import io.kamax.mxisd.backend.sql.synapse.Synapse; import io.kamax.mxisd.config.MatrixConfig; import io.kamax.mxisd.notification.NotificationManager; @@ -63,7 +62,7 @@ public class MembershipProcessor implements EventTypeProcessor { return ev; }); - if (!StringUtils.equals("invite", GsonUtil.getStringOrNull(content, "membership"))) { + if (!StringUtils.equals("invite", EventKey.Membership.getStringOrNull(content))) { log.debug("This is not an invite event, skipping"); return; } diff --git a/src/main/java/io/kamax/mxisd/as/SynapseRegistrationYaml.java b/src/main/java/io/kamax/mxisd/as/SynapseRegistrationYaml.java new file mode 100644 index 0000000..370fedb --- /dev/null +++ b/src/main/java/io/kamax/mxisd/as/SynapseRegistrationYaml.java @@ -0,0 +1,168 @@ +/* + * 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.as; + +import io.kamax.mxisd.config.ListenerConfig; + +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class SynapseRegistrationYaml { + + public static SynapseRegistrationYaml parse(ListenerConfig cfg) { + SynapseRegistrationYaml yaml = new SynapseRegistrationYaml(); + + yaml.setId("appservice-mxisd"); + yaml.setUrl(cfg.getUrl()); + yaml.setAsToken(cfg.getToken().getAs()); + yaml.setHsToken(cfg.getToken().getHs()); + yaml.setSenderLocalpart(cfg.getLocalpart()); + cfg.getUsers().forEach(template -> { + Namespace ns = new Namespace(); + ns.setRegex(template.getTemplate()); + ns.setExclusive(true); + yaml.getNamespaces().getUsers().add(ns); + }); + + return yaml; + } + + public static class Namespace { + + private String regex; + private boolean exclusive; + + public String getRegex() { + return regex; + } + + public void setRegex(String regex) { + this.regex = regex; + } + + public boolean isExclusive() { + return exclusive; + } + + public void setExclusive(boolean exclusive) { + this.exclusive = exclusive; + } + + } + + public static class Namespaces { + + private List users = new ArrayList<>(); + private List aliases = new ArrayList<>(); + private List rooms = new ArrayList<>(); + + public List getUsers() { + return users; + } + + public void setUsers(List users) { + this.users = users; + } + + public List getAliases() { + return aliases; + } + + public void setAliases(List aliases) { + this.aliases = aliases; + } + + public List getRooms() { + return rooms; + } + + public void setRooms(List rooms) { + this.rooms = rooms; + } + + } + + private String id; + private String url; + private String as_token; + private String hs_token; + private String sender_localpart; + private Namespaces namespaces = new Namespaces(); + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public void setUrl(URL url) { + if (Objects.isNull(url)) { + this.url = null; + } else { + this.url = url.toString(); + } + } + + public String getAsToken() { + return as_token; + } + + public void setAsToken(String as_token) { + this.as_token = as_token; + } + + public String getHsToken() { + return hs_token; + } + + public void setHsToken(String hs_token) { + this.hs_token = hs_token; + } + + public String getSenderLocalpart() { + return sender_localpart; + } + + public void setSenderLocalpart(String sender_localpart) { + this.sender_localpart = sender_localpart; + } + + public Namespaces getNamespaces() { + return namespaces; + } + + public void setNamespaces(Namespaces namespaces) { + this.namespaces = namespaces; + } + +} diff --git a/src/main/java/io/kamax/mxisd/config/ListenerConfig.java b/src/main/java/io/kamax/mxisd/config/ListenerConfig.java index 9a468b2..14d9937 100644 --- a/src/main/java/io/kamax/mxisd/config/ListenerConfig.java +++ b/src/main/java/io/kamax/mxisd/config/ListenerConfig.java @@ -25,9 +25,48 @@ import org.apache.commons.lang.StringUtils; import java.net.MalformedURLException; import java.net.URL; +import java.util.ArrayList; +import java.util.List; public class ListenerConfig { + public static class Synpase { + + private String registrationFile; + + public String getRegistrationFile() { + return registrationFile; + } + + public void setRegistrationFile(String registrationFile) { + this.registrationFile = registrationFile; + } + + } + + public static class UserTemplate { + + private String type = "regex"; + private String template; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getTemplate() { + return template; + } + + public void setTemplate(String template) { + this.template = template; + } + + } + public static class Token { private String as; @@ -51,10 +90,22 @@ public class ListenerConfig { } - private transient URL csUrl; + private String id = "appservice-mxisd"; private String url; - private String localpart; + private String localpart = "mxisd"; private Token token = new Token(); + private List users = new ArrayList<>(); + private Synpase synapse = new Synpase(); + + private transient URL csUrl; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } public URL getUrl() { return csUrl; @@ -80,6 +131,22 @@ public class ListenerConfig { this.token = token; } + public List getUsers() { + return users; + } + + public void setUsers(List users) { + this.users = users; + } + + public Synpase getSynapse() { + return synapse; + } + + public void setSynapse(Synpase synapse) { + this.synapse = synapse; + } + public void build() { try { if (StringUtils.isBlank(url)) { @@ -88,6 +155,10 @@ public class ListenerConfig { csUrl = new URL(url); + if (org.apache.commons.lang3.StringUtils.isBlank(getId())) { + throw new IllegalArgumentException("Matrix Listener ID is not set"); + } + if (StringUtils.isBlank(getLocalpart())) { throw new IllegalArgumentException("localpart for matrix listener is not set"); } From 254dc5684f27f32b59c8973e8fa43e00244e5551 Mon Sep 17 00:00:00 2001 From: Max Dor Date: Sat, 2 Mar 2019 03:19:47 +0100 Subject: [PATCH 18/28] Add mechanisms for 3PID invite expiration and AS integration - Integration with AS and a fallback user to decline expired invites (#120) - Rework of the AS feature to make it more independent/re-usable - Skeleton for admin interface via bot to manage invites (#138) --- src/main/java/io/kamax/mxisd/HttpMxisd.java | 13 +- src/main/java/io/kamax/mxisd/Mxisd.java | 4 +- .../java/io/kamax/mxisd/as/AppSvcManager.java | 144 +++++++-- .../kamax/mxisd/as/MembershipProcessor.java | 110 ------- .../mxisd/as/SynapseRegistrationYaml.java | 28 +- .../processor/MembershipEventProcessor.java | 176 +++++++++++ .../as/processor/MessageEventProcessor.java | 83 +++++ .../kamax/mxisd/config/AppServiceConfig.java | 287 ++++++++++++++++++ .../io/kamax/mxisd/config/ListenerConfig.java | 178 ----------- .../io/kamax/mxisd/config/MatrixConfig.java | 11 - .../io/kamax/mxisd/config/MxisdConfig.java | 10 + .../undertow/handler/as/v1/AsUserHandler.java | 46 +++ .../handler/status/VersionHandler.java | 2 +- .../mxisd/invitation/InvitationManager.java | 29 +- .../connector/email/EmailSmtpConnector.java | 3 +- 15 files changed, 771 insertions(+), 353 deletions(-) delete mode 100644 src/main/java/io/kamax/mxisd/as/MembershipProcessor.java create mode 100644 src/main/java/io/kamax/mxisd/as/processor/MembershipEventProcessor.java create mode 100644 src/main/java/io/kamax/mxisd/as/processor/MessageEventProcessor.java create mode 100644 src/main/java/io/kamax/mxisd/config/AppServiceConfig.java delete mode 100644 src/main/java/io/kamax/mxisd/config/ListenerConfig.java create mode 100644 src/main/java/io/kamax/mxisd/http/undertow/handler/as/v1/AsUserHandler.java diff --git a/src/main/java/io/kamax/mxisd/HttpMxisd.java b/src/main/java/io/kamax/mxisd/HttpMxisd.java index b6e34bc..0df6252 100644 --- a/src/main/java/io/kamax/mxisd/HttpMxisd.java +++ b/src/main/java/io/kamax/mxisd/HttpMxisd.java @@ -26,6 +26,7 @@ import io.kamax.mxisd.http.undertow.handler.OptionsHandler; import io.kamax.mxisd.http.undertow.handler.SaneHandler; import io.kamax.mxisd.http.undertow.handler.as.v1.AsNotFoundHandler; import io.kamax.mxisd.http.undertow.handler.as.v1.AsTransactionHandler; +import io.kamax.mxisd.http.undertow.handler.as.v1.AsUserHandler; import io.kamax.mxisd.http.undertow.handler.auth.RestAuthHandler; import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginGetHandler; import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginHandler; @@ -66,8 +67,11 @@ public class HttpMxisd { m.start(); HttpHandler helloHandler = SaneHandler.around(new HelloHandler()); - HttpHandler asNotFoundHandler = SaneHandler.around(new AsNotFoundHandler(m.getAs())); + + HttpHandler asUserHandler = SaneHandler.around(new AsUserHandler(m.getAs())); HttpHandler asTxnHandler = SaneHandler.around(new AsTransactionHandler(m.getAs())); + HttpHandler asNotFoundHandler = SaneHandler.around(new AsNotFoundHandler(m.getAs())); + HttpHandler storeInvHandler = SaneHandler.around(new StoreInviteHandler(m.getConfig().getServer(), m.getInvitationManager(), m.getKeyManager())); HttpHandler sessValidateHandler = SaneHandler.around(new SessionValidateHandler(m.getSession(), m.getConfig().getServer(), m.getConfig().getView())); @@ -117,11 +121,12 @@ public class HttpMxisd { .post(RoomInviteHandler.Path, SaneHandler.around(new RoomInviteHandler(m.getHttpClient(), m.getClientDns(), m.getInvitationManager()))) // Application Service endpoints - .get("/_matrix/app/v1/users/**", asNotFoundHandler) - .get("/users/**", asNotFoundHandler) // Legacy endpoint + .get(AsUserHandler.Path, asUserHandler) .get("/_matrix/app/v1/rooms/**", asNotFoundHandler) - .get("/rooms/**", asNotFoundHandler) // Legacy endpoint .put(AsTransactionHandler.Path, asTxnHandler) + + .get("/users/{" + AsUserHandler.ID + "}", asUserHandler) // Legacy endpoint + .get("/rooms/**", asNotFoundHandler) // Legacy endpoint .put("/transactions/{" + AsTransactionHandler.ID + "}", asTxnHandler) // Legacy endpoint // Banned endpoints diff --git a/src/main/java/io/kamax/mxisd/Mxisd.java b/src/main/java/io/kamax/mxisd/Mxisd.java index 6eb47ed..4908eae 100644 --- a/src/main/java/io/kamax/mxisd/Mxisd.java +++ b/src/main/java/io/kamax/mxisd/Mxisd.java @@ -59,7 +59,9 @@ import java.util.ServiceLoader; public class Mxisd { + public static final String Name = StringUtils.defaultIfBlank(Mxisd.class.getPackage().getImplementationTitle(), "mxisd"); public static final String Version = StringUtils.defaultIfBlank(Mxisd.class.getPackage().getImplementationVersion(), "UNKNOWN"); + public static final String Agent = Name + "/" + Version; private MxisdConfig cfg; @@ -89,7 +91,7 @@ public class Mxisd { private void build() { httpClient = HttpClients.custom() - .setUserAgent("mxisd/" + Version) + .setUserAgent(Agent) .setMaxConnPerRoute(Integer.MAX_VALUE) .setMaxConnTotal(Integer.MAX_VALUE) .build(); diff --git a/src/main/java/io/kamax/mxisd/as/AppSvcManager.java b/src/main/java/io/kamax/mxisd/as/AppSvcManager.java index 79c7a01..6d6260f 100644 --- a/src/main/java/io/kamax/mxisd/as/AppSvcManager.java +++ b/src/main/java/io/kamax/mxisd/as/AppSvcManager.java @@ -23,11 +23,16 @@ package io.kamax.mxisd.as; import com.google.gson.JsonObject; import io.kamax.matrix.MatrixID; import io.kamax.matrix._MatrixID; +import io.kamax.matrix.client.MatrixClientContext; +import io.kamax.matrix.client.as.MatrixApplicationServiceClient; import io.kamax.matrix.event.EventKey; import io.kamax.matrix.json.GsonUtil; +import io.kamax.mxisd.as.processor.MembershipEventProcessor; +import io.kamax.mxisd.as.processor.MessageEventProcessor; import io.kamax.mxisd.backend.sql.synapse.Synapse; -import io.kamax.mxisd.config.MatrixConfig; +import io.kamax.mxisd.config.AppServiceConfig; import io.kamax.mxisd.config.MxisdConfig; +import io.kamax.mxisd.exception.ConfigurationException; import io.kamax.mxisd.exception.HttpMatrixException; import io.kamax.mxisd.exception.NotAllowedException; import io.kamax.mxisd.notification.NotificationManager; @@ -36,6 +41,7 @@ import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.ormlite.dao.ASTransactionDao; import io.kamax.mxisd.util.GsonParser; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,65 +60,145 @@ import java.util.concurrent.ConcurrentHashMap; public class AppSvcManager { - private transient final Logger log = LoggerFactory.getLogger(AppSvcManager.class); + private static final Logger log = LoggerFactory.getLogger(AppSvcManager.class); - private final GsonParser parser; + private final AppServiceConfig cfg; + private final IStorage store; + private final GsonParser parser = new GsonParser(); - private MatrixConfig cfg; - private IStorage store; + private MatrixApplicationServiceClient client; private Map processors = new HashMap<>(); + private Map> transactionsInProgress = new ConcurrentHashMap<>(); - private Map> transactionsInProgress; - - public AppSvcManager(MxisdConfig cfg, IStorage store, ProfileManager profiler, NotificationManager notif, Synapse synapse) { - this.cfg = cfg.getMatrix(); + public AppSvcManager(MxisdConfig mxisdCfg, IStorage store, ProfileManager profiler, NotificationManager notif, Synapse synapse) { + this.cfg = mxisdCfg.getAppsvc(); this.store = store; - parser = new GsonParser(); - transactionsInProgress = new ConcurrentHashMap<>(); + /* + We process the configuration to make sure all is fine and setting default values if needed + */ - processors.put("m.room.member", new MembershipProcessor(cfg.getMatrix(), profiler, notif, synapse)); + // By default, the feature is enabled + cfg.setEnabled(ObjectUtils.defaultIfNull(cfg.isEnabled(), false)); - processConfig(); + if (!cfg.isEnabled()) { + return; + } + + if (Objects.isNull(cfg.getEndpoint().getToAS().getUrl())) { + throw new ConfigurationException("App Service: Endpoint: To AS: URL"); + } + + if (Objects.isNull(cfg.getEndpoint().getToAS().getToken())) { + throw new ConfigurationException("App Service: Endpoint: To AS: Token", "Must be set, even if to an empty string"); + } + + if (Objects.isNull(cfg.getEndpoint().getToHS().getUrl())) { + throw new ConfigurationException("App Service: Endpoint: To HS: URL"); + } + + if (Objects.isNull(cfg.getEndpoint().getToHS().getToken())) { + throw new ConfigurationException("App Service: Endpoint: To HS: Token", "Must be set, even if to an empty string"); + } + + // We set a default status for each feature individually + cfg.getFeature().getAdmin().setEnabled(ObjectUtils.defaultIfNull(cfg.getFeature().getAdmin().getEnabled(), cfg.isEnabled())); + cfg.getFeature().setCleanExpiredInvite(ObjectUtils.defaultIfNull(cfg.getFeature().getCleanExpiredInvite(), cfg.isEnabled())); + cfg.getFeature().setInviteById(ObjectUtils.defaultIfNull(cfg.getFeature().getInviteById(), false)); + + if (cfg.getFeature().getAdmin().getEnabled()) { + if (StringUtils.isBlank(cfg.getUser().getMain())) { + throw new ConfigurationException("App Service admin feature is enabled, but no main user configured"); + } + + if (cfg.getUser().getMain().startsWith("@") || cfg.getUser().getMain().contains(":")) { + throw new ConfigurationException("App Service: Users: Main ID: Is not a localpart"); + } + } + + if (cfg.getFeature().getCleanExpiredInvite()) { + if (StringUtils.isBlank(cfg.getUser().getInviteExpired())) { + throw new ConfigurationException("App Service user for Expired Invite is not set"); + } + + if (cfg.getUser().getMain().startsWith("@") || cfg.getUser().getMain().contains(":")) { + throw new ConfigurationException("App Service: Users: Expired Invite ID: Is not a localpart"); + } + } + + MatrixClientContext mxContext = new MatrixClientContext(); + mxContext.setDomain(mxisdCfg.getMatrix().getDomain()); + mxContext.setToken(cfg.getEndpoint().getToHS().getToken()); + mxContext.setHsBaseUrl(cfg.getEndpoint().getToHS().getUrl()); + client = new MatrixApplicationServiceClient(mxContext); + + processors.put("m.room.member", new MembershipEventProcessor(client, mxisdCfg, profiler, notif, synapse)); + processors.put("m.room.message", new MessageEventProcessor(client)); + + processSynapseConfig(mxisdCfg); } - private void processConfig() { - String synapseRegFile = cfg.getListener().getSynapse().getRegistrationFile(); - if (StringUtils.isNotBlank(synapseRegFile)) { - SynapseRegistrationYaml syncCfg = SynapseRegistrationYaml.parse(cfg.getListener()); + private void processSynapseConfig(MxisdConfig cfg) { + String synapseRegFile = cfg.getAppsvc().getRegistration().getSynapse().getFile(); - Representer rep = new Representer(); - rep.getPropertyUtils().setBeanAccess(BeanAccess.FIELD); - Yaml yaml = new Yaml(rep); - String synCfgRaw = yaml.dump(syncCfg); + if (StringUtils.isBlank(synapseRegFile)) { + log.info("No synapse registration file path given - skipping generation..."); + return; + } - try { - IOUtils.write(synCfgRaw, new FileOutputStream(synapseRegFile), StandardCharsets.UTF_8); - } catch (IOException e) { - throw new RuntimeException("Unable to write synapse appservice registration file", e); - } + SynapseRegistrationYaml syncCfg = SynapseRegistrationYaml.parse(cfg.getAppsvc(), cfg.getMatrix().getDomain()); + + Representer rep = new Representer(); + rep.getPropertyUtils().setBeanAccess(BeanAccess.FIELD); + Yaml yaml = new Yaml(rep); + + // SnakeYAML set the type of object on the first line, which can fail to be parsed on synapse + // We therefore need to split the resulting string, remove the first line, and then write it + List lines = new ArrayList<>(Arrays.asList(yaml.dump(syncCfg).split("\\R+"))); + if (StringUtils.equals(lines.get(0), "!!" + SynapseRegistrationYaml.class.getCanonicalName())) { + lines.remove(0); + } + + try (FileOutputStream os = new FileOutputStream(synapseRegFile)) { + IOUtils.writeLines(lines, System.lineSeparator(), os, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException("Unable to write synapse appservice registration file", e); + } + } + + private void ensureEnabled() { + if (!cfg.isEnabled()) { + throw new HttpMatrixException(503, "M_NOT_AVAILABLE", "This feature is disabled"); } } public AppSvcManager withToken(String token) { + ensureEnabled(); + if (StringUtils.isBlank(token)) { throw new HttpMatrixException(401, "M_UNAUTHORIZED", "No HS token"); } - if (!StringUtils.equals(cfg.getListener().getToken().getHs(), token)) { + if (!StringUtils.equals(cfg.getEndpoint().getToAS().getToken(), token)) { throw new NotAllowedException("Invalid HS token"); } return this; } + public void processUser(String userId) { + client.createUser(MatrixID.asAcceptable(userId).getLocalPart()); + } + public CompletableFuture processTransaction(String txnId, InputStream is) { + ensureEnabled(); + if (StringUtils.isEmpty(txnId)) { throw new IllegalArgumentException("Transaction ID cannot be empty"); } synchronized (this) { - Optional dao = store.getTransactionResult(cfg.getListener().getLocalpart(), txnId); + Optional dao = store.getTransactionResult(cfg.getUser().getMain(), txnId); if (dao.isPresent()) { log.info("AS Transaction {} already processed - returning computed result", txnId); return CompletableFuture.completedFuture(dao.get().getResult()); @@ -143,7 +229,7 @@ public class AppSvcManager { try { log.info("Saving transaction details to store"); - store.insertTransactionResult(cfg.getListener().getLocalpart(), txnId, end, result); + store.insertTransactionResult(cfg.getUser().getMain(), txnId, end, result); } finally { log.debug("Removing CompletedFuture from transaction map"); transactionsInProgress.remove(txnId); diff --git a/src/main/java/io/kamax/mxisd/as/MembershipProcessor.java b/src/main/java/io/kamax/mxisd/as/MembershipProcessor.java deleted file mode 100644 index 2e9b3cf..0000000 --- a/src/main/java/io/kamax/mxisd/as/MembershipProcessor.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * 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.as; - -import com.google.gson.JsonObject; -import io.kamax.matrix.MatrixID; -import io.kamax.matrix.ThreePidMedium; -import io.kamax.matrix._MatrixID; -import io.kamax.matrix._ThreePid; -import io.kamax.matrix.event.EventKey; -import io.kamax.mxisd.backend.sql.synapse.Synapse; -import io.kamax.mxisd.config.MatrixConfig; -import io.kamax.mxisd.notification.NotificationManager; -import io.kamax.mxisd.profile.ProfileManager; -import org.apache.commons.lang.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -public class MembershipProcessor implements EventTypeProcessor { - - private final static Logger log = LoggerFactory.getLogger(MembershipProcessor.class); - - private final MatrixConfig cfg; - private ProfileManager profiler; - private NotificationManager notif; - private Synapse synapse; - - public MembershipProcessor(MatrixConfig cfg, ProfileManager profiler, NotificationManager notif, Synapse synapse) { - this.cfg = cfg; - this.profiler = profiler; - this.notif = notif; - this.synapse = synapse; - } - - @Override - public void process(JsonObject ev, _MatrixID sender, String roomId) { - JsonObject content = EventKey.Content.findObj(ev).orElseGet(() -> { - log.debug("No content found, falling back to full object"); - return ev; - }); - - if (!StringUtils.equals("invite", EventKey.Membership.getStringOrNull(content))) { - log.debug("This is not an invite event, skipping"); - return; - } - - String inviteeId = EventKey.StateKey.getStringOrNull(ev); - if (StringUtils.isBlank(inviteeId)) { - log.warn("Invalid event: No invitee ID, skipping"); - return; - } - - _MatrixID invitee = MatrixID.asAcceptable(inviteeId); - if (!StringUtils.equals(invitee.getDomain(), cfg.getDomain())) { - log.debug("Ignoring invite for {}: not a local user"); - return; - } - - log.info("Got invite from {} to {}", sender.getId(), inviteeId); - - boolean wasSent = false; - List<_ThreePid> tpids = profiler.getThreepids(invitee).stream() - .filter(tpid -> ThreePidMedium.Email.is(tpid.getMedium())) - .collect(Collectors.toList()); - log.info("Found {} email(s) in identity store for {}", tpids.size(), inviteeId); - - for (_ThreePid tpid : tpids) { - log.info("Found Email to notify about room invitation: {}", tpid.getAddress()); - Map properties = new HashMap<>(); - profiler.getDisplayName(sender).ifPresent(name -> properties.put("sender_display_name", name)); - try { - synapse.getRoomName(roomId).ifPresent(name -> properties.put("room_name", name)); - } catch (RuntimeException e) { - log.warn("Could not fetch room name", e); - log.info("Unable to fetch room name: Did you integrate your Homeserver as documented?"); - } - - IMatrixIdInvite inv = new MatrixIdInvite(roomId, sender, invitee, tpid.getMedium(), tpid.getAddress(), properties); - notif.sendForInvite(inv); - log.info("Notification for invite of {} sent to {}", inviteeId, tpid.getAddress()); - wasSent = true; - } - - log.info("Was notification sent? {}", wasSent); - } - -} diff --git a/src/main/java/io/kamax/mxisd/as/SynapseRegistrationYaml.java b/src/main/java/io/kamax/mxisd/as/SynapseRegistrationYaml.java index 370fedb..5ab3988 100644 --- a/src/main/java/io/kamax/mxisd/as/SynapseRegistrationYaml.java +++ b/src/main/java/io/kamax/mxisd/as/SynapseRegistrationYaml.java @@ -20,7 +20,7 @@ package io.kamax.mxisd.as; -import io.kamax.mxisd.config.ListenerConfig; +import io.kamax.mxisd.config.AppServiceConfig; import java.net.URL; import java.util.ArrayList; @@ -29,20 +29,28 @@ import java.util.Objects; public class SynapseRegistrationYaml { - public static SynapseRegistrationYaml parse(ListenerConfig cfg) { + public static SynapseRegistrationYaml parse(AppServiceConfig cfg, String domain) { SynapseRegistrationYaml yaml = new SynapseRegistrationYaml(); - yaml.setId("appservice-mxisd"); - yaml.setUrl(cfg.getUrl()); - yaml.setAsToken(cfg.getToken().getAs()); - yaml.setHsToken(cfg.getToken().getHs()); - yaml.setSenderLocalpart(cfg.getLocalpart()); - cfg.getUsers().forEach(template -> { + yaml.setId(cfg.getRegistration().getSynapse().getId()); + yaml.setUrl(cfg.getEndpoint().getToAS().getUrl()); + yaml.setAsToken(cfg.getEndpoint().getToHS().getToken()); + yaml.setHsToken(cfg.getEndpoint().getToAS().getToken()); + yaml.setSenderLocalpart(cfg.getUser().getMain()); + + if (cfg.getFeature().getCleanExpiredInvite()) { Namespace ns = new Namespace(); - ns.setRegex(template.getTemplate()); ns.setExclusive(true); + ns.setRegex("@" + cfg.getUser().getInviteExpired() + ":" + domain); yaml.getNamespaces().getUsers().add(ns); - }); + } + + if (cfg.getFeature().getInviteById()) { + Namespace ns = new Namespace(); + ns.setExclusive(false); + ns.setRegex("@*:" + domain); + yaml.getNamespaces().getUsers().add(ns); + } return yaml; } diff --git a/src/main/java/io/kamax/mxisd/as/processor/MembershipEventProcessor.java b/src/main/java/io/kamax/mxisd/as/processor/MembershipEventProcessor.java new file mode 100644 index 0000000..009611a --- /dev/null +++ b/src/main/java/io/kamax/mxisd/as/processor/MembershipEventProcessor.java @@ -0,0 +1,176 @@ +/* + * 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.as.processor; + +import com.google.gson.JsonObject; +import io.kamax.matrix.MatrixID; +import io.kamax.matrix.ThreePidMedium; +import io.kamax.matrix._MatrixID; +import io.kamax.matrix._ThreePid; +import io.kamax.matrix.client.as.MatrixApplicationServiceClient; +import io.kamax.matrix.event.EventKey; +import io.kamax.matrix.hs._MatrixRoom; +import io.kamax.mxisd.as.EventTypeProcessor; +import io.kamax.mxisd.as.IMatrixIdInvite; +import io.kamax.mxisd.as.MatrixIdInvite; +import io.kamax.mxisd.backend.sql.synapse.Synapse; +import io.kamax.mxisd.config.MxisdConfig; +import io.kamax.mxisd.notification.NotificationManager; +import io.kamax.mxisd.profile.ProfileManager; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class MembershipEventProcessor implements EventTypeProcessor { + + private final static Logger log = LoggerFactory.getLogger(MembershipEventProcessor.class); + + private MatrixApplicationServiceClient client; + + private final MxisdConfig cfg; + private ProfileManager profiler; + private NotificationManager notif; + private Synapse synapse; + + public MembershipEventProcessor( + MatrixApplicationServiceClient client, + MxisdConfig cfg, + ProfileManager profiler, + NotificationManager notif, + Synapse synapse + ) { + this.client = client; + this.cfg = cfg; + this.profiler = profiler; + this.notif = notif; + this.synapse = synapse; + } + + @Override + public void process(JsonObject ev, _MatrixID sender, String roomId) { + JsonObject content = EventKey.Content.findObj(ev).orElseGet(() -> { + log.debug("No content found, falling back to full object"); + return ev; + }); + + String targetId = EventKey.StateKey.getStringOrNull(ev); + if (StringUtils.isBlank(targetId)) { + log.warn("Invalid event: No invitee ID, skipping"); + return; + } + + _MatrixID target = MatrixID.asAcceptable(targetId); + if (!StringUtils.equals(target.getDomain(), cfg.getMatrix().getDomain())) { + log.debug("Ignoring invite for {}: not a local user"); + return; + } + + log.info("Got membership event from {} to {} for room {}", sender.getId(), targetId, roomId); + + boolean isForMainUser = StringUtils.equals(target.getLocalPart(), cfg.getAppsvc().getUser().getMain()); + boolean isForExpInvUser = StringUtils.equals(target.getLocalPart(), cfg.getAppsvc().getUser().getInviteExpired()); + boolean isUs = isForMainUser || isForExpInvUser; + + if (StringUtils.equals("join", EventKey.Membership.getStringOrNull(content))) { + if (!isForMainUser) { + log.warn("We joined the room {} for another identity as the main user, which is not supported. Leaving...", roomId); + + client.getUser(target.getLocalPart()).getRoom(roomId).tryLeave().ifPresent(err -> { + log.warn("Could not decline invite to room {}: {} - {}", roomId, err.getErrcode(), err.getError()); + }); + } + } else if (StringUtils.equals("invite", EventKey.Membership.getStringOrNull(content))) { + if (isForMainUser) { + processForMainUser(roomId, sender); + } else if (isForExpInvUser) { + processForExpiredInviteUser(roomId, target); + } else { + processForUserIdInvite(roomId, sender, target); + } + } else if (StringUtils.equals("leave", EventKey.Membership.getStringOrNull(content))) { + _MatrixRoom room = client.getRoom(roomId); + if (!isUs && room.getJoinedUsers().size() == 1) { + // TODO we need to find out if this is only us remaining and leave the room if so, using the right client for it + } + } else { + log.debug("This is not an supported type of membership event, skipping"); + } + } + + private void processForMainUser(String roomId, _MatrixID sender) { + List roles = profiler.getRoles(sender); + if (Collections.disjoint(roles, cfg.getAppsvc().getFeature().getAdmin().getAllowedRoles())) { + log.info("Sender does not have any of the required roles, denying"); + client.getRoom(roomId).tryLeave().ifPresent(err -> { + log.warn("Could not decline invite to room {}: {} - {}", roomId, err.getErrcode(), err.getError()); + }); + } else { + client.getRoom(roomId).tryJoin().ifPresent(err -> { + log.warn("Could not join room {}: {} - {}", roomId, err.getErrcode(), err.getError()); + client.getRoom(roomId).tryLeave().ifPresent(err1 -> { + log.warn("Could not decline invite to room {} after failed join: {} - {}", roomId, err1.getErrcode(), err1.getError()); + }); + }); + } + } + + private void processForExpiredInviteUser(String roomId, _MatrixID invitee) { + client.getUser(invitee.getLocalPart()).getRoom(roomId).tryLeave().ifPresent(err -> { + log.warn("Could not decline invite to room {}: {} - {}", roomId, err.getErrcode(), err.getError()); + }); + } + + private void processForUserIdInvite(String roomId, _MatrixID sender, _MatrixID invitee) { + String inviteeId = invitee.getId(); + + boolean wasSent = false; + List<_ThreePid> tpids = profiler.getThreepids(invitee).stream() + .filter(tpid -> ThreePidMedium.Email.is(tpid.getMedium())) + .collect(Collectors.toList()); + log.info("Found {} email(s) in identity store for {}", tpids.size(), inviteeId); + + for (_ThreePid tpid : tpids) { + log.info("Found Email to notify about room invitation: {}", tpid.getAddress()); + Map properties = new HashMap<>(); + profiler.getDisplayName(sender).ifPresent(name -> properties.put("sender_display_name", name)); + try { + synapse.getRoomName(roomId).ifPresent(name -> properties.put("room_name", name)); + } catch (RuntimeException e) { + log.warn("Could not fetch room name", e); + log.info("Unable to fetch room name: Did you integrate your Homeserver as documented?"); + } + + IMatrixIdInvite inv = new MatrixIdInvite(roomId, sender, invitee, tpid.getMedium(), tpid.getAddress(), properties); + notif.sendForInvite(inv); + log.info("Notification for invite of {} sent to {}", inviteeId, tpid.getAddress()); + wasSent = true; + } + + log.info("Was notification sent? {}", wasSent); + } + +} diff --git a/src/main/java/io/kamax/mxisd/as/processor/MessageEventProcessor.java b/src/main/java/io/kamax/mxisd/as/processor/MessageEventProcessor.java new file mode 100644 index 0000000..8053943 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/as/processor/MessageEventProcessor.java @@ -0,0 +1,83 @@ +/* + * 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.as.processor; + +import com.google.gson.JsonObject; +import io.kamax.matrix._MatrixID; +import io.kamax.matrix._MatrixUserProfile; +import io.kamax.matrix.client.as.MatrixApplicationServiceClient; +import io.kamax.matrix.hs._MatrixRoom; +import io.kamax.matrix.json.event.MatrixJsonRoomMessageEvent; +import io.kamax.mxisd.as.EventTypeProcessor; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.stream.Collectors; + +public class MessageEventProcessor implements EventTypeProcessor { + + private static final Logger log = LoggerFactory.getLogger(MessageEventProcessor.class); + + private final MatrixApplicationServiceClient client; + + public MessageEventProcessor(MatrixApplicationServiceClient client) { + this.client = client; + } + + @Override + public void process(JsonObject ev, _MatrixID sender, String roomId) { + _MatrixRoom room = client.getRoom(roomId); + List<_MatrixID> joinedUsers = room.getJoinedUsers().stream().map(_MatrixUserProfile::getId).collect(Collectors.toList()); + boolean joinedWithMainUser = joinedUsers.contains(client.getWhoAmI()); + boolean isAdminPrivate = joinedWithMainUser && joinedUsers.size() == 2; + + MatrixJsonRoomMessageEvent msgEv = new MatrixJsonRoomMessageEvent(ev); + if (StringUtils.equals("m.notice", msgEv.getBodyType())) { + log.info("Ignoring automated message"); + return; + } + + if (!StringUtils.equals("m.text", msgEv.getBodyType())) { + log.info("Unsupported message event type: {}", msgEv.getBodyType()); + return; + } + + String command = msgEv.getBody(); + if (!isAdminPrivate) { + if (StringUtils.equals(command, "!mxisd")) { + // TODO show help + } + if (!StringUtils.startsWith(command, "!mxisd ")) { + // Not for us + return; + } + + command = command.substring("!mxisd ".length()); + } + + if (StringUtils.equals("ping", command)) { + room.sendText("Pong!"); + } + } + +} diff --git a/src/main/java/io/kamax/mxisd/config/AppServiceConfig.java b/src/main/java/io/kamax/mxisd/config/AppServiceConfig.java new file mode 100644 index 0000000..24f39d3 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/config/AppServiceConfig.java @@ -0,0 +1,287 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2018 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.config; + +import io.kamax.mxisd.Mxisd; +import io.kamax.mxisd.exception.ConfigurationException; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class AppServiceConfig { + + public static class Users { + + private String main = "mxisd"; + private String inviteExpired = "_mxisd_invite-expired"; + + public String getMain() { + return main; + } + + public void setMain(String main) { + this.main = main; + } + + public String getInviteExpired() { + return inviteExpired; + } + + public void setInviteExpired(String inviteExpired) { + this.inviteExpired = inviteExpired; + } + + public void build() { + // no-op + } + + } + + public static class Endpoint { + + private String url; + private String token; + + private transient URL cUrl; + + public URL getUrl() { + return cUrl; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public void build() { + if (Objects.isNull(url)) { + return; + } + + try { + cUrl = new URL(url); + } catch (MalformedURLException e) { + throw new ConfigurationException("AppService endpoint(s) URL definition"); + } + } + + } + + public static class Endpoints { + + private Endpoint toAS = new Endpoint(); + private Endpoint toHS = new Endpoint(); + + public Endpoint getToAS() { + return toAS; + } + + public void setToAS(Endpoint toAS) { + this.toAS = toAS; + } + + public Endpoint getToHS() { + return toHS; + } + + public void setToHS(Endpoint toHS) { + this.toHS = toHS; + } + + public void build() { + toAS.build(); + toHS.build(); + } + + } + + public static class Synapse { + + private String id = "appservice-" + Mxisd.Name; + private String file; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getFile() { + return file; + } + + public void setFile(String file) { + this.file = file; + } + + public void build() { + // no-op + } + + } + + public static class Registration { + + private Synapse synapse = new Synapse(); + + public Synapse getSynapse() { + return synapse; + } + + public void setSynapse(Synapse synapse) { + this.synapse = synapse; + } + + public void build() { + synapse.build(); + } + + } + + public static class AdminFeature { + + private Boolean enabled; + private List allowedRoles = new ArrayList<>(); + + public Boolean getEnabled() { + return enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public List getAllowedRoles() { + return allowedRoles; + } + + public void setAllowedRoles(List allowedRoles) { + this.allowedRoles = allowedRoles; + } + + public void build() { + // no-op + } + + } + + public static class Features { + + private AdminFeature admin = new AdminFeature(); + private Boolean inviteById; + private Boolean cleanExpiredInvite; + + public AdminFeature getAdmin() { + return admin; + } + + public void setAdmin(AdminFeature admin) { + this.admin = admin; + } + + public Boolean getInviteById() { + return inviteById; + } + + public void setInviteById(Boolean inviteById) { + this.inviteById = inviteById; + } + + public Boolean getCleanExpiredInvite() { + return cleanExpiredInvite; + } + + public void setCleanExpiredInvite(Boolean cleanExpiredInvite) { + this.cleanExpiredInvite = cleanExpiredInvite; + } + + public void build() { + admin.build(); + } + + } + + private Boolean enabled; + private Features feature = new Features(); + private Endpoints endpoint = new Endpoints(); + private Registration registration = new Registration(); + private Users user = new Users(); + + public Boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public Features getFeature() { + return feature; + } + + public void setFeature(Features feature) { + this.feature = feature; + } + + public Endpoints getEndpoint() { + return endpoint; + } + + public void setEndpoint(Endpoints endpoint) { + this.endpoint = endpoint; + } + + public Registration getRegistration() { + return registration; + } + + public void setRegistration(Registration registration) { + this.registration = registration; + } + + public Users getUser() { + return user; + } + + public void setUser(Users user) { + this.user = user; + } + + public void build() { + endpoint.build(); + feature.build(); + registration.build(); + user.build(); + } + +} diff --git a/src/main/java/io/kamax/mxisd/config/ListenerConfig.java b/src/main/java/io/kamax/mxisd/config/ListenerConfig.java deleted file mode 100644 index 14d9937..0000000 --- a/src/main/java/io/kamax/mxisd/config/ListenerConfig.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * mxisd - Matrix Identity Server Daemon - * Copyright (C) 2018 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.config; - -import io.kamax.mxisd.exception.ConfigurationException; -import org.apache.commons.lang.StringUtils; - -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; -import java.util.List; - -public class ListenerConfig { - - public static class Synpase { - - private String registrationFile; - - public String getRegistrationFile() { - return registrationFile; - } - - public void setRegistrationFile(String registrationFile) { - this.registrationFile = registrationFile; - } - - } - - public static class UserTemplate { - - private String type = "regex"; - private String template; - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public String getTemplate() { - return template; - } - - public void setTemplate(String template) { - this.template = template; - } - - } - - public static class Token { - - private String as; - private String hs; - - public String getAs() { - return as; - } - - public void setAs(String as) { - this.as = as; - } - - public String getHs() { - return hs; - } - - public void setHs(String hs) { - this.hs = hs; - } - - } - - private String id = "appservice-mxisd"; - private String url; - private String localpart = "mxisd"; - private Token token = new Token(); - private List users = new ArrayList<>(); - private Synpase synapse = new Synpase(); - - private transient URL csUrl; - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public URL getUrl() { - return csUrl; - } - - public void setUrl(String url) { - this.url = url; - } - - public String getLocalpart() { - return localpart; - } - - public void setLocalpart(String localpart) { - this.localpart = localpart; - } - - public Token getToken() { - return token; - } - - public void setToken(Token token) { - this.token = token; - } - - public List getUsers() { - return users; - } - - public void setUsers(List users) { - this.users = users; - } - - public Synpase getSynapse() { - return synapse; - } - - public void setSynapse(Synpase synapse) { - this.synapse = synapse; - } - - public void build() { - try { - if (StringUtils.isBlank(url)) { - return; - } - - csUrl = new URL(url); - - if (org.apache.commons.lang3.StringUtils.isBlank(getId())) { - throw new IllegalArgumentException("Matrix Listener ID is not set"); - } - - if (StringUtils.isBlank(getLocalpart())) { - throw new IllegalArgumentException("localpart for matrix listener is not set"); - } - - if (StringUtils.isBlank(getToken().getAs())) { - throw new IllegalArgumentException("AS token is not set"); - } - - if (StringUtils.isBlank(getToken().getHs())) { - throw new IllegalArgumentException("HS token is not set"); - } - } catch (MalformedURLException e) { - throw new ConfigurationException(e); - } - } - -} diff --git a/src/main/java/io/kamax/mxisd/config/MatrixConfig.java b/src/main/java/io/kamax/mxisd/config/MatrixConfig.java index 0fe9db1..c66751a 100644 --- a/src/main/java/io/kamax/mxisd/config/MatrixConfig.java +++ b/src/main/java/io/kamax/mxisd/config/MatrixConfig.java @@ -63,7 +63,6 @@ public class MatrixConfig { private String domain; private Identity identity = new Identity(); - private ListenerConfig listener = new ListenerConfig(); public String getDomain() { return domain; @@ -81,14 +80,6 @@ public class MatrixConfig { this.identity = identity; } - public ListenerConfig getListener() { - return listener; - } - - public void setListener(ListenerConfig listener) { - this.listener = listener; - } - public void build() { log.info("--- Matrix config ---"); @@ -99,8 +90,6 @@ public class MatrixConfig { log.info("Domain: {}", getDomain()); log.info("Identity:"); log.info("\tServers: {}", GsonUtil.get().toJson(identity.getServers())); - - listener.build(); } } diff --git a/src/main/java/io/kamax/mxisd/config/MxisdConfig.java b/src/main/java/io/kamax/mxisd/config/MxisdConfig.java index 3fb5ffa..4a0dc4b 100644 --- a/src/main/java/io/kamax/mxisd/config/MxisdConfig.java +++ b/src/main/java/io/kamax/mxisd/config/MxisdConfig.java @@ -83,6 +83,7 @@ public class MxisdConfig { } + private AppServiceConfig appsvc = new AppServiceConfig(); private AuthenticationConfig auth = new AuthenticationConfig(); private DirectoryConfig directory = new DirectoryConfig(); private Dns dns = new Dns(); @@ -108,6 +109,14 @@ public class MxisdConfig { private ViewConfig view = new ViewConfig(); private WordpressConfig wordpress = new WordpressConfig(); + public AppServiceConfig getAppsvc() { + return appsvc; + } + + public void setAppsvc(AppServiceConfig appsvc) { + this.appsvc = appsvc; + } + public AuthenticationConfig getAuth() { return auth; } @@ -306,6 +315,7 @@ public class MxisdConfig { log.debug("server.name is empty, using matrix.domain"); } + getAppsvc().build(); getAuth().build(); getDirectory().build(); getExec().build(); diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/as/v1/AsUserHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/as/v1/AsUserHandler.java new file mode 100644 index 0000000..a37af1f --- /dev/null +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/as/v1/AsUserHandler.java @@ -0,0 +1,46 @@ +/* + * 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.as.v1; + +import io.kamax.mxisd.as.AppSvcManager; +import io.undertow.server.HttpServerExchange; + +import java.util.LinkedList; + +public class AsUserHandler extends ApplicationServiceHandler { + + public static final String ID = "userId"; + public static final String Path = "/_matrix/app/v1/users/{" + ID + "}"; + + private final AppSvcManager app; + + public AsUserHandler(AppSvcManager app) { + this.app = app; + } + + @Override + public void handleRequest(HttpServerExchange exchange) { + String userId = exchange.getQueryParameters().getOrDefault(ID, new LinkedList<>()).peekFirst(); + app.withToken(getToken(exchange)).processUser(userId); + respondJson(exchange, "{}"); + } + +} diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/status/VersionHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/status/VersionHandler.java index 5118789..ab4cf6a 100644 --- a/src/main/java/io/kamax/mxisd/http/undertow/handler/status/VersionHandler.java +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/status/VersionHandler.java @@ -34,7 +34,7 @@ public class VersionHandler extends BasicHttpHandler { public VersionHandler() { JsonObject server = new JsonObject(); - server.addProperty("name", "mxisd"); + server.addProperty("name", Mxisd.Name); server.addProperty("version", Mxisd.Version); body = GsonUtil.getPrettyForLog(GsonUtil.makeObj("server", server)); diff --git a/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java b/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java index 362f867..8dfcc2a 100644 --- a/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java +++ b/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java @@ -186,7 +186,12 @@ public class InvitationManager { } if (StringUtils.isBlank(cfg.getInvite().getExpiration().getResolveTo())) { - throw new ConfigurationException("Invitation expiration resolution target cannot be empty/blank"); + String localpart = cfg.getAppsvc().getUser().getInviteExpired(); + if (StringUtils.isBlank(localpart)) { + throw new ConfigurationException("Could not compute the Invitation expiration resolution target from App service user: not set"); + } + + cfg.getInvite().getExpiration().setResolveTo(MatrixID.asAcceptable(localpart, cfg.getMatrix().getDomain()).getId()); } try { @@ -395,8 +400,8 @@ public class InvitationManager { Instant ts = Instant.ofEpochMilli(Long.parseLong(tsRaw)); Instant targetTs = ts.plusSeconds(cfg.getExpiration().getAfter() * 60); Instant now = Instant.now(); - log.debug("Invite {} - Created at {} - Expire at {} - Current time is {}", reply.getId(), ts, targetTs, now); - if (targetTs.isBefore(Instant.now())) { + log.debug("Invite {} - Created at {} - Expires at {} - Current time is {}", reply.getId(), ts, targetTs, now); + if (targetTs.isAfter(now)) { log.debug("Invite {} has not expired yet, skipping", reply.getId()); continue; } @@ -494,6 +499,7 @@ public class InvitationManager { Instant resolvedAt = Instant.now(); boolean couldPublish = false; + boolean shouldArchive = true; try { log.info("Posting onBind event to {}", req.getURI()); CloseableHttpResponse response = client.execute(req); @@ -501,7 +507,12 @@ public class InvitationManager { log.info("Answer code: {}", statusCode); if (statusCode >= 300 && statusCode != 403) { log.info("Answer body: {}", IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8)); - log.warn("HS returned an error. Invite can be found in historical storage for manual re-processing"); + log.warn("HS returned an error."); + + shouldArchive = statusCode != 502; + if (shouldArchive) { + log.info("Invite can be found in historical storage for manual re-processing"); + } } else { couldPublish = true; if (statusCode == 403) { @@ -512,10 +523,12 @@ public class InvitationManager { } catch (IOException e) { log.warn("Unable to tell HS {} about invite being mapped", domain, e); } finally { - synchronized (this) { - storage.insertHistoricalInvite(reply, mxid, resolvedAt, couldPublish); - removeInvite(reply); - log.info("Moved invite {} to historical table", reply.getId()); + if (shouldArchive) { + synchronized (this) { + storage.insertHistoricalInvite(reply, mxid, resolvedAt, couldPublish); + removeInvite(reply); + log.info("Moved invite {} to historical table", reply.getId()); + } } } }).start(); diff --git a/src/main/java/io/kamax/mxisd/threepid/connector/email/EmailSmtpConnector.java b/src/main/java/io/kamax/mxisd/threepid/connector/email/EmailSmtpConnector.java index 24d840f..af3c3f6 100644 --- a/src/main/java/io/kamax/mxisd/threepid/connector/email/EmailSmtpConnector.java +++ b/src/main/java/io/kamax/mxisd/threepid/connector/email/EmailSmtpConnector.java @@ -22,6 +22,7 @@ package io.kamax.mxisd.threepid.connector.email; import com.sun.mail.smtp.SMTPTransport; import io.kamax.matrix.ThreePidMedium; +import io.kamax.mxisd.Mxisd; import io.kamax.mxisd.config.threepid.connector.EmailSmtpConfig; import io.kamax.mxisd.exception.FeatureNotAvailable; import io.kamax.mxisd.exception.InternalServerError; @@ -92,7 +93,7 @@ public class EmailSmtpConnector implements EmailConnector { try { InternetAddress sender = new InternetAddress(senderAddress, senderName); MimeMessage msg = new MimeMessage(session, IOUtils.toInputStream(content, StandardCharsets.UTF_8)); - msg.setHeader("X-Mailer", "mxisd"); // FIXME set version + msg.setHeader("X-Mailer", Mxisd.Agent); msg.setSentDate(new Date()); msg.setFrom(sender); msg.setRecipients(Message.RecipientType.TO, recipient); From 53c85d22481fa8b48e12c995297477410704c8ad Mon Sep 17 00:00:00 2001 From: Max Dor Date: Sun, 3 Mar 2019 03:44:38 +0100 Subject: [PATCH 19/28] Package/Class refactoring (no-op) --- src/main/java/io/kamax/mxisd/as/AppSvcManager.java | 5 +++-- .../as/processor/{ => event}/MembershipEventProcessor.java | 6 +++--- .../as/processor/{ => event}/MessageEventProcessor.java | 2 +- .../as/{ => registration}/SynapseRegistrationYaml.java | 2 +- .../io/kamax/mxisd/{as => invitation}/IMatrixIdInvite.java | 3 +-- .../io/kamax/mxisd/{as => invitation}/MatrixIdInvite.java | 2 +- .../io/kamax/mxisd/notification/NotificationHandler.java | 2 +- .../io/kamax/mxisd/notification/NotificationManager.java | 2 +- .../generator/GenericTemplateNotificationGenerator.java | 2 +- .../mxisd/threepid/generator/NotificationGenerator.java | 2 +- .../generator/PlaceholderNotificationGenerator.java | 2 +- .../threepid/notification/GenericNotificationHandler.java | 2 +- .../email/EmailSendGridNotificationHandler.java | 2 +- .../mxisd/test/notification/EmailNotificationTest.java | 2 +- 14 files changed, 18 insertions(+), 18 deletions(-) rename src/main/java/io/kamax/mxisd/as/processor/{ => event}/MembershipEventProcessor.java (98%) rename src/main/java/io/kamax/mxisd/as/processor/{ => event}/MessageEventProcessor.java (98%) rename src/main/java/io/kamax/mxisd/as/{ => registration}/SynapseRegistrationYaml.java (99%) rename src/main/java/io/kamax/mxisd/{as => invitation}/IMatrixIdInvite.java (92%) rename src/main/java/io/kamax/mxisd/{as => invitation}/MatrixIdInvite.java (98%) diff --git a/src/main/java/io/kamax/mxisd/as/AppSvcManager.java b/src/main/java/io/kamax/mxisd/as/AppSvcManager.java index 6d6260f..fd55223 100644 --- a/src/main/java/io/kamax/mxisd/as/AppSvcManager.java +++ b/src/main/java/io/kamax/mxisd/as/AppSvcManager.java @@ -27,8 +27,9 @@ import io.kamax.matrix.client.MatrixClientContext; import io.kamax.matrix.client.as.MatrixApplicationServiceClient; import io.kamax.matrix.event.EventKey; import io.kamax.matrix.json.GsonUtil; -import io.kamax.mxisd.as.processor.MembershipEventProcessor; -import io.kamax.mxisd.as.processor.MessageEventProcessor; +import io.kamax.mxisd.as.processor.event.MembershipEventProcessor; +import io.kamax.mxisd.as.processor.event.MessageEventProcessor; +import io.kamax.mxisd.as.registration.SynapseRegistrationYaml; import io.kamax.mxisd.backend.sql.synapse.Synapse; import io.kamax.mxisd.config.AppServiceConfig; import io.kamax.mxisd.config.MxisdConfig; diff --git a/src/main/java/io/kamax/mxisd/as/processor/MembershipEventProcessor.java b/src/main/java/io/kamax/mxisd/as/processor/event/MembershipEventProcessor.java similarity index 98% rename from src/main/java/io/kamax/mxisd/as/processor/MembershipEventProcessor.java rename to src/main/java/io/kamax/mxisd/as/processor/event/MembershipEventProcessor.java index 009611a..f14bc28 100644 --- a/src/main/java/io/kamax/mxisd/as/processor/MembershipEventProcessor.java +++ b/src/main/java/io/kamax/mxisd/as/processor/event/MembershipEventProcessor.java @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.as.processor; +package io.kamax.mxisd.as.processor.event; import com.google.gson.JsonObject; import io.kamax.matrix.MatrixID; @@ -29,10 +29,10 @@ import io.kamax.matrix.client.as.MatrixApplicationServiceClient; import io.kamax.matrix.event.EventKey; import io.kamax.matrix.hs._MatrixRoom; import io.kamax.mxisd.as.EventTypeProcessor; -import io.kamax.mxisd.as.IMatrixIdInvite; -import io.kamax.mxisd.as.MatrixIdInvite; import io.kamax.mxisd.backend.sql.synapse.Synapse; import io.kamax.mxisd.config.MxisdConfig; +import io.kamax.mxisd.invitation.IMatrixIdInvite; +import io.kamax.mxisd.invitation.MatrixIdInvite; import io.kamax.mxisd.notification.NotificationManager; import io.kamax.mxisd.profile.ProfileManager; import org.apache.commons.lang.StringUtils; diff --git a/src/main/java/io/kamax/mxisd/as/processor/MessageEventProcessor.java b/src/main/java/io/kamax/mxisd/as/processor/event/MessageEventProcessor.java similarity index 98% rename from src/main/java/io/kamax/mxisd/as/processor/MessageEventProcessor.java rename to src/main/java/io/kamax/mxisd/as/processor/event/MessageEventProcessor.java index 8053943..4253728 100644 --- a/src/main/java/io/kamax/mxisd/as/processor/MessageEventProcessor.java +++ b/src/main/java/io/kamax/mxisd/as/processor/event/MessageEventProcessor.java @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.as.processor; +package io.kamax.mxisd.as.processor.event; import com.google.gson.JsonObject; import io.kamax.matrix._MatrixID; diff --git a/src/main/java/io/kamax/mxisd/as/SynapseRegistrationYaml.java b/src/main/java/io/kamax/mxisd/as/registration/SynapseRegistrationYaml.java similarity index 99% rename from src/main/java/io/kamax/mxisd/as/SynapseRegistrationYaml.java rename to src/main/java/io/kamax/mxisd/as/registration/SynapseRegistrationYaml.java index 5ab3988..167333b 100644 --- a/src/main/java/io/kamax/mxisd/as/SynapseRegistrationYaml.java +++ b/src/main/java/io/kamax/mxisd/as/registration/SynapseRegistrationYaml.java @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.as; +package io.kamax.mxisd.as.registration; import io.kamax.mxisd.config.AppServiceConfig; diff --git a/src/main/java/io/kamax/mxisd/as/IMatrixIdInvite.java b/src/main/java/io/kamax/mxisd/invitation/IMatrixIdInvite.java similarity index 92% rename from src/main/java/io/kamax/mxisd/as/IMatrixIdInvite.java rename to src/main/java/io/kamax/mxisd/invitation/IMatrixIdInvite.java index 626025d..91e19aa 100644 --- a/src/main/java/io/kamax/mxisd/as/IMatrixIdInvite.java +++ b/src/main/java/io/kamax/mxisd/invitation/IMatrixIdInvite.java @@ -18,10 +18,9 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.as; +package io.kamax.mxisd.invitation; import io.kamax.matrix._MatrixID; -import io.kamax.mxisd.invitation.IThreePidInvite; public interface IMatrixIdInvite extends IThreePidInvite { diff --git a/src/main/java/io/kamax/mxisd/as/MatrixIdInvite.java b/src/main/java/io/kamax/mxisd/invitation/MatrixIdInvite.java similarity index 98% rename from src/main/java/io/kamax/mxisd/as/MatrixIdInvite.java rename to src/main/java/io/kamax/mxisd/invitation/MatrixIdInvite.java index 1c9d630..1e36ea7 100644 --- a/src/main/java/io/kamax/mxisd/as/MatrixIdInvite.java +++ b/src/main/java/io/kamax/mxisd/invitation/MatrixIdInvite.java @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.as; +package io.kamax.mxisd.invitation; import io.kamax.matrix._MatrixID; diff --git a/src/main/java/io/kamax/mxisd/notification/NotificationHandler.java b/src/main/java/io/kamax/mxisd/notification/NotificationHandler.java index 07056e5..b6c5ba2 100644 --- a/src/main/java/io/kamax/mxisd/notification/NotificationHandler.java +++ b/src/main/java/io/kamax/mxisd/notification/NotificationHandler.java @@ -21,7 +21,7 @@ package io.kamax.mxisd.notification; import io.kamax.matrix.ThreePid; -import io.kamax.mxisd.as.IMatrixIdInvite; +import io.kamax.mxisd.invitation.IMatrixIdInvite; import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.threepid.session.IThreePidSession; diff --git a/src/main/java/io/kamax/mxisd/notification/NotificationManager.java b/src/main/java/io/kamax/mxisd/notification/NotificationManager.java index 22a8e57..33eea8c 100644 --- a/src/main/java/io/kamax/mxisd/notification/NotificationManager.java +++ b/src/main/java/io/kamax/mxisd/notification/NotificationManager.java @@ -21,9 +21,9 @@ package io.kamax.mxisd.notification; import io.kamax.matrix.ThreePid; -import io.kamax.mxisd.as.IMatrixIdInvite; import io.kamax.mxisd.config.threepid.notification.NotificationConfig; import io.kamax.mxisd.exception.NotImplementedException; +import io.kamax.mxisd.invitation.IMatrixIdInvite; import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.threepid.session.IThreePidSession; import org.apache.commons.lang.StringUtils; diff --git a/src/main/java/io/kamax/mxisd/threepid/generator/GenericTemplateNotificationGenerator.java b/src/main/java/io/kamax/mxisd/threepid/generator/GenericTemplateNotificationGenerator.java index ad35ddd..fb5a363 100644 --- a/src/main/java/io/kamax/mxisd/threepid/generator/GenericTemplateNotificationGenerator.java +++ b/src/main/java/io/kamax/mxisd/threepid/generator/GenericTemplateNotificationGenerator.java @@ -21,11 +21,11 @@ package io.kamax.mxisd.threepid.generator; import io.kamax.matrix.ThreePid; -import io.kamax.mxisd.as.IMatrixIdInvite; import io.kamax.mxisd.config.MatrixConfig; import io.kamax.mxisd.config.ServerConfig; import io.kamax.mxisd.config.threepid.medium.GenericTemplateConfig; import io.kamax.mxisd.exception.InternalServerError; +import io.kamax.mxisd.invitation.IMatrixIdInvite; import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.threepid.session.IThreePidSession; import io.kamax.mxisd.util.FileUtil; diff --git a/src/main/java/io/kamax/mxisd/threepid/generator/NotificationGenerator.java b/src/main/java/io/kamax/mxisd/threepid/generator/NotificationGenerator.java index eb5f49d..aeb85f1 100644 --- a/src/main/java/io/kamax/mxisd/threepid/generator/NotificationGenerator.java +++ b/src/main/java/io/kamax/mxisd/threepid/generator/NotificationGenerator.java @@ -21,7 +21,7 @@ package io.kamax.mxisd.threepid.generator; import io.kamax.matrix.ThreePid; -import io.kamax.mxisd.as.IMatrixIdInvite; +import io.kamax.mxisd.invitation.IMatrixIdInvite; import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.threepid.session.IThreePidSession; diff --git a/src/main/java/io/kamax/mxisd/threepid/generator/PlaceholderNotificationGenerator.java b/src/main/java/io/kamax/mxisd/threepid/generator/PlaceholderNotificationGenerator.java index 34d6e05..c1fccf6 100644 --- a/src/main/java/io/kamax/mxisd/threepid/generator/PlaceholderNotificationGenerator.java +++ b/src/main/java/io/kamax/mxisd/threepid/generator/PlaceholderNotificationGenerator.java @@ -21,10 +21,10 @@ package io.kamax.mxisd.threepid.generator; import io.kamax.matrix.ThreePid; -import io.kamax.mxisd.as.IMatrixIdInvite; import io.kamax.mxisd.config.MatrixConfig; import io.kamax.mxisd.config.ServerConfig; import io.kamax.mxisd.http.IsAPIv1; +import io.kamax.mxisd.invitation.IMatrixIdInvite; import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.threepid.session.IThreePidSession; import org.apache.commons.lang.StringUtils; diff --git a/src/main/java/io/kamax/mxisd/threepid/notification/GenericNotificationHandler.java b/src/main/java/io/kamax/mxisd/threepid/notification/GenericNotificationHandler.java index b6992a0..cd77907 100644 --- a/src/main/java/io/kamax/mxisd/threepid/notification/GenericNotificationHandler.java +++ b/src/main/java/io/kamax/mxisd/threepid/notification/GenericNotificationHandler.java @@ -21,8 +21,8 @@ package io.kamax.mxisd.threepid.notification; import io.kamax.matrix.ThreePid; -import io.kamax.mxisd.as.IMatrixIdInvite; import io.kamax.mxisd.exception.ConfigurationException; +import io.kamax.mxisd.invitation.IMatrixIdInvite; import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.notification.NotificationHandler; import io.kamax.mxisd.threepid.connector.ThreePidConnector; diff --git a/src/main/java/io/kamax/mxisd/threepid/notification/email/EmailSendGridNotificationHandler.java b/src/main/java/io/kamax/mxisd/threepid/notification/email/EmailSendGridNotificationHandler.java index 93b19a3..ce8760c 100644 --- a/src/main/java/io/kamax/mxisd/threepid/notification/email/EmailSendGridNotificationHandler.java +++ b/src/main/java/io/kamax/mxisd/threepid/notification/email/EmailSendGridNotificationHandler.java @@ -24,10 +24,10 @@ import com.sendgrid.SendGrid; import com.sendgrid.SendGridException; import io.kamax.matrix.ThreePid; import io.kamax.matrix.ThreePidMedium; -import io.kamax.mxisd.as.IMatrixIdInvite; import io.kamax.mxisd.config.MxisdConfig; import io.kamax.mxisd.config.threepid.connector.EmailSendGridConfig; import io.kamax.mxisd.exception.FeatureNotAvailable; +import io.kamax.mxisd.invitation.IMatrixIdInvite; import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.notification.NotificationHandler; import io.kamax.mxisd.threepid.generator.PlaceholderNotificationGenerator; diff --git a/src/test/java/io/kamax/mxisd/test/notification/EmailNotificationTest.java b/src/test/java/io/kamax/mxisd/test/notification/EmailNotificationTest.java index 924b010..f4677d9 100644 --- a/src/test/java/io/kamax/mxisd/test/notification/EmailNotificationTest.java +++ b/src/test/java/io/kamax/mxisd/test/notification/EmailNotificationTest.java @@ -28,10 +28,10 @@ import io.kamax.matrix.ThreePidMedium; import io.kamax.matrix._MatrixID; import io.kamax.matrix.json.GsonUtil; import io.kamax.mxisd.Mxisd; -import io.kamax.mxisd.as.MatrixIdInvite; import io.kamax.mxisd.config.MxisdConfig; import io.kamax.mxisd.config.threepid.connector.EmailSmtpConfig; import io.kamax.mxisd.config.threepid.medium.EmailConfig; +import io.kamax.mxisd.invitation.MatrixIdInvite; import io.kamax.mxisd.threepid.connector.email.EmailSmtpConnector; import io.kamax.mxisd.threepid.session.ThreePidSession; import org.apache.commons.lang.RandomStringUtils; From de840b9d00fbd68477852bd9cf8723631950cb1b Mon Sep 17 00:00:00 2001 From: Max Dor Date: Sun, 3 Mar 2019 16:39:58 +0100 Subject: [PATCH 20/28] Skeleton for modular AS admin command processing --- .../java/io/kamax/mxisd/as/AppSvcManager.java | 1 + .../processor/command/CommandProcessor.java | 31 +++++++++ .../command/InviteCommandProcessor.java | 68 +++++++++++++++++++ .../command/PingCommandProcessor.java | 36 ++++++++++ .../event}/EventTypeProcessor.java | 2 +- .../event/MembershipEventProcessor.java | 1 - .../event/MessageEventProcessor.java | 11 ++- 7 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 src/main/java/io/kamax/mxisd/as/processor/command/CommandProcessor.java create mode 100644 src/main/java/io/kamax/mxisd/as/processor/command/InviteCommandProcessor.java create mode 100644 src/main/java/io/kamax/mxisd/as/processor/command/PingCommandProcessor.java rename src/main/java/io/kamax/mxisd/as/{ => processor/event}/EventTypeProcessor.java (95%) diff --git a/src/main/java/io/kamax/mxisd/as/AppSvcManager.java b/src/main/java/io/kamax/mxisd/as/AppSvcManager.java index fd55223..5676c16 100644 --- a/src/main/java/io/kamax/mxisd/as/AppSvcManager.java +++ b/src/main/java/io/kamax/mxisd/as/AppSvcManager.java @@ -27,6 +27,7 @@ import io.kamax.matrix.client.MatrixClientContext; import io.kamax.matrix.client.as.MatrixApplicationServiceClient; import io.kamax.matrix.event.EventKey; import io.kamax.matrix.json.GsonUtil; +import io.kamax.mxisd.as.processor.event.EventTypeProcessor; import io.kamax.mxisd.as.processor.event.MembershipEventProcessor; import io.kamax.mxisd.as.processor.event.MessageEventProcessor; import io.kamax.mxisd.as.registration.SynapseRegistrationYaml; diff --git a/src/main/java/io/kamax/mxisd/as/processor/command/CommandProcessor.java b/src/main/java/io/kamax/mxisd/as/processor/command/CommandProcessor.java new file mode 100644 index 0000000..2c2b7c9 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/as/processor/command/CommandProcessor.java @@ -0,0 +1,31 @@ +/* + * 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.as.processor.command; + +import io.kamax.matrix.client._MatrixClient; +import io.kamax.matrix.hs._MatrixRoom; +import io.kamax.mxisd.Mxisd; + +public interface CommandProcessor { + + void process(Mxisd m, _MatrixClient client, _MatrixRoom room, String command, String[] arguments); + +} diff --git a/src/main/java/io/kamax/mxisd/as/processor/command/InviteCommandProcessor.java b/src/main/java/io/kamax/mxisd/as/processor/command/InviteCommandProcessor.java new file mode 100644 index 0000000..a2a9fc1 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/as/processor/command/InviteCommandProcessor.java @@ -0,0 +1,68 @@ +/* + * 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.as.processor.command; + +import io.kamax.matrix.client._MatrixClient; +import io.kamax.matrix.hs._MatrixRoom; +import io.kamax.mxisd.Mxisd; +import org.apache.commons.lang.StringUtils; + +public class InviteCommandProcessor implements CommandProcessor { + + public static final String Command = "invite"; + + @Override + public void process(Mxisd m, _MatrixClient client, _MatrixRoom room, String command, String[] arguments) { + if (arguments.length < 1) { + room.sendText(buildHelp()); + } + + String subcmd = arguments[0]; + String response; + if (StringUtils.equals("list", subcmd)) { + response = buildError("This command is not supported yet", false); + } else if (StringUtils.endsWith("show", subcmd)) { + response = buildError("This command is not supported yet", false); + } else if (StringUtils.equals("revoke", subcmd)) { + response = buildError("This command is not supported yet", false); + } else { + response = buildError("Unknown command: " + subcmd, true); + } + + room.sendText(response); + } + + private String buildError(String message, boolean showHelp) { + if (showHelp) { + message = message + "\n\n" + buildHelp(); + } + + return message; + } + + private String buildHelp() { + return "Available actions:\n\n" + + "list - List invites\n" + + "show - Show detailed info about a specific invite\n" + + "revoke - Revoke a pending invite by resolving it to the configured Expiration user\n"; + } + +} diff --git a/src/main/java/io/kamax/mxisd/as/processor/command/PingCommandProcessor.java b/src/main/java/io/kamax/mxisd/as/processor/command/PingCommandProcessor.java new file mode 100644 index 0000000..b8475f7 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/as/processor/command/PingCommandProcessor.java @@ -0,0 +1,36 @@ +/* + * 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.as.processor.command; + +import io.kamax.matrix.client._MatrixClient; +import io.kamax.matrix.hs._MatrixRoom; +import io.kamax.mxisd.Mxisd; + +public class PingCommandProcessor implements CommandProcessor { + + public static final String Command = "ping"; + + @Override + public void process(Mxisd m, _MatrixClient client, _MatrixRoom room, String command, String[] arguments) { + room.sendText("Pong!"); + } + +} diff --git a/src/main/java/io/kamax/mxisd/as/EventTypeProcessor.java b/src/main/java/io/kamax/mxisd/as/processor/event/EventTypeProcessor.java similarity index 95% rename from src/main/java/io/kamax/mxisd/as/EventTypeProcessor.java rename to src/main/java/io/kamax/mxisd/as/processor/event/EventTypeProcessor.java index 0d55602..a0a506a 100644 --- a/src/main/java/io/kamax/mxisd/as/EventTypeProcessor.java +++ b/src/main/java/io/kamax/mxisd/as/processor/event/EventTypeProcessor.java @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.as; +package io.kamax.mxisd.as.processor.event; import com.google.gson.JsonObject; import io.kamax.matrix._MatrixID; diff --git a/src/main/java/io/kamax/mxisd/as/processor/event/MembershipEventProcessor.java b/src/main/java/io/kamax/mxisd/as/processor/event/MembershipEventProcessor.java index f14bc28..05d4612 100644 --- a/src/main/java/io/kamax/mxisd/as/processor/event/MembershipEventProcessor.java +++ b/src/main/java/io/kamax/mxisd/as/processor/event/MembershipEventProcessor.java @@ -28,7 +28,6 @@ import io.kamax.matrix._ThreePid; import io.kamax.matrix.client.as.MatrixApplicationServiceClient; import io.kamax.matrix.event.EventKey; import io.kamax.matrix.hs._MatrixRoom; -import io.kamax.mxisd.as.EventTypeProcessor; import io.kamax.mxisd.backend.sql.synapse.Synapse; import io.kamax.mxisd.config.MxisdConfig; import io.kamax.mxisd.invitation.IMatrixIdInvite; diff --git a/src/main/java/io/kamax/mxisd/as/processor/event/MessageEventProcessor.java b/src/main/java/io/kamax/mxisd/as/processor/event/MessageEventProcessor.java index 4253728..2f50490 100644 --- a/src/main/java/io/kamax/mxisd/as/processor/event/MessageEventProcessor.java +++ b/src/main/java/io/kamax/mxisd/as/processor/event/MessageEventProcessor.java @@ -26,12 +26,16 @@ import io.kamax.matrix._MatrixUserProfile; import io.kamax.matrix.client.as.MatrixApplicationServiceClient; import io.kamax.matrix.hs._MatrixRoom; import io.kamax.matrix.json.event.MatrixJsonRoomMessageEvent; -import io.kamax.mxisd.as.EventTypeProcessor; +import io.kamax.mxisd.as.processor.command.CommandProcessor; +import io.kamax.mxisd.as.processor.command.InviteCommandProcessor; +import io.kamax.mxisd.as.processor.command.PingCommandProcessor; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; public class MessageEventProcessor implements EventTypeProcessor { @@ -39,9 +43,14 @@ public class MessageEventProcessor implements EventTypeProcessor { private static final Logger log = LoggerFactory.getLogger(MessageEventProcessor.class); private final MatrixApplicationServiceClient client; + private Map processors; public MessageEventProcessor(MatrixApplicationServiceClient client) { this.client = client; + + processors = new HashMap<>(); + processors.put(PingCommandProcessor.Command, new PingCommandProcessor()); + processors.put(InviteCommandProcessor.Command, new InviteCommandProcessor()); } @Override From 1dce59a02e6fd98d400d9fc61e894d4f1250ffd6 Mon Sep 17 00:00:00 2001 From: Max Dor Date: Mon, 4 Mar 2019 00:02:13 +0100 Subject: [PATCH 21/28] Add lookup and invite commands to the admin AS interface --- build.gradle | 3 + src/main/java/io/kamax/mxisd/HttpMxisd.java | 8 +- src/main/java/io/kamax/mxisd/Mxisd.java | 17 +++- .../java/io/kamax/mxisd/as/AppSvcManager.java | 18 ++-- .../processor/command/CommandProcessor.java | 3 +- .../command/InviteCommandProcessor.java | 85 +++++++++++++++---- .../command/LookupCommandProcessor.java | 73 ++++++++++++++++ .../command/PingCommandProcessor.java | 5 +- .../event/MembershipEventProcessor.java | 19 ++--- .../event/MessageEventProcessor.java | 61 ++++++++++--- .../sql/generic/GenericSqlStoreSupplier.java | 2 +- .../mxisd/invitation/InvitationManager.java | 17 ++++ .../kamax/mxisd/profile/ProfileManager.java | 9 +- 13 files changed, 253 insertions(+), 67 deletions(-) create mode 100644 src/main/java/io/kamax/mxisd/as/processor/command/LookupCommandProcessor.java diff --git a/build.gradle b/build.gradle index 9d685b5..4ac3e55 100644 --- a/build.gradle +++ b/build.gradle @@ -145,6 +145,9 @@ dependencies { // HTTP server compile 'io.undertow:undertow-core:2.0.16.Final' + + // Command parser for AS interface + implementation 'commons-cli:commons-cli:1.4' testCompile 'junit:junit:4.12' testCompile 'com.github.tomakehurst:wiremock:2.8.0' diff --git a/src/main/java/io/kamax/mxisd/HttpMxisd.java b/src/main/java/io/kamax/mxisd/HttpMxisd.java index 0df6252..0ee9d9b 100644 --- a/src/main/java/io/kamax/mxisd/HttpMxisd.java +++ b/src/main/java/io/kamax/mxisd/HttpMxisd.java @@ -72,7 +72,7 @@ public class HttpMxisd { HttpHandler asTxnHandler = SaneHandler.around(new AsTransactionHandler(m.getAs())); HttpHandler asNotFoundHandler = SaneHandler.around(new AsNotFoundHandler(m.getAs())); - HttpHandler storeInvHandler = SaneHandler.around(new StoreInviteHandler(m.getConfig().getServer(), m.getInvitationManager(), m.getKeyManager())); + HttpHandler storeInvHandler = SaneHandler.around(new StoreInviteHandler(m.getConfig().getServer(), m.getInvite(), m.getKeyManager())); HttpHandler sessValidateHandler = SaneHandler.around(new SessionValidateHandler(m.getSession(), m.getConfig().getServer(), m.getConfig().getView())); httpSrv = Undertow.builder().addHttpListener(m.getConfig().getServer().getPort(), "0.0.0.0").setHandler(Handlers.routing() @@ -106,9 +106,9 @@ public class HttpMxisd { .get(SessionValidateHandler.Path, sessValidateHandler) .post(SessionValidateHandler.Path, sessValidateHandler) .get(SessionTpidGetValidatedHandler.Path, SaneHandler.around(new SessionTpidGetValidatedHandler(m.getSession()))) - .post(SessionTpidBindHandler.Path, SaneHandler.around(new SessionTpidBindHandler(m.getSession(), m.getInvitationManager()))) + .post(SessionTpidBindHandler.Path, SaneHandler.around(new SessionTpidBindHandler(m.getSession(), m.getInvite()))) .post(SessionTpidUnbindHandler.Path, SaneHandler.around(new SessionTpidUnbindHandler(m.getSession()))) - .post(SignEd25519Handler.Path, SaneHandler.around(new SignEd25519Handler(m.getConfig(), m.getInvitationManager(), m.getSign()))) + .post(SignEd25519Handler.Path, SaneHandler.around(new SignEd25519Handler(m.getConfig(), m.getInvite(), m.getSign()))) // Profile endpoints .get(ProfileHandler.Path, SaneHandler.around(new ProfileHandler(m.getProfile()))) @@ -118,7 +118,7 @@ public class HttpMxisd { .post(Register3pidRequestTokenHandler.Path, SaneHandler.around(new Register3pidRequestTokenHandler(m.getReg(), m.getClientDns(), m.getHttpClient()))) // Invite endpoints - .post(RoomInviteHandler.Path, SaneHandler.around(new RoomInviteHandler(m.getHttpClient(), m.getClientDns(), m.getInvitationManager()))) + .post(RoomInviteHandler.Path, SaneHandler.around(new RoomInviteHandler(m.getHttpClient(), m.getClientDns(), m.getInvite()))) // Application Service endpoints .get(AsUserHandler.Path, asUserHandler) diff --git a/src/main/java/io/kamax/mxisd/Mxisd.java b/src/main/java/io/kamax/mxisd/Mxisd.java index 4908eae..e6470ea 100644 --- a/src/main/java/io/kamax/mxisd/Mxisd.java +++ b/src/main/java/io/kamax/mxisd/Mxisd.java @@ -85,6 +85,9 @@ public class Mxisd { private NotificationManager notifMgr; private RegistrationManager regMgr; + // HS-specific classes + private Synapse synapse; + public Mxisd(MxisdConfig cfg) { this.cfg = cfg.build(); } @@ -104,7 +107,7 @@ public class Mxisd { signMgr = CryptoFactory.getSignatureManager(keyMgr); clientDns = new ClientDnsOverwrite(cfg.getDns().getOverwrite()); FederationDnsOverwrite fedDns = new FederationDnsOverwrite(cfg.getDns().getOverwrite()); - Synapse synapse = new Synapse(cfg.getSynapseSql()); + synapse = new Synapse(cfg.getSynapseSql()); BridgeFetcher bridgeFetcher = new BridgeFetcher(cfg.getLookup().getRecursive().getBridge(), srvFetcher); ServiceLoader.load(IdentityStoreSupplier.class).iterator().forEachRemaining(p -> p.accept(this)); @@ -118,7 +121,7 @@ public class Mxisd { authMgr = new AuthManager(cfg, AuthProviders.get(), idStrategy, invMgr, clientDns, httpClient); dirMgr = new DirectoryManager(cfg.getDirectory(), clientDns, httpClient, DirectoryProviders.get()); regMgr = new RegistrationManager(cfg.getRegister(), httpClient, clientDns, invMgr); - asHander = new AppSvcManager(cfg, store, pMgr, notifMgr, synapse); + asHander = new AppSvcManager(this); } public MxisdConfig getConfig() { @@ -141,7 +144,7 @@ public class Mxisd { return keyMgr; } - public InvitationManager getInvitationManager() { + public InvitationManager getInvite() { return invMgr; } @@ -181,6 +184,14 @@ public class Mxisd { return notifMgr; } + public IStorage getStore() { + return store; + } + + public Synapse getSynapse() { + return synapse; + } + public void start() { build(); } diff --git a/src/main/java/io/kamax/mxisd/as/AppSvcManager.java b/src/main/java/io/kamax/mxisd/as/AppSvcManager.java index 5676c16..e389522 100644 --- a/src/main/java/io/kamax/mxisd/as/AppSvcManager.java +++ b/src/main/java/io/kamax/mxisd/as/AppSvcManager.java @@ -27,18 +27,16 @@ import io.kamax.matrix.client.MatrixClientContext; import io.kamax.matrix.client.as.MatrixApplicationServiceClient; import io.kamax.matrix.event.EventKey; import io.kamax.matrix.json.GsonUtil; +import io.kamax.mxisd.Mxisd; import io.kamax.mxisd.as.processor.event.EventTypeProcessor; import io.kamax.mxisd.as.processor.event.MembershipEventProcessor; import io.kamax.mxisd.as.processor.event.MessageEventProcessor; import io.kamax.mxisd.as.registration.SynapseRegistrationYaml; -import io.kamax.mxisd.backend.sql.synapse.Synapse; import io.kamax.mxisd.config.AppServiceConfig; import io.kamax.mxisd.config.MxisdConfig; import io.kamax.mxisd.exception.ConfigurationException; import io.kamax.mxisd.exception.HttpMatrixException; import io.kamax.mxisd.exception.NotAllowedException; -import io.kamax.mxisd.notification.NotificationManager; -import io.kamax.mxisd.profile.ProfileManager; import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.ormlite.dao.ASTransactionDao; import io.kamax.mxisd.util.GsonParser; @@ -72,9 +70,9 @@ public class AppSvcManager { private Map processors = new HashMap<>(); private Map> transactionsInProgress = new ConcurrentHashMap<>(); - public AppSvcManager(MxisdConfig mxisdCfg, IStorage store, ProfileManager profiler, NotificationManager notif, Synapse synapse) { - this.cfg = mxisdCfg.getAppsvc(); - this.store = store; + public AppSvcManager(Mxisd m) { + this.cfg = m.getConfig().getAppsvc(); + this.store = m.getStore(); /* We process the configuration to make sure all is fine and setting default values if needed @@ -129,15 +127,15 @@ public class AppSvcManager { } MatrixClientContext mxContext = new MatrixClientContext(); - mxContext.setDomain(mxisdCfg.getMatrix().getDomain()); + mxContext.setDomain(m.getConfig().getMatrix().getDomain()); mxContext.setToken(cfg.getEndpoint().getToHS().getToken()); mxContext.setHsBaseUrl(cfg.getEndpoint().getToHS().getUrl()); client = new MatrixApplicationServiceClient(mxContext); - processors.put("m.room.member", new MembershipEventProcessor(client, mxisdCfg, profiler, notif, synapse)); - processors.put("m.room.message", new MessageEventProcessor(client)); + processors.put("m.room.member", new MembershipEventProcessor(client, m)); + processors.put("m.room.message", new MessageEventProcessor(m, client)); - processSynapseConfig(mxisdCfg); + processSynapseConfig(m.getConfig()); } private void processSynapseConfig(MxisdConfig cfg) { diff --git a/src/main/java/io/kamax/mxisd/as/processor/command/CommandProcessor.java b/src/main/java/io/kamax/mxisd/as/processor/command/CommandProcessor.java index 2c2b7c9..bb9f9f5 100644 --- a/src/main/java/io/kamax/mxisd/as/processor/command/CommandProcessor.java +++ b/src/main/java/io/kamax/mxisd/as/processor/command/CommandProcessor.java @@ -23,9 +23,10 @@ package io.kamax.mxisd.as.processor.command; import io.kamax.matrix.client._MatrixClient; import io.kamax.matrix.hs._MatrixRoom; import io.kamax.mxisd.Mxisd; +import org.apache.commons.cli.CommandLine; public interface CommandProcessor { - void process(Mxisd m, _MatrixClient client, _MatrixRoom room, String command, String[] arguments); + void process(Mxisd m, _MatrixClient client, _MatrixRoom room, CommandLine cmdLine); } diff --git a/src/main/java/io/kamax/mxisd/as/processor/command/InviteCommandProcessor.java b/src/main/java/io/kamax/mxisd/as/processor/command/InviteCommandProcessor.java index a2a9fc1..adc32f0 100644 --- a/src/main/java/io/kamax/mxisd/as/processor/command/InviteCommandProcessor.java +++ b/src/main/java/io/kamax/mxisd/as/processor/command/InviteCommandProcessor.java @@ -23,31 +23,80 @@ package io.kamax.mxisd.as.processor.command; import io.kamax.matrix.client._MatrixClient; import io.kamax.matrix.hs._MatrixRoom; import io.kamax.mxisd.Mxisd; +import io.kamax.mxisd.invitation.IThreePidInviteReply; +import org.apache.commons.cli.CommandLine; import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.text.StrBuilder; + +import java.util.List; public class InviteCommandProcessor implements CommandProcessor { public static final String Command = "invite"; @Override - public void process(Mxisd m, _MatrixClient client, _MatrixRoom room, String command, String[] arguments) { - if (arguments.length < 1) { - room.sendText(buildHelp()); - } - - String subcmd = arguments[0]; - String response; - if (StringUtils.equals("list", subcmd)) { - response = buildError("This command is not supported yet", false); - } else if (StringUtils.endsWith("show", subcmd)) { - response = buildError("This command is not supported yet", false); - } else if (StringUtils.equals("revoke", subcmd)) { - response = buildError("This command is not supported yet", false); + public void process(Mxisd m, _MatrixClient client, _MatrixRoom room, CommandLine cmdLine) { + if (cmdLine.getArgs().length < 2) { + room.sendNotice(buildHelp()); } else { - response = buildError("Unknown command: " + subcmd, true); - } + String arg = cmdLine.getArgList().get(1); + String response; + if (StringUtils.equals("list", arg)) { - room.sendText(response); + StrBuilder b = new StrBuilder(); + + List invites = m.getInvite().listInvites(); + if (invites.isEmpty()) { + b.appendln("No invites!"); + response = b.toString(); + } else { + b.appendln("Invites:"); + + + for (IThreePidInviteReply invite : invites) { + b.appendNewLine().append("ID: ").append(invite.getId()); + b.appendNewLine().append("Room: ").append(invite.getInvite().getRoomId()); + b.appendNewLine().append("Medium: ").append(invite.getInvite().getMedium()); + b.appendNewLine().append("Address: ").append(invite.getInvite().getAddress()); + b.appendNewLine(); + } + + response = b.appendNewLine().append("Total: " + invites.size()).toString(); + } + } else if (StringUtils.equals("show", arg)) { + if (cmdLine.getArgList().size() < 3) { + response = buildHelp(); + } else { + String id = cmdLine.getArgList().get(2); + IThreePidInviteReply invite = m.getInvite().getInvite(id); + StrBuilder b = new StrBuilder(); + b.appendln("Details for Invitation #" + id); + b.appendNewLine().append("Room: ").append(invite.getInvite().getRoomId()); + b.appendNewLine().append("Sender: ").append(invite.getInvite().getSender().toString()); + b.appendNewLine().append("Medium: ").append(invite.getInvite().getMedium()); + b.appendNewLine().append("Address: ").append(invite.getInvite().getAddress()); + b.appendNewLine().append("Display name: ").append(invite.getDisplayName()); + b.appendNewLine().appendNewLine().append("Properties:"); + invite.getInvite().getProperties().forEach((k, v) -> { + b.appendNewLine().append("\t").append(k).append("=").append(v); + }); + b.appendNewLine(); + + response = b.toString(); + } + } else if (StringUtils.equals("revoke", arg)) { + if (cmdLine.getArgList().size() < 3) { + response = buildHelp(); + } else { + m.getInvite().expireInvite(cmdLine.getArgList().get(2)); + response = "OK"; + } + } else { + response = buildError("Unknown invite action: " + arg, true); + } + + room.sendNotice(response); + } } private String buildError(String message, boolean showHelp) { @@ -61,8 +110,8 @@ public class InviteCommandProcessor implements CommandProcessor { private String buildHelp() { return "Available actions:\n\n" + "list - List invites\n" + - "show - Show detailed info about a specific invite\n" + - "revoke - Revoke a pending invite by resolving it to the configured Expiration user\n"; + "show ID - Show detailed info about a specific invite\n" + + "revoke ID - Revoke a pending invite by resolving it to the configured Expiration user\n"; } } diff --git a/src/main/java/io/kamax/mxisd/as/processor/command/LookupCommandProcessor.java b/src/main/java/io/kamax/mxisd/as/processor/command/LookupCommandProcessor.java new file mode 100644 index 0000000..ba5a5b0 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/as/processor/command/LookupCommandProcessor.java @@ -0,0 +1,73 @@ +/* + * 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.as.processor.command; + +import io.kamax.matrix.client._MatrixClient; +import io.kamax.matrix.hs._MatrixRoom; +import io.kamax.mxisd.Mxisd; +import io.kamax.mxisd.lookup.SingleLookupReply; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.lang.text.StrBuilder; +import org.apache.commons.lang3.StringUtils; + +import java.util.Optional; + +public class LookupCommandProcessor implements CommandProcessor { + + public static final String Command = "lookup"; + + @Override + public void process(Mxisd m, _MatrixClient client, _MatrixRoom room, CommandLine cmdLine) { + if (cmdLine.getArgList().size() != 3) { + room.sendNotice(getUsage()); + return; + } + + String medium = cmdLine.getArgList().get(1); + String address = cmdLine.getArgList().get(2); + if (StringUtils.isAnyBlank(medium, address)) { + room.sendNotice(getUsage()); + return; + } + + room.sendNotice("Processing..."); + Optional r = m.getIdentity().find(medium, address, true); + if (!r.isPresent()) { + room.sendNotice("No result"); + return; + } + + SingleLookupReply lookup = r.get(); + StrBuilder b = new StrBuilder(); + b.append("Result for 3PID lookup on ").append(medium).append(" ").appendln(address).appendNewLine(); + b.append("Matrix ID: ").appendln(lookup.getMxid().getId()); + b.appendln("Validity:") + .append("\tNot Before: ").appendln(lookup.getNotBefore()) + .append("\tNot After: ").appendln(lookup.getNotAfter()); + + room.sendNotice(b.toString()); + } + + public String getUsage() { + return "lookup MEDIUM ADDRESS"; + } + +} diff --git a/src/main/java/io/kamax/mxisd/as/processor/command/PingCommandProcessor.java b/src/main/java/io/kamax/mxisd/as/processor/command/PingCommandProcessor.java index b8475f7..32f8364 100644 --- a/src/main/java/io/kamax/mxisd/as/processor/command/PingCommandProcessor.java +++ b/src/main/java/io/kamax/mxisd/as/processor/command/PingCommandProcessor.java @@ -23,14 +23,15 @@ package io.kamax.mxisd.as.processor.command; import io.kamax.matrix.client._MatrixClient; import io.kamax.matrix.hs._MatrixRoom; import io.kamax.mxisd.Mxisd; +import org.apache.commons.cli.CommandLine; public class PingCommandProcessor implements CommandProcessor { public static final String Command = "ping"; @Override - public void process(Mxisd m, _MatrixClient client, _MatrixRoom room, String command, String[] arguments) { - room.sendText("Pong!"); + public void process(Mxisd m, _MatrixClient client, _MatrixRoom room, CommandLine cmdLine) { + room.sendNotice("Pong!"); } } diff --git a/src/main/java/io/kamax/mxisd/as/processor/event/MembershipEventProcessor.java b/src/main/java/io/kamax/mxisd/as/processor/event/MembershipEventProcessor.java index 05d4612..2f83fc9 100644 --- a/src/main/java/io/kamax/mxisd/as/processor/event/MembershipEventProcessor.java +++ b/src/main/java/io/kamax/mxisd/as/processor/event/MembershipEventProcessor.java @@ -28,6 +28,7 @@ import io.kamax.matrix._ThreePid; import io.kamax.matrix.client.as.MatrixApplicationServiceClient; import io.kamax.matrix.event.EventKey; import io.kamax.matrix.hs._MatrixRoom; +import io.kamax.mxisd.Mxisd; import io.kamax.mxisd.backend.sql.synapse.Synapse; import io.kamax.mxisd.config.MxisdConfig; import io.kamax.mxisd.invitation.IMatrixIdInvite; @@ -38,7 +39,6 @@ import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -57,16 +57,13 @@ public class MembershipEventProcessor implements EventTypeProcessor { public MembershipEventProcessor( MatrixApplicationServiceClient client, - MxisdConfig cfg, - ProfileManager profiler, - NotificationManager notif, - Synapse synapse + Mxisd m ) { this.client = client; - this.cfg = cfg; - this.profiler = profiler; - this.notif = notif; - this.synapse = synapse; + this.cfg = m.getConfig(); + this.profiler = m.getProfile(); + this.notif = m.getNotif(); + this.synapse = m.getSynapse(); } @Override @@ -121,8 +118,8 @@ public class MembershipEventProcessor implements EventTypeProcessor { } private void processForMainUser(String roomId, _MatrixID sender) { - List roles = profiler.getRoles(sender); - if (Collections.disjoint(roles, cfg.getAppsvc().getFeature().getAdmin().getAllowedRoles())) { + boolean isAllowed = profiler.hasAnyRole(sender, cfg.getAppsvc().getFeature().getAdmin().getAllowedRoles()); + if (!isAllowed) { log.info("Sender does not have any of the required roles, denying"); client.getRoom(roomId).tryLeave().ifPresent(err -> { log.warn("Could not decline invite to room {}: {} - {}", roomId, err.getErrcode(), err.getError()); diff --git a/src/main/java/io/kamax/mxisd/as/processor/event/MessageEventProcessor.java b/src/main/java/io/kamax/mxisd/as/processor/event/MessageEventProcessor.java index 2f50490..06850dc 100644 --- a/src/main/java/io/kamax/mxisd/as/processor/event/MessageEventProcessor.java +++ b/src/main/java/io/kamax/mxisd/as/processor/event/MessageEventProcessor.java @@ -26,9 +26,13 @@ import io.kamax.matrix._MatrixUserProfile; import io.kamax.matrix.client.as.MatrixApplicationServiceClient; import io.kamax.matrix.hs._MatrixRoom; import io.kamax.matrix.json.event.MatrixJsonRoomMessageEvent; +import io.kamax.mxisd.Mxisd; import io.kamax.mxisd.as.processor.command.CommandProcessor; import io.kamax.mxisd.as.processor.command.InviteCommandProcessor; +import io.kamax.mxisd.as.processor.command.LookupCommandProcessor; import io.kamax.mxisd.as.processor.command.PingCommandProcessor; +import org.apache.commons.cli.*; +import org.apache.commons.lang.text.StrBuilder; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,36 +40,48 @@ import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; public class MessageEventProcessor implements EventTypeProcessor { private static final Logger log = LoggerFactory.getLogger(MessageEventProcessor.class); + private final Mxisd m; private final MatrixApplicationServiceClient client; private Map processors; - public MessageEventProcessor(MatrixApplicationServiceClient client) { + public MessageEventProcessor(Mxisd m, MatrixApplicationServiceClient client) { + this.m = m; this.client = client; processors = new HashMap<>(); + processors.put("?", (m1, client1, room, cmdLine) -> room.sendNotice(getHelp())); + processors.put("help", (m1, client1, room, cmdLine) -> room.sendNotice(getHelp())); processors.put(PingCommandProcessor.Command, new PingCommandProcessor()); processors.put(InviteCommandProcessor.Command, new InviteCommandProcessor()); + processors.put(LookupCommandProcessor.Command, new LookupCommandProcessor()); } @Override public void process(JsonObject ev, _MatrixID sender, String roomId) { - _MatrixRoom room = client.getRoom(roomId); - List<_MatrixID> joinedUsers = room.getJoinedUsers().stream().map(_MatrixUserProfile::getId).collect(Collectors.toList()); - boolean joinedWithMainUser = joinedUsers.contains(client.getWhoAmI()); - boolean isAdminPrivate = joinedWithMainUser && joinedUsers.size() == 2; - MatrixJsonRoomMessageEvent msgEv = new MatrixJsonRoomMessageEvent(ev); if (StringUtils.equals("m.notice", msgEv.getBodyType())) { log.info("Ignoring automated message"); return; } + _MatrixRoom room = client.getRoom(roomId); + + if (!m.getProfile().hasAnyRole(sender, m.getConfig().getAppsvc().getFeature().getAdmin().getAllowedRoles())) { + room.sendNotice("You are not allowed to interact with me."); + return; + } + + List<_MatrixID> joinedUsers = room.getJoinedUsers().stream().map(_MatrixUserProfile::getId).collect(Collectors.toList()); + boolean joinedWithMainUser = joinedUsers.contains(client.getWhoAmI()); + boolean isAdminPrivate = joinedWithMainUser && joinedUsers.size() == 2; + if (!StringUtils.equals("m.text", msgEv.getBodyType())) { log.info("Unsupported message event type: {}", msgEv.getBodyType()); return; @@ -73,20 +89,39 @@ public class MessageEventProcessor implements EventTypeProcessor { String command = msgEv.getBody(); if (!isAdminPrivate) { - if (StringUtils.equals(command, "!mxisd")) { - // TODO show help - } - if (!StringUtils.startsWith(command, "!mxisd ")) { + if (!StringUtils.startsWith(command, "!" + Mxisd.Name + " ")) { // Not for us return; } - command = command.substring("!mxisd ".length()); + command = command.substring(("!" + Mxisd.Name + " ").length()); } - if (StringUtils.equals("ping", command)) { - room.sendText("Pong!"); + try { + CommandLineParser p = new DefaultParser(); + CommandLine cmdLine = p.parse(new Options(), command.split(" ", 0)); + String cmd = cmdLine.getArgList().get(0); + + CommandProcessor cp = processors.get(cmd); + if (Objects.isNull(cp)) { + room.sendNotice("Unknown command: " + command + "\n\n" + getHelp()); + } else { + cp.process(m, client, room, cmdLine); + } + } catch (ParseException e) { + room.sendNotice("Invalid input" + "\n\n" + getHelp()); + } catch (RuntimeException e) { + room.sendNotice("Error when running command: " + e.getMessage()); } } + public String getHelp() { + StrBuilder builder = new StrBuilder(); + builder.appendln("Available commands:"); + for (String cmd : processors.keySet()) { + builder.append("\t").appendln(cmd); + } + return builder.toString(); + } + } diff --git a/src/main/java/io/kamax/mxisd/backend/sql/generic/GenericSqlStoreSupplier.java b/src/main/java/io/kamax/mxisd/backend/sql/generic/GenericSqlStoreSupplier.java index 4983369..21eb465 100644 --- a/src/main/java/io/kamax/mxisd/backend/sql/generic/GenericSqlStoreSupplier.java +++ b/src/main/java/io/kamax/mxisd/backend/sql/generic/GenericSqlStoreSupplier.java @@ -32,7 +32,7 @@ public class GenericSqlStoreSupplier implements IdentityStoreSupplier { @Override public void accept(Mxisd mxisd) { if (mxisd.getConfig().getSql().getAuth().isEnabled()) { - AuthProviders.register(() -> new GenericSqlAuthProvider(mxisd.getConfig().getSql(), mxisd.getInvitationManager())); + AuthProviders.register(() -> new GenericSqlAuthProvider(mxisd.getConfig().getSql(), mxisd.getInvite())); } if (mxisd.getConfig().getSql().getDirectory().isEnabled()) { diff --git a/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java b/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java index 8dfcc2a..11e665b 100644 --- a/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java +++ b/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java @@ -271,6 +271,19 @@ public class InvitationManager { return lookupMgr.find(medium, address, cfg.getResolution().isRecursive()); } + public List listInvites() { + return new ArrayList<>(invitations.values()); + } + + public IThreePidInviteReply getInvite(String id) { + IThreePidInviteReply v = invitations.get(id); + if (Objects.isNull(v)) { + throw new ObjectNotFoundException("Invite", id); + } + + return v; + } + public boolean canInvite(_MatrixID sender, JsonObject request) { if (!request.has("medium")) { log.info("Not a 3PID invite, allowing"); @@ -417,6 +430,10 @@ public class InvitationManager { log.debug("Invite expiration: finished"); } + public void expireInvite(String id) { + publishMapping(getInvite(id), cfg.getExpiration().getResolveTo()); + } + public void lookupMappingsForInvites() { if (!invitations.isEmpty()) { log.info("Checking for existing mapping for pending invites"); diff --git a/src/main/java/io/kamax/mxisd/profile/ProfileManager.java b/src/main/java/io/kamax/mxisd/profile/ProfileManager.java index c803626..f8c143a 100644 --- a/src/main/java/io/kamax/mxisd/profile/ProfileManager.java +++ b/src/main/java/io/kamax/mxisd/profile/ProfileManager.java @@ -37,10 +37,7 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @@ -113,4 +110,8 @@ public class ProfileManager { } } + public boolean hasAnyRole(_MatrixID user, List requiredRoles) { + return !requiredRoles.isEmpty() || Collections.disjoint(getRoles(user), requiredRoles); + } + } From 57c7e4a91d25866f0612dae51d13c73d90cc7bbd Mon Sep 17 00:00:00 2001 From: Max Dor Date: Mon, 4 Mar 2019 02:12:55 +0100 Subject: [PATCH 22/28] Show signatures into admin lookup queries --- .../as/processor/command/LookupCommandProcessor.java | 11 ++++++++--- .../mxisd/http/io/identity/SingeLookupReplyJson.java | 8 ++++++++ .../io/kamax/mxisd/lookup/SingleLookupReply.java | 12 ++++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/kamax/mxisd/as/processor/command/LookupCommandProcessor.java b/src/main/java/io/kamax/mxisd/as/processor/command/LookupCommandProcessor.java index ba5a5b0..40f148d 100644 --- a/src/main/java/io/kamax/mxisd/as/processor/command/LookupCommandProcessor.java +++ b/src/main/java/io/kamax/mxisd/as/processor/command/LookupCommandProcessor.java @@ -57,11 +57,16 @@ public class LookupCommandProcessor implements CommandProcessor { SingleLookupReply lookup = r.get(); StrBuilder b = new StrBuilder(); - b.append("Result for 3PID lookup on ").append(medium).append(" ").appendln(address).appendNewLine(); + b.append("Result for 3PID lookup of ").append(medium).append(" ").appendln(address).appendNewLine(); b.append("Matrix ID: ").appendln(lookup.getMxid().getId()); b.appendln("Validity:") - .append("\tNot Before: ").appendln(lookup.getNotBefore()) - .append("\tNot After: ").appendln(lookup.getNotAfter()); + .append(" Not Before: ").appendln(lookup.getNotBefore()) + .append(" Not After: ").appendln(lookup.getNotAfter()); + b.appendln("Signatures:"); + lookup.getSignatures().forEach((host, signs) -> { + b.append(" ").append(host).appendln(":"); + signs.forEach((key, sign) -> b.append(" ").append(key).append(" -> ").appendln("OK")); + }); room.sendNotice(b.toString()); } diff --git a/src/main/java/io/kamax/mxisd/http/io/identity/SingeLookupReplyJson.java b/src/main/java/io/kamax/mxisd/http/io/identity/SingeLookupReplyJson.java index 32c1d91..e1e662c 100644 --- a/src/main/java/io/kamax/mxisd/http/io/identity/SingeLookupReplyJson.java +++ b/src/main/java/io/kamax/mxisd/http/io/identity/SingeLookupReplyJson.java @@ -22,6 +22,9 @@ package io.kamax.mxisd.http.io.identity; import io.kamax.mxisd.lookup.SingleLookupReply; +import java.util.HashMap; +import java.util.Map; + public class SingeLookupReplyJson { private String address; @@ -30,6 +33,7 @@ public class SingeLookupReplyJson { private long not_after; private long not_before; private long ts; + private Map> signatures = new HashMap<>(); public SingeLookupReplyJson(SingleLookupReply reply) { this.address = reply.getRequest().getThreePid(); @@ -64,4 +68,8 @@ public class SingeLookupReplyJson { return ts; } + public Map> getSignatures() { + return signatures; + } + } diff --git a/src/main/java/io/kamax/mxisd/lookup/SingleLookupReply.java b/src/main/java/io/kamax/mxisd/lookup/SingleLookupReply.java index 2d7bcb3..de291d9 100644 --- a/src/main/java/io/kamax/mxisd/lookup/SingleLookupReply.java +++ b/src/main/java/io/kamax/mxisd/lookup/SingleLookupReply.java @@ -27,6 +27,8 @@ import io.kamax.matrix._MatrixID; import io.kamax.mxisd.http.io.identity.SingeLookupReplyJson; import java.time.Instant; +import java.util.HashMap; +import java.util.Map; public class SingleLookupReply { @@ -39,6 +41,7 @@ public class SingleLookupReply { private Instant notBefore; private Instant notAfter; private Instant timestamp; + private Map> signatures = new HashMap<>(); public static SingleLookupReply fromRecursive(SingleLookupRequest request, String body) { SingleLookupReply reply = new SingleLookupReply(); @@ -52,6 +55,7 @@ public class SingleLookupReply { reply.notAfter = Instant.ofEpochMilli(json.getNot_after()); reply.notBefore = Instant.ofEpochMilli(json.getNot_before()); reply.timestamp = Instant.ofEpochMilli(json.getTs()); + reply.signatures = new HashMap<>(json.getSignatures()); } catch (JsonSyntaxException e) { // stub - we only want to try, nothing more } @@ -107,4 +111,12 @@ public class SingleLookupReply { return timestamp; } + public Map> getSignatures() { + return signatures; + } + + public Map getSignature(String host) { + return signatures.computeIfAbsent(host, k -> new HashMap<>()); + } + } From 1cbb0a135b466ad8ddcd1c4fb772f03dccedde1c Mon Sep 17 00:00:00 2001 From: Max Dor Date: Tue, 2 Apr 2019 11:56:48 +0200 Subject: [PATCH 23/28] Add doc about new registration control feature --- README.md | 3 +- docs/features/registration.md | 108 ++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 docs/features/registration.md diff --git a/README.md b/README.md index 5aba319..6912868 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ users. 3PIDs can be anything that uniquely and globally identify a user, like: - Twitter handle - Facebook ID -If you are unfamiliar with the Identity vocabulary and concepts in Matrix, **please read this [introduction](docs/concepts.md)**. +If you are unfamiliar with the Identity vocabulary and concepts in Matrix, **please read this [introduction](docs/concepts.md)**. # Features [Identity](docs/features/identity.md): As a [regular Matrix Identity service](https://matrix.org/docs/spec/identity_service/r0.1.0.html#general-principles): @@ -53,6 +53,7 @@ As an enhanced Identity service: - Central Matrix Identity servers - [Session Control](docs/threepids/session/session.md): Extensive control of where 3PIDs are transmitted so they are not leaked publicly by users +- [Registration control](docs/features/registration.md): Control and restrict user registration based on 3PID patterns or criterias, like a pending invite - [Authentication](docs/features/authentication.md): Use your Identity stores to perform authentication in [synapse](https://github.com/matrix-org/synapse) via the [REST password provider](https://github.com/kamax-io/matrix-synapse-rest-auth) - [Directory search](docs/features/directory.md) which allows you to search for users within your organisation, diff --git a/docs/features/registration.md b/docs/features/registration.md new file mode 100644 index 0000000..73e7e86 --- /dev/null +++ b/docs/features/registration.md @@ -0,0 +1,108 @@ +# Registration +- [Overview](#overview) +- [Integration](#integration) + - [Reverse Proxy](#reverse-proxy) + - [nginx](#nginx) + - [Apache](#apache) + - [Homeserver](#homeserver) + - [synapse](#synapse) + +## Overview +**NOTE**: This feature is beta: it is considered stable enough for production but is incomplete and may contain bugs. + +Registration is an enhanced feature of mxisd to control registrations involving 3PIDs on a Homeserver based on policies: +- Match pending 3PID invites on the server +- Match 3PID pattern, like a specific set of domains for emails +- In futher releases, use 3PIDs found in Identity stores + +It aims to help open or invite-only registration servers control what is possible to do and ensure only approved people +can register on a given server in a implementation-agnostic manner. + +**IMPORTANT:** This feature does not control registration in general. It only acts on endpoints related to 3PIDs during +the registration process. +As such, it relies on the homeserver to require 3PIDs with the registration flows. + +This feature is not part of the Matrix spec. + +## Integration +mxisd needs to be integrated at several levels for this feature to work: +- Reverse proxy: intercept the 3PID register endpoints and act on them +- Homeserver: require 3PID to be part of the registration data + +Later version(s) of this feature may directly control registration itself to create a coherent experience +### Reverse Proxy +#### nginx +```nginx +location ^/_matrix/client/r0/register/[^/]/?$ { + proxy_pass http://127.0.0.1:8090; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; +} +``` + +#### apache +> TBC + +### Homeserver +#### Synapse +```yaml +enable_registration: true +registrations_require_3pid: + - email +``` + +## Configuration +See the [Configuration](../configuration.md) introduction doc on how to read the configuration keys. +An example of working configuration is avaiable at the end of this section. +### Enable/Disable +`register.allowed`, taking a boolean, can be used to enable/disable registration if the attempt is not 3PID-based. +`false` is the default value to prevent open registration, as you must allow it on the homeserver side. + +### For invites +`register.invite`, taking a boolean, controls if registration can be made using a 3PID which matches a pending 3PID invite. +`true` is the default value. + +### 3PID-specific +At this time, only `email` is supported with 3PID specific configuration with this feature. + +#### Email +**Base key**: `register.threepid.email` + +##### Domain whitelist/blacklist +If you would like to control which domains are allowed to be used when registrating with an email, the following sub-keys +are available: +- `domain.whitelist` +- `domain.blacklist` + +The value format is an hybrid between glob patterns and postfix configuration files with the following syntax: +- `*` will match the domain and any sub-domain(s) +- `.` will only match sub-domain(s) +- `` will only match the exact domain + +The following table illustrates pattern and maching status against example values: + +| Config value | Matches `example.org` | Matches `sub.example.org` | +|--------------- |-----------------------|---------------------------| +| `*example.org` | Yes | Yes | +| `.example.org` | No | Yes | +| `example.org` | Yes | No | + +### Full example +For the following example configuration: +```yaml +register: + policy: + threepid: + email: + domain: + whitelist: + - '*example.org' + - '.example.net' + - 'example.com' +``` +- Users can register using 3PIDs of pending invites, being allowed by default. +- Users can register using an email from `example.org` and any sub-domain, only sub-domains of `example.net` and `example.com` but not its sub-domains. +- Otherwise, user registration will be denied. + +## Usage +Nothing special is needed. Register using a regular Matrix client. From eb903bf22610dc7d3c80c3f47b9114c146cf9a6d Mon Sep 17 00:00:00 2001 From: Max Dor Date: Wed, 3 Apr 2019 00:44:30 +0200 Subject: [PATCH 24/28] Document new 3PID invite expiration feature --- docs/features/identity.md | 67 ++++++++++++++++++- .../kamax/mxisd/config/InvitationConfig.java | 2 +- .../mxisd/invitation/InvitationManager.java | 6 -- 3 files changed, 65 insertions(+), 10 deletions(-) diff --git a/docs/features/identity.md b/docs/features/identity.md index 6e8d830..08c1aaa 100644 --- a/docs/features/identity.md +++ b/docs/features/identity.md @@ -1,6 +1,13 @@ # Identity Implementation of the [Identity Service API r0.1.0](https://matrix.org/docs/spec/identity_service/r0.1.0.html). +- [Lookups](#lookups) +- [Invitations](#invitations) + - [Expiration](#expiration) + - [Policies](#policies) + - [Resolution](#resolution) +- [3PIDs Management](#3pids-management) + ## Lookups If you would like to use the central matrix.org Identity server to ensure maximum discovery at the cost of potentially leaking all your contacts information, add the following to your configuration: @@ -12,8 +19,62 @@ forward: **NOTE:** You should carefully consider enabling this option, which is discouraged. For more info, see the [relevant issue](https://github.com/kamax-matrix/mxisd/issues/76). -## Room Invitations -Resolution can be customized using the following configuration: +## Invitations +### Expiration +#### Overview +Matrix does not provide a mean to remove/cancel pending 3PID invitations with the APIs. The current reference +implementations also do not provide any mean to do so. This leads to 3PID invites forever stuck in rooms. + +To provide this functionality, mxisd uses a workaround: resolve the invite to a dedicated User ID, which can be +controlled by mxisd or a bot/service that will then reject the invite. + +If this dedicated User ID is to be controlled by mxisd, the [Application Service](experimental/application-service.md) +feature must be configured and integrated with your Homeserver. + +#### Configuration +```yaml +invite: + expiration: + enabled: true/false + after: 5 + resolveTo: '@john.doe:example.org' +``` +`enabled` +- Purpose: Enable or disable the invite expiration feature. +- Default: `true` + +`after` +- Purpose: Amount of minutes before an invitation expires. +- Default: `10080` (7 days) + +`resolveTo` +- Purpose: Matrix User ID to resolve the expired invitations to. +- Default: Computed from `appsvc.user.inviteExpired` and `matrix.domain` + +### Policies +#### Integration +##### Reverse Proxy +###### nginx +```nginx +location ~* ^/_matrix/client/r0/rooms/([^/]+)/invite$ { + proxy_pass http://127.0.0.1:8090; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; +} +``` + +##### Configuration +```yaml +invite: + policy: + ifSender: + hasRole: + - '' + - '' +``` + +### Resolution +Resolution of 3PID invitations can be customized using the following configuration: `invite.resolution.recursive` - Default value: `true` @@ -26,5 +87,5 @@ Resolution can be customized using the following configuration: - Default value: `1` - Description: How often, in minutes, mxisd should try to resolve pending invites. -## 3PID addition to user profile +## 3PIDs Management See the [3PID session documents](../threepids/session) diff --git a/src/main/java/io/kamax/mxisd/config/InvitationConfig.java b/src/main/java/io/kamax/mxisd/config/InvitationConfig.java index e819524..d0f5d8c 100644 --- a/src/main/java/io/kamax/mxisd/config/InvitationConfig.java +++ b/src/main/java/io/kamax/mxisd/config/InvitationConfig.java @@ -34,7 +34,7 @@ public class InvitationConfig { public static class Expiration { private Boolean enabled; - private long after; + private long after = 60 * 24 * 7; // One calendar week (60min/1h * 24 = 1d * 7 = 1w) private String resolveTo; public Boolean isEnabled() { diff --git a/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java b/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java index 11e665b..13bd6f7 100644 --- a/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java +++ b/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java @@ -172,12 +172,6 @@ public class InvitationManager { // Enabled by default cfg.getInvite().getExpiration().setEnabled(true); - - // We'll resolve to our computed User ID - cfg.getInvite().getExpiration().setResolveTo(mxId); - - // One calendar week (60min/1h * 24 = 1d * 7 = 1w) - cfg.getInvite().getExpiration().setAfter(60 * 24 * 7); } if (cfg.getInvite().getExpiration().isEnabled()) { From 9abdcc15ba6abedd5f9b5167f33e77a3b25f8061 Mon Sep 17 00:00:00 2001 From: Max Dor Date: Tue, 2 Apr 2019 11:59:33 +0200 Subject: [PATCH 25/28] Clarify specifics about synapse identity store --- docs/stores/synapse.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/stores/synapse.md b/docs/stores/synapse.md index 21d768b..584b276 100644 --- a/docs/stores/synapse.md +++ b/docs/stores/synapse.md @@ -1,5 +1,6 @@ # Synapse Identity Store -Synapse's Database itself can be used as an Identity store. +Synapse's Database itself can be used as an Identity store. This identity store is a regular SQL store with +built-in default queries that matches Synapse DB. ## Features | Name | Supported | @@ -9,7 +10,8 @@ Synapse's Database itself can be used as an Identity store. | [Identity](../features/identity.md) | Yes | | [Profile](../features/profile.md) | Yes | -Authentication is done by Synapse itself. +- Authentication is done by Synapse itself. +- Roles are mapped to communities. The Role name/ID uses the community ID in the form `+id:domain.tld` ## Configuration ### Basic From 6bb0c93f57f0a737b38c83d122ef2c87a8062428 Mon Sep 17 00:00:00 2001 From: Max Dor Date: Fri, 5 Apr 2019 21:56:05 +0200 Subject: [PATCH 26/28] Fix typo --- src/main/java/io/kamax/mxisd/crypto/GenericKeyIdentifier.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/kamax/mxisd/crypto/GenericKeyIdentifier.java b/src/main/java/io/kamax/mxisd/crypto/GenericKeyIdentifier.java index 9dda144..cc9c0f0 100644 --- a/src/main/java/io/kamax/mxisd/crypto/GenericKeyIdentifier.java +++ b/src/main/java/io/kamax/mxisd/crypto/GenericKeyIdentifier.java @@ -36,7 +36,7 @@ public class GenericKeyIdentifier implements KeyIdentifier { public GenericKeyIdentifier(KeyType type, String algo, String serial) { if (StringUtils.isAnyBlank(algo, serial)) { - throw new IllegalArgumentException("Aglorith and/or Serial cannot be blank"); + throw new IllegalArgumentException("Algorithm and/or Serial cannot be blank"); } this.type = Objects.requireNonNull(type); From a7b5accd751a0adcb7cc901a17b7277fe6f14d0d Mon Sep 17 00:00:00 2001 From: Max Dor Date: Tue, 9 Apr 2019 02:50:58 +0200 Subject: [PATCH 27/28] Adapt AS doc to new format and capabilities --- .../experimental/application-service.md | 145 ++++++++++++------ 1 file changed, 97 insertions(+), 48 deletions(-) diff --git a/docs/features/experimental/application-service.md b/docs/features/experimental/application-service.md index d5e3a5e..1e9683c 100644 --- a/docs/features/experimental/application-service.md +++ b/docs/features/experimental/application-service.md @@ -1,25 +1,106 @@ -# Integration as an Application Service +# Application Service **WARNING:** These features are currently highly experimental. They can be removed or modified without notice. -All the features requires a Homeserver capable of connecting Application Services. +All the features requires a Homeserver capable of connecting [Application Services](https://matrix.org/docs/spec/application_service/r0.1.0.html). -## Email notification for Room invites by Matrix ID +The following capabilities are provided in this features: +- [Admin commands](#admin-commands) +- [Email Notification about room invites by Matrix IDs](#email-notification-about-room-invites-by-matrix-ids) +- [Auto-reject of expired 3PID invites](#auto-reject-of-expired-3pid-invites) + +## Setup +> **NOTE:** Make sure you are familiar with [configuration format and rules](../../configure.md). + +Integration as an Application service is a three steps process: +1. Create the baseline mxisd configuration to allow integration. +2. Integrate with the homeserver. +3. Configure the specific capabilities, if applicable. + +### Configuration +#### Variables +Under the `appsvc` namespace: + +| Key | Type | Required | Default | Purpose | +|-----------------------|---------|----------|---------|----------------------------------------------------------------| +| `enabled` | boolean | No | `true` | Globally enable/disable the feature | +| `user.main` | string | No | `mxisd` | Localpart for the main appservice user | +| `endpoint.toHS.url` | string | Yes | *None* | Base URL to the Homeserver | +| `endpoint.toHS.token` | string | Yes | *None* | Token to use when sending requests to the Homeserver | +| `endpoint.toAS.url` | string | Yes | *None* | Base URL to mxisd from the Homeserver | +| `endpoint.toHS.token` | string | Yes | *None* | Token for the Homeserver to use when sending requests to mxisd | + +#### Example +```yaml +appsvc: + endpoint: + toHS: + url: 'http://localhost:8008' + token: 'ExampleTokenToHS-ChangeMe!' + toAS: + url: 'http://localhost:8090' + token: 'ExampleTokenToAS-ChangeMe!' +``` +### Integration +#### Synapse +Under the `appsvc.registration.synapse` namespace: + +| Key | Type | Required | Default | Purpose | +|--------|--------|----------|--------------------|--------------------------------------------------------------------------| +| `id` | string | No | `appservice-mxisd` | The unique, user-defined ID of this application service. See spec. | +| `file` | string | Yes | *None* | If defined, the synapse registration file that should be created/updated | + +##### Example +```yaml +appsvc: + registration: + synapse: + file: '/etc/matrix-synapse/mxisd-appservice-registration.yaml' +``` + +Edit your `homeserver.yaml` and add a new entry to the appservice config file, which should look something like this: +```yaml +app_service_config_files: + - '/etc/matrix-synapse/mxisd-appservice-registration.yaml' + - ... +``` + +Restart synapse when done to register mxisd. + +#### Others +See your Homeserver documentation on how to integrate. + +## Capabilities +### Admin commands +#### Setup +Min config: +```yaml +appsvc: + feature: + admin: + allowedRoles: + - '+aMatrixCommunity:example.org' + - 'SomeLdapGroup' + - 'AnyOtherArbitraryRoleFromIdentityStores' +``` + +#### Use +The following steps assume: +- `matrix.domain` set to `example.org` +- `appsvc.user.main` set to `mxisd` or not set + +1. Invite `@mxisd:example.org` to a new direct chat +2. Type `!help` + +### Email Notification about room invites by Matrix IDs This feature allows for users found in Identity stores to be instantly notified about Room Invites, regardless if their account was already provisioned on the Homeserver. -### Requirements +#### Requirements - [Identity store(s)](../../stores/README.md) supporting the Profile feature - At least one email entry in the identity store for each user that could be invited. -### Configuration +#### Configuration In your mxisd config file: ```yaml -matrix: - listener: - url: '' - localpart: 'appservice-mxisd' - token: - hs: 'HS_TOKEN_CHANGE_ME' - synapseSql: enabled: false ## Do not use this line if Synapse is used as an Identity Store type: '' @@ -33,40 +114,8 @@ If you do not configure it, some placeholders will not be available in the notif You can also change the default template of the notification using the `generic.matrixId` template option. See [the Template generator documentation](../../threepids/notification/template-generator.md) for more info. -### Homeserver integration -#### Synapse -Create a new appservice registration file. Futher config will assume it is in `/etc/matrix-synapse/appservice-mxisd.yaml` -```yaml -id: "appservice-mxisd" -url: "http://127.0.0.1:8090" -as_token: "AS_TOKEN_CHANGE_ME" -hs_token: "HS_TOKEN_CHANGE_ME" -sender_localpart: "appservice-mxisd" -namespaces: - users: - - regex: "@*" - exclusive: false - aliases: [] - rooms: [] -``` -`id`: An arbitrary unique string to identify the AS. -`url`: mxisd to reach mxisd. This ideally should be HTTP and not going through any reverse proxy. -`as_token`: Arbitrary value used by mxisd when talking to the HS. Not currently used. -`hs_token`: Arbitrary value used by synapse when talking to mxisd. Must match `token.hs` in mxisd config. -`sender_localpart`: Username for the mxisd itself on the HS. Default configuration should be kept. -`namespaces`: To be kept as is. - -Edit your `homeserver.yaml` and add a new entry to the appservice config file, which should look something like this: -```yaml -app_service_config_files: - - '/etc/matrix-synapse/appservice-mxisd.yaml' - - ... -``` - -Restart synapse when done to register mxisd. - -#### Others -See your Homeserver documentation on how to integrate. - -### Test +#### Test Invite a user which is part of your domain while an appropriate Identity store is used. + +### Auto-reject of expired 3PID invites +*TBC* From 6cc17abf2ce6e9a8111424c0425359f82dac542f Mon Sep 17 00:00:00 2001 From: Max Dor Date: Tue, 9 Apr 2019 12:06:13 +0200 Subject: [PATCH 28/28] Further document new features --- .../experimental/application-service.md | 6 +++--- docs/features/identity.md | 20 +++++++++++++++++-- docs/features/registration.md | 7 +++++-- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/docs/features/experimental/application-service.md b/docs/features/experimental/application-service.md index 1e9683c..d592361 100644 --- a/docs/features/experimental/application-service.md +++ b/docs/features/experimental/application-service.md @@ -2,7 +2,7 @@ **WARNING:** These features are currently highly experimental. They can be removed or modified without notice. All the features requires a Homeserver capable of connecting [Application Services](https://matrix.org/docs/spec/application_service/r0.1.0.html). -The following capabilities are provided in this features: +The following capabilities are provided in this feature: - [Admin commands](#admin-commands) - [Email Notification about room invites by Matrix IDs](#email-notification-about-room-invites-by-matrix-ids) - [Auto-reject of expired 3PID invites](#auto-reject-of-expired-3pid-invites) @@ -26,7 +26,7 @@ Under the `appsvc` namespace: | `endpoint.toHS.url` | string | Yes | *None* | Base URL to the Homeserver | | `endpoint.toHS.token` | string | Yes | *None* | Token to use when sending requests to the Homeserver | | `endpoint.toAS.url` | string | Yes | *None* | Base URL to mxisd from the Homeserver | -| `endpoint.toHS.token` | string | Yes | *None* | Token for the Homeserver to use when sending requests to mxisd | +| `endpoint.toAS.token` | string | Yes | *None* | Token for the Homeserver to use when sending requests to mxisd | #### Example ```yaml @@ -88,7 +88,7 @@ The following steps assume: - `appsvc.user.main` set to `mxisd` or not set 1. Invite `@mxisd:example.org` to a new direct chat -2. Type `!help` +2. Type `!help` to get all available commands ### Email Notification about room invites by Matrix IDs This feature allows for users found in Identity stores to be instantly notified about Room Invites, regardless if their diff --git a/docs/features/identity.md b/docs/features/identity.md index 08c1aaa..6805efe 100644 --- a/docs/features/identity.md +++ b/docs/features/identity.md @@ -29,7 +29,7 @@ To provide this functionality, mxisd uses a workaround: resolve the invite to a controlled by mxisd or a bot/service that will then reject the invite. If this dedicated User ID is to be controlled by mxisd, the [Application Service](experimental/application-service.md) -feature must be configured and integrated with your Homeserver. +feature must be configured and integrated with your Homeserver, as well as the *Auto-reject 3PID invite capability*. #### Configuration ```yaml @@ -52,9 +52,23 @@ invite: - Default: Computed from `appsvc.user.inviteExpired` and `matrix.domain` ### Policies +3PID invite policies are the companion feature of [Registration](registration.md). While the Registration feature acts on +requirements for the invitee/register, this feature acts on requirement for the one(s) performing 3PID invites, ensuring +a coherent system. + +It relies on only allowing people with specific [Roles](profile.md) to perform 3PID invites. This would typically allow +a tight-control on a server setup with is "invite-only" or semi-open (relying on trusted people to invite new members). + +It's a middle ground between a closed server, where every user must be created or already exists in an Identity store, +and an open server, where anyone can register. + #### Integration +Because Identity Servers do not control 3PID invites as per Matrix spec, mxisd needs to intercept a set of Homeserver +endpoints to apply the policies. + ##### Reverse Proxy ###### nginx +**IMPORTANT**: Must be placed before your global `/_matrix` entry: ```nginx location ~* ^/_matrix/client/r0/rooms/([^/]+)/invite$ { proxy_pass http://127.0.0.1:8090; @@ -63,7 +77,9 @@ location ~* ^/_matrix/client/r0/rooms/([^/]+)/invite$ { } ``` -##### Configuration +#### Configuration +The only policy currently available is to restrict 3PID invite to users having a specific (set of) role(s), like so: + ```yaml invite: policy: diff --git a/docs/features/registration.md b/docs/features/registration.md index 73e7e86..54a3a7a 100644 --- a/docs/features/registration.md +++ b/docs/features/registration.md @@ -6,6 +6,9 @@ - [Apache](#apache) - [Homeserver](#homeserver) - [synapse](#synapse) +- [Configuration](#configuration) + - [Example](#example) +- [Usage](#usage) ## Overview **NOTE**: This feature is beta: it is considered stable enough for production but is incomplete and may contain bugs. @@ -22,7 +25,7 @@ can register on a given server in a implementation-agnostic manner. the registration process. As such, it relies on the homeserver to require 3PIDs with the registration flows. -This feature is not part of the Matrix spec. +This feature is not part of the Matrix Identity Server spec. ## Integration mxisd needs to be integrated at several levels for this feature to work: @@ -87,7 +90,7 @@ The following table illustrates pattern and maching status against example value | `.example.org` | No | Yes | | `example.org` | Yes | No | -### Full example +### Example For the following example configuration: ```yaml register: