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); + } + +}