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