diff --git a/conf/musaplink.sql b/conf/musaplink.sql index 770c675..b96bc65 100644 --- a/conf/musaplink.sql +++ b/conf/musaplink.sql @@ -10,6 +10,7 @@ CREATE TABLE transactions ( CREATE TABLE coupling_codes ( couplingcode TEXT, linkid TEXT, + created_dt TIMESTAMP PRIMARY KEY (couplingcode) ); @@ -39,5 +40,9 @@ CREATE TABLE key_details ( musapid TEXT, keyid TEXT, keyname TEXT, + publickey BYTEA, + certificate BYTEA, + created_dt TIMESTAMP, + modified_dt TIMESTAMP, PRIMARY KEY (musapid, keyid) ); \ No newline at end of file diff --git a/libs/laverca-rest-1.2.0.jar b/libs/laverca-rest-1.4.0.jar similarity index 50% rename from libs/laverca-rest-1.2.0.jar rename to libs/laverca-rest-1.4.0.jar index 60ce9a6..ecebf91 100644 Binary files a/libs/laverca-rest-1.2.0.jar and b/libs/laverca-rest-1.4.0.jar differ diff --git a/pom.xml b/pom.xml index a2a22de..5b28edd 100644 --- a/pom.xml +++ b/pom.xml @@ -29,7 +29,7 @@ laverca-rest 1.2.0 system - ${project.basedir}/libs/laverca-rest-1.2.0.jar + ${project.basedir}/libs/laverca-rest-1.4.0.jar com.eatthepath diff --git a/src/main/java/fi/methics/webapp/musaplink/MusapLinkAccount.java b/src/main/java/fi/methics/webapp/musaplink/MusapLinkAccount.java index f15ee97..1359954 100644 --- a/src/main/java/fi/methics/webapp/musaplink/MusapLinkAccount.java +++ b/src/main/java/fi/methics/webapp/musaplink/MusapLinkAccount.java @@ -10,6 +10,7 @@ import fi.methics.webapp.musaplink.coupling.json.ExternalSignatureResp; import fi.methics.webapp.musaplink.link.json.MusapSignResp; +import fi.methics.webapp.musaplink.util.MusapTransportEncryption.TransportKeys; /** * Class representing a single account in MUSAP Link @@ -28,6 +29,16 @@ public MusapLinkAccount() { } + /** + * Get transport keys + * @return transport keys - or null if not available + */ + public TransportKeys getTransportKeys() { + if (this.aesKey == null) return null; + if (this.macKey == null) return null; + return new TransportKeys(this.musapid, this.aesKey, this.macKey); + } + @Override public String toString() { return musapid; diff --git a/src/main/java/fi/methics/webapp/musaplink/coupling/CouplingCommand.java b/src/main/java/fi/methics/webapp/musaplink/coupling/CouplingCommand.java index ce4a9f7..9d65f0a 100644 --- a/src/main/java/fi/methics/webapp/musaplink/coupling/CouplingCommand.java +++ b/src/main/java/fi/methics/webapp/musaplink/coupling/CouplingCommand.java @@ -61,7 +61,7 @@ public T getRequestPayload() { * @return Transport Encryption handler */ public MusapTransportEncryption getTransportEncryption() { - return null; + return new MusapTransportEncryption(getConfig()); } /** diff --git a/src/main/java/fi/methics/webapp/musaplink/coupling/MusapCouplingServlet.java b/src/main/java/fi/methics/webapp/musaplink/coupling/MusapCouplingServlet.java index a1d0ee7..81d62fd 100644 --- a/src/main/java/fi/methics/webapp/musaplink/coupling/MusapCouplingServlet.java +++ b/src/main/java/fi/methics/webapp/musaplink/coupling/MusapCouplingServlet.java @@ -7,8 +7,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import com.google.gson.Gson; - +import fi.methics.webapp.musaplink.MusapLinkAccount; import fi.methics.webapp.musaplink.coupling.cmd.CmdEnrollData; import fi.methics.webapp.musaplink.coupling.cmd.CmdExternalSignature; import fi.methics.webapp.musaplink.coupling.cmd.CmdGenerateKeyCallback; @@ -20,6 +19,8 @@ import fi.methics.webapp.musaplink.link.json.MusapResp; import fi.methics.webapp.musaplink.util.MusapException; import fi.methics.webapp.musaplink.util.MusapLinkConf; +import fi.methics.webapp.musaplink.util.MusapTransportEncryption; +import fi.methics.webapp.musaplink.util.db.AccountStorage; /** * Servlet for communication between MUSAP and MUSAP Link. @@ -30,9 +31,8 @@ public class MusapCouplingServlet { private static final Log log = LogFactory.getLog(MusapCouplingServlet.class); - private static final Gson GSON = new Gson(); - private static MusapLinkConf conf; + private static MusapTransportEncryption enc; /** * Initialize the servlet @@ -40,6 +40,7 @@ public class MusapCouplingServlet { public static void init() { // Read conf first conf = MusapLinkConf.getInstance(); + enc = new MusapTransportEncryption(conf); if (conf == null || !conf.isInitialized()) { log.fatal("Cannot read configuration file " + conf.getConfFilePath()); System.out.println("Cannot read configuration file " + conf.getConfFilePath()); @@ -52,16 +53,52 @@ public static void init() { @Path("/musap") public Response musapEndpoint(String body) { - CouplingApiMessage jReq = GSON.fromJson(body, CouplingApiMessage.class); + CouplingApiMessage jReq = CouplingApiMessage.fromJson(body); CouplingApiMessage jResp = null; - + if (jReq == null) { log.debug("No request body"); - return MusapResp.createErrorResponse(MusapResp.ERROR_WRONG_PARAM); + return MusapResp.createErrorResponse(MusapResp.ERROR_WRONG_PARAM, "Missing request body"); + } + + MusapLinkAccount account = null; + + boolean isEncrypted = jReq.isEncrypted(); + boolean shouldDecrypt = MusapTransportEncryption.shouldDecrypt(jReq); + boolean encryptRequired = conf.isTransportEncryptionRequired(); + + if (!isEncrypted && shouldDecrypt && encryptRequired) { + // Request is encrypted even when it should be + return MusapResp.createErrorResponse(MusapResp.ERROR_WRONG_PARAM, "Missing transport encryption"); } - log.debug("Request Payload: " + jReq.getPayloadJson()); try { + if (isEncrypted) { + // Fetch transport encryption keys and decrypt if needed + account = AccountStorage.findAccountByMusapId(jReq.musapid); + + if (account == null) { + log.debug("Could not find account with MUSAP ID " + jReq.musapid); + return MusapResp.createErrorResponse(MusapResp.ERROR_WRONG_PARAM, "Failed to decrypt the request: Could not find account with MUSAP ID " + jReq.musapid); + } + if (account.getTransportKeys() == null) { + log.debug("Could not find transport encryption key"); + return MusapResp.createErrorResponse(MusapResp.ERROR_WRONG_PARAM, "Failed to decrypt the request: Missing transport key"); + } + try { + enc.decrypt(jReq, account.getTransportKeys()); + } catch (Exception e) { + log.error("Failed to decrypt message", e); + return MusapResp.createErrorResponse(MusapResp.ERROR_WRONG_PARAM, "Failed to decrypt the request: " + e.getMessage()); + } + if (!enc.isNonceValid(jReq)) { + log.error("NONCE check failed. Returning error."); + return MusapResp.createErrorResponse(MusapResp.ERROR_INTERNAL, "Replay-attack detection failed. Invalid NONCE."); + } + } + + log.debug("Request Payload: " + jReq.getPayloadJson()); + switch (jReq.type) { case CouplingApiMessage.TYPE_ENROLLDATA: { log.debug("Enrolling data"); @@ -104,14 +141,15 @@ public Response musapEndpoint(String body) { } default: { log.debug("Unknown request type " + jReq.type); - return MusapResp.createErrorResponse(MusapResp.ERROR_WRONG_PARAM); + return MusapResp.createErrorResponse(MusapResp.ERROR_WRONG_PARAM, "Unknown request type " + jReq.type); } } } catch (MusapException e) { + log.error(jReq.type + " failed", e); throw e; } catch (Exception e) { log.error(jReq.type + " failed", e); - return MusapResp.createErrorResponse(MusapResp.ERROR_INTERNAL); + return MusapResp.createErrorResponse(MusapResp.ERROR_INTERNAL, e.getMessage()); } if (jResp == null) { @@ -119,8 +157,23 @@ public Response musapEndpoint(String body) { return Response.ok().build(); } else { log.debug("Response Payload: " + jReq.getPayloadJson()); - return Response.ok(GSON.toJson(jResp)).build(); + + try { + if (MusapTransportEncryption.shouldEncrypt(jResp)) { + if (account == null) { + account = AccountStorage.findAccountByLinkId(body); + } + if (account != null) { + enc.encrypt(jResp, account.getTransportKeys()); + } + } + } catch (Exception e) { + log.error("Failed to encrypt the response", e); + return MusapResp.createErrorResponse(MusapResp.ERROR_WRONG_PARAM, "Failed to encrypt the response: " + e.getMessage()); + } + + return Response.ok(jResp.toJson()).build(); } } - + } diff --git a/src/main/java/fi/methics/webapp/musaplink/coupling/cmd/CmdEnrollData.java b/src/main/java/fi/methics/webapp/musaplink/coupling/cmd/CmdEnrollData.java index a638657..2714260 100644 --- a/src/main/java/fi/methics/webapp/musaplink/coupling/cmd/CmdEnrollData.java +++ b/src/main/java/fi/methics/webapp/musaplink/coupling/cmd/CmdEnrollData.java @@ -1,14 +1,22 @@ package fi.methics.webapp.musaplink.coupling.cmd; import java.io.IOException; +import java.util.Base64; import java.util.UUID; -import fi.methics.webapp.musaplink.AccountStorage; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.generators.HKDFBytesGenerator; +import org.bouncycastle.crypto.params.HKDFParameters; + import fi.methics.webapp.musaplink.MusapLinkAccount; import fi.methics.webapp.musaplink.coupling.CouplingCommand; +import fi.methics.webapp.musaplink.coupling.json.CouplingApiMessage; import fi.methics.webapp.musaplink.coupling.json.EnrollDataReq; import fi.methics.webapp.musaplink.coupling.json.EnrollDataResp; -import fi.methics.webapp.musaplink.coupling.json.CouplingApiMessage; +import fi.methics.webapp.musaplink.link.json.MusapResp; +import fi.methics.webapp.musaplink.util.MusapException; +import fi.methics.webapp.musaplink.util.MusapTransportEncryption.TransportKeys; +import fi.methics.webapp.musaplink.util.db.AccountStorage; /** * Coupling API command for enrolling MUSAP to this MUSAP Link. @@ -33,14 +41,57 @@ public CouplingApiMessage execute() throws IOException { account.apnsToken = payload.apnstoken; account.fcmToken = payload.fcmtoken; account.musapid = UUID.randomUUID().toString(); + + String sharedSecret = payload.getSharedSecret(); + if (sharedSecret != null && sharedSecret.length() > 0) { + byte[] ss = Base64.getDecoder().decode(sharedSecret); + byte[][] keypair_mac_aes = this.deriveKeys(ss); + account.macKey = keypair_mac_aes[0]; + account.aesKey = keypair_mac_aes[1]; + } else { + if (this.getConfig().isTransportEncryptionRequired()) { + log.error("Transport encryption is required, but client did not provide a secret"); + throw new MusapException(MusapResp.ERROR_WRONG_PARAM, "Missing transport security secret"); + } else { + log.info("Skipping transport encryption as client did not provide a secret"); + } + } log.debug("Storing account with MusapID " + account.musapid); AccountStorage.storeAccount(account); - EnrollDataResp resp = new EnrollDataResp(); - resp.musapid = account.musapid; + EnrollDataResp respPayload = new EnrollDataResp(); + respPayload.musapid = account.musapid; - return req.createResponse(resp); + CouplingApiMessage resp = req.createResponse(respPayload); + TransportKeys keys = account.getTransportKeys(); + + resp.musapid = account.musapid; + if (keys != null) { + try { + this.getTransportEncryption().encrypt(resp, keys); + } catch (Exception e) { + log.warn("Failed to encrypt response", e); + } + } + return resp; + } + + /** + * Calculates MAC and AES keys and returns them in a small array in that order (indexes 0, 1) + * @param secret Shared secret + */ + public byte[][] deriveKeys(final byte[] secret) { + HKDFBytesGenerator hkdf = new HKDFBytesGenerator(new SHA256Digest()); + hkdf.init(new HKDFParameters(secret, null, null)); + byte[] macKey = new byte[32]; + byte[] aesKey = new byte[16]; + byte[] output = new byte[macKey.length+aesKey.length]; + + hkdf.generateBytes(output, 0, output.length); + System.arraycopy(output, 0, macKey, 0, macKey.length); + System.arraycopy(output, macKey.length, aesKey, 0, aesKey.length); + return new byte[][] {macKey, aesKey}; } } diff --git a/src/main/java/fi/methics/webapp/musaplink/coupling/cmd/CmdExternalSignature.java b/src/main/java/fi/methics/webapp/musaplink/coupling/cmd/CmdExternalSignature.java index 693f536..d12ee13 100644 --- a/src/main/java/fi/methics/webapp/musaplink/coupling/cmd/CmdExternalSignature.java +++ b/src/main/java/fi/methics/webapp/musaplink/coupling/cmd/CmdExternalSignature.java @@ -4,7 +4,6 @@ import java.util.Map; import java.util.stream.Collectors; -import fi.methics.webapp.musaplink.AccountStorage; import fi.methics.webapp.musaplink.MusapLinkAccount; import fi.methics.webapp.musaplink.MusapLinkAccount.MusapKey; import fi.methics.webapp.musaplink.coupling.CouplingCommand; @@ -16,6 +15,7 @@ import fi.methics.webapp.musaplink.link.json.MusapResp; import fi.methics.webapp.musaplink.util.IdGenerator; import fi.methics.webapp.musaplink.util.MusapException; +import fi.methics.webapp.musaplink.util.db.AccountStorage; import fi.methics.webapp.musaplink.util.etsi204.Etsi204Client; import fi.methics.webapp.musaplink.util.etsi204.Etsi204Exception; import fi.methics.webapp.musaplink.util.etsi204.Etsi204Response; @@ -40,7 +40,7 @@ public CouplingApiMessage execute() throws Exception { CouplingApiMessage req = this.getRequest(); String musapid = req.musapid; - MusapLinkAccount account = AccountStorage.findByMusapId(musapid); + MusapLinkAccount account = AccountStorage.findAccountByMusapId(musapid); if (account == null) throw new MusapException(MusapResp.ERROR_UNKNOWN_USER); ExternalSignatureReq sigReq = this.getRequestPayload(); @@ -126,6 +126,7 @@ private void sendRequest(MusapLinkAccount account, try { sigResp.publickey = resp.getPublicKeyB64(); sigResp.certificate = resp.getCertificateB64(); + sigResp.certChain = resp.getCertificateChain(); } catch (Exception e) { log.warn("Failed to parse certificate from response", e); } diff --git a/src/main/java/fi/methics/webapp/musaplink/coupling/cmd/CmdGenerateKeyCallback.java b/src/main/java/fi/methics/webapp/musaplink/coupling/cmd/CmdGenerateKeyCallback.java index 15efae2..b34b8cf 100644 --- a/src/main/java/fi/methics/webapp/musaplink/coupling/cmd/CmdGenerateKeyCallback.java +++ b/src/main/java/fi/methics/webapp/musaplink/coupling/cmd/CmdGenerateKeyCallback.java @@ -1,14 +1,14 @@ package fi.methics.webapp.musaplink.coupling.cmd; -import fi.methics.webapp.musaplink.AccountStorage; import fi.methics.webapp.musaplink.MusapLinkAccount; -import fi.methics.webapp.musaplink.TxnStorage; import fi.methics.webapp.musaplink.coupling.CouplingCommand; import fi.methics.webapp.musaplink.coupling.json.CouplingApiMessage; import fi.methics.webapp.musaplink.coupling.json.GenerateKeyCallbackResp; import fi.methics.webapp.musaplink.link.json.MusapResp; import fi.methics.webapp.musaplink.link.json.MusapSignResp; import fi.methics.webapp.musaplink.util.MusapException; +import fi.methics.webapp.musaplink.util.db.AccountStorage; +import fi.methics.webapp.musaplink.util.db.TxnStorage; /** * Coupling API command for delivering a key generation response to MUSAP Link @@ -27,7 +27,7 @@ public CouplingApiMessage execute() throws Exception { String transid = req.transid; log.info("Got generated key for MUSAP ID " + musapid); - MusapLinkAccount account = AccountStorage.findByMusapId(musapid); + MusapLinkAccount account = AccountStorage.findAccountByMusapId(musapid); if (account == null) throw new MusapException(MusapResp.ERROR_UNKNOWN_USER); GenerateKeyCallbackResp callback = req.getPayload(GenerateKeyCallbackResp.class); diff --git a/src/main/java/fi/methics/webapp/musaplink/coupling/cmd/CmdGetData.java b/src/main/java/fi/methics/webapp/musaplink/coupling/cmd/CmdGetData.java index 0a03732..339bf4c 100644 --- a/src/main/java/fi/methics/webapp/musaplink/coupling/cmd/CmdGetData.java +++ b/src/main/java/fi/methics/webapp/musaplink/coupling/cmd/CmdGetData.java @@ -1,13 +1,13 @@ package fi.methics.webapp.musaplink.coupling.cmd; -import fi.methics.webapp.musaplink.AccountStorage; import fi.methics.webapp.musaplink.MusapLinkAccount; -import fi.methics.webapp.musaplink.TxnStorage; import fi.methics.webapp.musaplink.coupling.CouplingCommand; import fi.methics.webapp.musaplink.coupling.json.CouplingApiMessage; import fi.methics.webapp.musaplink.coupling.json.SignatureReq; import fi.methics.webapp.musaplink.link.json.MusapResp; import fi.methics.webapp.musaplink.util.MusapException; +import fi.methics.webapp.musaplink.util.db.AccountStorage; +import fi.methics.webapp.musaplink.util.db.TxnStorage; /** * Coupling API command for checking for pending signature or key generation requests. @@ -27,7 +27,7 @@ public CouplingApiMessage execute() throws Exception { log.info("Getting data for MUSAP ID " + musapid); log.debug("Total requests stored: " + TxnStorage.countTransactions()); - MusapLinkAccount account = AccountStorage.findByMusapId(musapid); + MusapLinkAccount account = AccountStorage.findAccountByMusapId(musapid); if (account == null) throw new MusapException(MusapResp.ERROR_UNKNOWN_USER); for (String linkid : account.linkids) { diff --git a/src/main/java/fi/methics/webapp/musaplink/coupling/cmd/CmdLinkAccount.java b/src/main/java/fi/methics/webapp/musaplink/coupling/cmd/CmdLinkAccount.java index 955cc14..169f51a 100644 --- a/src/main/java/fi/methics/webapp/musaplink/coupling/cmd/CmdLinkAccount.java +++ b/src/main/java/fi/methics/webapp/musaplink/coupling/cmd/CmdLinkAccount.java @@ -2,7 +2,6 @@ import java.io.IOException; -import fi.methics.webapp.musaplink.AccountStorage; import fi.methics.webapp.musaplink.coupling.CouplingCommand; import fi.methics.webapp.musaplink.coupling.json.CouplingApiMessage; import fi.methics.webapp.musaplink.coupling.json.LinkAccountReq; @@ -10,6 +9,8 @@ import fi.methics.webapp.musaplink.link.json.MusapResp; import fi.methics.webapp.musaplink.util.MusapException; import fi.methics.webapp.musaplink.util.MusapRandom; +import fi.methics.webapp.musaplink.util.db.AccountStorage; +import fi.methics.webapp.musaplink.util.db.CouplingStorage; /** * Coupling API command for requesting MUSAP to link with an RP @@ -44,7 +45,7 @@ public CouplingApiMessage execute() throws IOException { return req.createResponse(new LinkAccountResp(linkid, null)); } - final String linkid = AccountStorage.findLinkId(couplingcode); + final String linkid = CouplingStorage.findLinkId(couplingcode); if (linkid != null) { AccountStorage.addLinkId(payload.musapid, linkid); diff --git a/src/main/java/fi/methics/webapp/musaplink/coupling/cmd/CmdSignatureCallback.java b/src/main/java/fi/methics/webapp/musaplink/coupling/cmd/CmdSignatureCallback.java index f5621a8..1b0681e 100644 --- a/src/main/java/fi/methics/webapp/musaplink/coupling/cmd/CmdSignatureCallback.java +++ b/src/main/java/fi/methics/webapp/musaplink/coupling/cmd/CmdSignatureCallback.java @@ -1,14 +1,14 @@ package fi.methics.webapp.musaplink.coupling.cmd; -import fi.methics.webapp.musaplink.AccountStorage; import fi.methics.webapp.musaplink.MusapLinkAccount; -import fi.methics.webapp.musaplink.TxnStorage; import fi.methics.webapp.musaplink.coupling.CouplingCommand; import fi.methics.webapp.musaplink.coupling.json.CouplingApiMessage; import fi.methics.webapp.musaplink.coupling.json.SignatureCallbackResp; import fi.methics.webapp.musaplink.link.json.MusapResp; import fi.methics.webapp.musaplink.link.json.MusapSignResp; import fi.methics.webapp.musaplink.util.MusapException; +import fi.methics.webapp.musaplink.util.db.AccountStorage; +import fi.methics.webapp.musaplink.util.db.TxnStorage; /** * Coupling API command for delivering a signature generation response to MUSAP Link @@ -27,7 +27,7 @@ public CouplingApiMessage execute() throws Exception { String transid = req.transid; log.info("Got signature from MUSAP ID " + musapid); - MusapLinkAccount account = AccountStorage.findByMusapId(musapid); + MusapLinkAccount account = AccountStorage.findAccountByMusapId(musapid); if (account == null) throw new MusapException(MusapResp.ERROR_UNKNOWN_USER); SignatureCallbackResp callback = req.getPayload(SignatureCallbackResp.class); diff --git a/src/main/java/fi/methics/webapp/musaplink/coupling/cmd/CmdUpdateData.java b/src/main/java/fi/methics/webapp/musaplink/coupling/cmd/CmdUpdateData.java index 9bc8dde..4d05fe6 100644 --- a/src/main/java/fi/methics/webapp/musaplink/coupling/cmd/CmdUpdateData.java +++ b/src/main/java/fi/methics/webapp/musaplink/coupling/cmd/CmdUpdateData.java @@ -2,12 +2,12 @@ import java.io.IOException; -import fi.methics.webapp.musaplink.AccountStorage; import fi.methics.webapp.musaplink.MusapLinkAccount; import fi.methics.webapp.musaplink.coupling.CouplingCommand; import fi.methics.webapp.musaplink.coupling.json.CouplingApiMessage; import fi.methics.webapp.musaplink.coupling.json.UpdateDataReq; import fi.methics.webapp.musaplink.coupling.json.UpdateDataResp; +import fi.methics.webapp.musaplink.util.db.AccountStorage; public class CmdUpdateData extends CouplingCommand { @@ -23,7 +23,7 @@ public CouplingApiMessage execute() throws IOException { final UpdateDataReq payload = this.getRequestPayload(); - MusapLinkAccount account = AccountStorage.findByMusapId(req.musapid); + MusapLinkAccount account = AccountStorage.findAccountByMusapId(req.musapid); account.apnsToken = payload.apnstoken; account.fcmToken = payload.fcmtoken; diff --git a/src/main/java/fi/methics/webapp/musaplink/coupling/json/CouplingApiMessage.java b/src/main/java/fi/methics/webapp/musaplink/coupling/json/CouplingApiMessage.java index dc5a50a..275b0fe 100644 --- a/src/main/java/fi/methics/webapp/musaplink/coupling/json/CouplingApiMessage.java +++ b/src/main/java/fi/methics/webapp/musaplink/coupling/json/CouplingApiMessage.java @@ -210,10 +210,6 @@ public String calculateMac(final byte[] macKey) throws GeneralSecurityException, if (this.transid == null && this.musapid == null) { throw new IOException("Message is missing transid and uuid"); } - if (this.transid != null && this.musapid != null) { - log.trace("UUID and TransID not allowed in same message. UUID=" + this.musapid + ", TransID=" + this.transid); - throw new IOException("Message has both transid and uuid"); - } String msgid = this.transid != null ? this.transid : this.musapid; byte[] message = (msgid + this.type + this.iv + this.payload).getBytes(StandardCharsets.UTF_8); if (log.isTraceEnabled()) { @@ -304,7 +300,7 @@ public byte[] getPayload() { try { return Base64.getDecoder().decode(this.payload); } catch (Exception e) { - log.trace("Failed to decode payload:", e); + log.error("Failed to decode payload:", e); return null; } } @@ -325,7 +321,7 @@ public T getPayload(Class clazz) { } return payload; } catch (Exception e) { - log.trace("Could not parse payload", e); + log.error("Could not parse payload", e); return null; } } @@ -575,11 +571,11 @@ public void decrypt(final byte[] aesKey) throws GeneralSecurityException, IOExce log.debug("No key to decrypt with."); throw new IOException("Decryption failed. Missing decryption key."); } - log.trace("Decrypting payload: " + this.payload); + log.debug("Decrypting payload: " + this.payload); Cipher cipher = this.initCipher(Cipher.DECRYPT_MODE, aesKey); byte[] decrypted = cipher.doFinal(this.getPayload()); - this.payload = Base64.getEncoder().encodeToString(decrypted); - log.trace("Decrypted payload: " + this.payload); + this.payload = new String(decrypted, StandardCharsets.UTF_8); + log.debug("Decrypted payload: " + this.payload); this.isEncrypted = false; } diff --git a/src/main/java/fi/methics/webapp/musaplink/coupling/json/CouplingApiPayload.java b/src/main/java/fi/methics/webapp/musaplink/coupling/json/CouplingApiPayload.java index c74dd85..e76fec0 100644 --- a/src/main/java/fi/methics/webapp/musaplink/coupling/json/CouplingApiPayload.java +++ b/src/main/java/fi/methics/webapp/musaplink/coupling/json/CouplingApiPayload.java @@ -1,5 +1,12 @@ package fi.methics.webapp.musaplink.coupling.json; +import java.time.Instant; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import com.google.gson.annotations.SerializedName; + import fi.methics.webapp.musaplink.util.GsonMessage; /** @@ -7,13 +14,29 @@ */ public class CouplingApiPayload extends GsonMessage { + protected static final Log log = LogFactory.getLog(CouplingApiPayload.class); + // General values expected in MO messages + @SerializedName("os") public String os; + + @SerializedName("version") public String version; + + @SerializedName("osversion") public String osversion; + + @SerializedName("model") public String model; + @SerializedName("nonce") + public String nonce; + + @SerializedName("timestamp") + public String timestamp; + // General values expected in MT messages + @SerializedName("status") public String status; /** @@ -23,5 +46,19 @@ public class CouplingApiPayload extends GsonMessage { public void validate() throws Exception { // Default implementation is empty } + + /** + * Get the message timestamp as a Java {@link Instant} + * @return timestamp or null if unavailable or unparseable + */ + public Instant getTimestamp() { + if (this.timestamp == null) return null; + try { + return Instant.parse(this.timestamp); + } catch (Exception e) { + log.error("Failed to parse timestamp " + this.timestamp, e); + return null; + } + } } diff --git a/src/main/java/fi/methics/webapp/musaplink/coupling/json/EnrollDataReq.java b/src/main/java/fi/methics/webapp/musaplink/coupling/json/EnrollDataReq.java index 406ac52..e602e79 100644 --- a/src/main/java/fi/methics/webapp/musaplink/coupling/json/EnrollDataReq.java +++ b/src/main/java/fi/methics/webapp/musaplink/coupling/json/EnrollDataReq.java @@ -15,7 +15,6 @@ public class EnrollDataReq extends CouplingApiPayload { @SerializedName("apnstoken") public String apnstoken; - // This is always encrypted @SerializedName("tokendata") public String tokendata; @@ -25,7 +24,7 @@ public static EnrollDataReq fromJson(final String str) { /** * Security related tokens & shared secret. - * These can be encrypted by the app and decrypted on SAM + * This can be encrypted by the app and decrypted on MUSAP Link. */ public static class TokenData extends GsonMessage { @SerializedName("secret") @@ -38,4 +37,11 @@ public static EnrollDataReq.TokenData fromBase64(String b64) { } } + public String getSharedSecret() { + if (this.tokendata == null) return null; + TokenData data = TokenData.fromBase64(this.tokendata); + if (data == null) return null; + return data.secret; + } + } diff --git a/src/main/java/fi/methics/webapp/musaplink/coupling/json/ExternalSignatureResp.java b/src/main/java/fi/methics/webapp/musaplink/coupling/json/ExternalSignatureResp.java index cc255ab..93f3120 100644 --- a/src/main/java/fi/methics/webapp/musaplink/coupling/json/ExternalSignatureResp.java +++ b/src/main/java/fi/methics/webapp/musaplink/coupling/json/ExternalSignatureResp.java @@ -31,9 +31,12 @@ public class ExternalSignatureResp extends CouplingApiPayload { @SerializedName("certificate") public String certificate; + @SerializedName("certificate_chain") + public List certChain; + @SerializedName("attributes") public Map attributes = new HashMap<>(); - + public static ExternalSignatureResp fromJson(final String str) { return GSON.fromJson(str, ExternalSignatureResp.class); } diff --git a/src/main/java/fi/methics/webapp/musaplink/coupling/json/GenerateKeyCallbackResp.java b/src/main/java/fi/methics/webapp/musaplink/coupling/json/GenerateKeyCallbackResp.java index cf7a7fa..76f5e7d 100644 --- a/src/main/java/fi/methics/webapp/musaplink/coupling/json/GenerateKeyCallbackResp.java +++ b/src/main/java/fi/methics/webapp/musaplink/coupling/json/GenerateKeyCallbackResp.java @@ -1,5 +1,7 @@ package fi.methics.webapp.musaplink.coupling.json; +import java.util.List; + import com.google.gson.annotations.SerializedName; import fi.methics.webapp.musaplink.link.json.MusapCertificate; @@ -17,6 +19,9 @@ public class GenerateKeyCallbackResp extends CouplingApiPayload { @SerializedName("certificate") public String certificate; + + @SerializedName("certificate_chain") + public List certChain; @SerializedName("keyname") public String keyname; diff --git a/src/main/java/fi/methics/webapp/musaplink/link/LinkCommand.java b/src/main/java/fi/methics/webapp/musaplink/link/LinkCommand.java index 6953004..0b02596 100644 --- a/src/main/java/fi/methics/webapp/musaplink/link/LinkCommand.java +++ b/src/main/java/fi/methics/webapp/musaplink/link/LinkCommand.java @@ -10,6 +10,7 @@ import fi.methics.webapp.musaplink.link.json.MusapReq; import fi.methics.webapp.musaplink.link.json.MusapResp; +import fi.methics.webapp.musaplink.util.GsonMessage; import fi.methics.webapp.musaplink.util.MusapLinkConf; /** @@ -19,7 +20,7 @@ public abstract class LinkCommand { protected static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(10); - protected static final Gson GSON = new Gson(); + protected static final Gson GSON = GsonMessage.GSON; protected static final Log log = LogFactory.getLog(LinkCommand.class); diff --git a/src/main/java/fi/methics/webapp/musaplink/link/MusapLinkServlet.java b/src/main/java/fi/methics/webapp/musaplink/link/MusapLinkServlet.java index 7cd6aa2..7299722 100644 --- a/src/main/java/fi/methics/webapp/musaplink/link/MusapLinkServlet.java +++ b/src/main/java/fi/methics/webapp/musaplink/link/MusapLinkServlet.java @@ -11,8 +11,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import fi.methics.webapp.musaplink.AccountStorage; -import fi.methics.webapp.musaplink.TxnStorage; import fi.methics.webapp.musaplink.link.cmd.CmdDocSign; import fi.methics.webapp.musaplink.link.cmd.CmdGenerateKey; import fi.methics.webapp.musaplink.link.cmd.CmdLink; @@ -31,6 +29,8 @@ import fi.methics.webapp.musaplink.link.json.MusapUpdateKeyReq; import fi.methics.webapp.musaplink.link.json.MusapUpdateKeyResp; import fi.methics.webapp.musaplink.util.MusapLinkConf; +import fi.methics.webapp.musaplink.util.db.CouplingStorage; +import fi.methics.webapp.musaplink.util.db.TxnStorage; /** * Servlet for communication between AP and MUSAP Link. @@ -56,7 +56,7 @@ public static void init() { } log.info("MUSAP Link Servlet initialized"); TxnStorage.scheduleCleaner(Duration.ofMinutes(1).toMillis()); - AccountStorage.scheduleCleaner(Duration.ofMinutes(1).toMillis()); + CouplingStorage.scheduleCleaner(Duration.ofMinutes(1).toMillis()); } diff --git a/src/main/java/fi/methics/webapp/musaplink/link/cmd/CmdDocSign.java b/src/main/java/fi/methics/webapp/musaplink/link/cmd/CmdDocSign.java index b34c4cf..ca1ca14 100644 --- a/src/main/java/fi/methics/webapp/musaplink/link/cmd/CmdDocSign.java +++ b/src/main/java/fi/methics/webapp/musaplink/link/cmd/CmdDocSign.java @@ -5,10 +5,8 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import fi.methics.webapp.musaplink.AccountStorage; import fi.methics.webapp.musaplink.MusapLinkAccount; import fi.methics.webapp.musaplink.MusapLinkAccount.MusapKey; -import fi.methics.webapp.musaplink.TxnStorage; import fi.methics.webapp.musaplink.link.LinkCommand; import fi.methics.webapp.musaplink.link.json.MusapDocSignReq; import fi.methics.webapp.musaplink.link.json.MusapDocSignReq.DTBS; @@ -16,6 +14,8 @@ import fi.methics.webapp.musaplink.link.json.MusapSignResp; import fi.methics.webapp.musaplink.util.MusapException; import fi.methics.webapp.musaplink.util.SignatureCallback; +import fi.methics.webapp.musaplink.util.db.AccountStorage; +import fi.methics.webapp.musaplink.util.db.TxnStorage; import fi.methics.webapp.musaplink.util.push.PushClient; /** @@ -33,7 +33,7 @@ public MusapSignResp execute() throws MusapException { if (jReq == null) throw new MusapException(MusapResp.ERROR_WRONG_PARAM); String linkid = jReq.linkid; - MusapLinkAccount account = AccountStorage.findByLinkId(linkid); + MusapLinkAccount account = AccountStorage.findAccountByLinkId(linkid); if (account == null) { log.error("No account found with linkid " + linkid); throw new MusapException(MusapResp.ERROR_UNKNOWN_USER); diff --git a/src/main/java/fi/methics/webapp/musaplink/link/cmd/CmdGenerateKey.java b/src/main/java/fi/methics/webapp/musaplink/link/cmd/CmdGenerateKey.java index ec93c7f..f6aa262 100644 --- a/src/main/java/fi/methics/webapp/musaplink/link/cmd/CmdGenerateKey.java +++ b/src/main/java/fi/methics/webapp/musaplink/link/cmd/CmdGenerateKey.java @@ -5,10 +5,8 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import fi.methics.webapp.musaplink.AccountStorage; import fi.methics.webapp.musaplink.MusapLinkAccount; import fi.methics.webapp.musaplink.MusapLinkAccount.MusapKey; -import fi.methics.webapp.musaplink.TxnStorage; import fi.methics.webapp.musaplink.link.LinkCommand; import fi.methics.webapp.musaplink.link.json.MusapGenerateKeyReq; import fi.methics.webapp.musaplink.link.json.MusapGenerateKeyResp; @@ -16,6 +14,8 @@ import fi.methics.webapp.musaplink.link.json.MusapSignResp; import fi.methics.webapp.musaplink.util.MusapException; import fi.methics.webapp.musaplink.util.SignatureCallback; +import fi.methics.webapp.musaplink.util.db.AccountStorage; +import fi.methics.webapp.musaplink.util.db.TxnStorage; import fi.methics.webapp.musaplink.util.push.PushClient; /** @@ -33,7 +33,7 @@ public MusapGenerateKeyResp execute() throws MusapException { if (jReq == null) throw new MusapException(MusapResp.ERROR_WRONG_PARAM); String linkid = jReq.linkid; - MusapLinkAccount account = AccountStorage.findByLinkId(linkid); + MusapLinkAccount account = AccountStorage.findAccountByLinkId(linkid); if (account == null) { log.error("No account found with linkid " + linkid); throw new MusapException(MusapResp.ERROR_UNKNOWN_USER); diff --git a/src/main/java/fi/methics/webapp/musaplink/link/cmd/CmdLink.java b/src/main/java/fi/methics/webapp/musaplink/link/cmd/CmdLink.java index c7a05a3..1bb3865 100644 --- a/src/main/java/fi/methics/webapp/musaplink/link/cmd/CmdLink.java +++ b/src/main/java/fi/methics/webapp/musaplink/link/cmd/CmdLink.java @@ -2,12 +2,12 @@ import java.util.UUID; -import fi.methics.webapp.musaplink.AccountStorage; import fi.methics.webapp.musaplink.link.LinkCommand; import fi.methics.webapp.musaplink.link.json.MusapLinkReq; import fi.methics.webapp.musaplink.link.json.MusapLinkResp; import fi.methics.webapp.musaplink.util.CouplingCode; import fi.methics.webapp.musaplink.util.MusapException; +import fi.methics.webapp.musaplink.util.db.CouplingStorage; /** @@ -24,7 +24,7 @@ public MusapLinkResp execute() throws MusapException { MusapLinkResp jResp = new MusapLinkResp(); String linkid = newLinkId(); - CouplingCode code = AccountStorage.newCouplingCode(linkid); + CouplingCode code = CouplingStorage.newCouplingCode(linkid); jResp.linkid = linkid; jResp.couplingcode = code.getCode(); diff --git a/src/main/java/fi/methics/webapp/musaplink/link/cmd/CmdListKeys.java b/src/main/java/fi/methics/webapp/musaplink/link/cmd/CmdListKeys.java index 79b79f1..88e0c85 100644 --- a/src/main/java/fi/methics/webapp/musaplink/link/cmd/CmdListKeys.java +++ b/src/main/java/fi/methics/webapp/musaplink/link/cmd/CmdListKeys.java @@ -2,7 +2,6 @@ import java.util.Collection; -import fi.methics.webapp.musaplink.AccountStorage; import fi.methics.webapp.musaplink.MusapLinkAccount; import fi.methics.webapp.musaplink.MusapLinkAccount.MusapKey; import fi.methics.webapp.musaplink.link.LinkCommand; @@ -10,6 +9,7 @@ import fi.methics.webapp.musaplink.link.json.MusapListKeysResp; import fi.methics.webapp.musaplink.link.json.MusapResp; import fi.methics.webapp.musaplink.util.MusapException; +import fi.methics.webapp.musaplink.util.db.AccountStorage; /** @@ -34,7 +34,7 @@ public MusapListKeysResp execute() throws MusapException { if (jReq == null) throw new MusapException(MusapResp.ERROR_WRONG_PARAM); String linkid = jReq.linkid; - MusapLinkAccount account = AccountStorage.findByLinkId(linkid); + MusapLinkAccount account = AccountStorage.findAccountByLinkId(linkid); if (account == null) { log.error("No account found with linkid " + linkid); throw new MusapException(MusapResp.ERROR_UNKNOWN_USER); diff --git a/src/main/java/fi/methics/webapp/musaplink/link/cmd/CmdSign.java b/src/main/java/fi/methics/webapp/musaplink/link/cmd/CmdSign.java index bed94b3..3ce5cce 100644 --- a/src/main/java/fi/methics/webapp/musaplink/link/cmd/CmdSign.java +++ b/src/main/java/fi/methics/webapp/musaplink/link/cmd/CmdSign.java @@ -5,16 +5,16 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import fi.methics.webapp.musaplink.AccountStorage; import fi.methics.webapp.musaplink.MusapLinkAccount; import fi.methics.webapp.musaplink.MusapLinkAccount.MusapKey; -import fi.methics.webapp.musaplink.TxnStorage; import fi.methics.webapp.musaplink.link.LinkCommand; import fi.methics.webapp.musaplink.link.json.MusapResp; import fi.methics.webapp.musaplink.link.json.MusapSignReq; import fi.methics.webapp.musaplink.link.json.MusapSignResp; import fi.methics.webapp.musaplink.util.MusapException; import fi.methics.webapp.musaplink.util.SignatureCallback; +import fi.methics.webapp.musaplink.util.db.AccountStorage; +import fi.methics.webapp.musaplink.util.db.TxnStorage; import fi.methics.webapp.musaplink.util.push.PushClient; /** @@ -32,7 +32,7 @@ public MusapSignResp execute() throws MusapException { if (jReq == null) throw new MusapException(MusapResp.ERROR_WRONG_PARAM); String linkid = jReq.linkid; - MusapLinkAccount account = AccountStorage.findByLinkId(linkid); + MusapLinkAccount account = AccountStorage.findAccountByLinkId(linkid); if (account == null) { log.error("No account found with linkid " + linkid); throw new MusapException(MusapResp.ERROR_UNKNOWN_USER); diff --git a/src/main/java/fi/methics/webapp/musaplink/link/cmd/CmdUpdateKey.java b/src/main/java/fi/methics/webapp/musaplink/link/cmd/CmdUpdateKey.java index 3201755..729e1e1 100644 --- a/src/main/java/fi/methics/webapp/musaplink/link/cmd/CmdUpdateKey.java +++ b/src/main/java/fi/methics/webapp/musaplink/link/cmd/CmdUpdateKey.java @@ -1,6 +1,5 @@ package fi.methics.webapp.musaplink.link.cmd; -import fi.methics.webapp.musaplink.AccountStorage; import fi.methics.webapp.musaplink.MusapLinkAccount; import fi.methics.webapp.musaplink.MusapLinkAccount.MusapKey; import fi.methics.webapp.musaplink.link.LinkCommand; @@ -8,6 +7,7 @@ import fi.methics.webapp.musaplink.link.json.MusapUpdateKeyReq; import fi.methics.webapp.musaplink.link.json.MusapUpdateKeyResp; import fi.methics.webapp.musaplink.util.MusapException; +import fi.methics.webapp.musaplink.util.db.AccountStorage; /** * Link API command for updating key data. @@ -29,7 +29,7 @@ public MusapUpdateKeyResp execute() throws MusapException { if (jReq == null) throw new MusapException(MusapResp.ERROR_WRONG_PARAM); String linkid = jReq.linkid; - MusapLinkAccount account = AccountStorage.findByLinkId(linkid); + MusapLinkAccount account = AccountStorage.findAccountByLinkId(linkid); if (account == null) { log.error("No account found with linkid " + linkid); throw new MusapException(MusapResp.ERROR_UNKNOWN_USER); diff --git a/src/main/java/fi/methics/webapp/musaplink/link/json/MusapResp.java b/src/main/java/fi/methics/webapp/musaplink/link/json/MusapResp.java index ede3650..fc14de8 100644 --- a/src/main/java/fi/methics/webapp/musaplink/link/json/MusapResp.java +++ b/src/main/java/fi/methics/webapp/musaplink/link/json/MusapResp.java @@ -84,9 +84,17 @@ public static MusapResp createError(int errorcode) { * @return Response */ public static Response createErrorResponse(int errorcode) { - return Response.status(Status.INTERNAL_SERVER_ERROR).entity(MusapResp.createError(errorcode)).build(); + return createError(errorcode).toResponse(); + } + + /** + * Create a JAX-RS Response + * @param errorcode Error code + * @return Response + */ + public static Response createErrorResponse(int errorcode, String msg) { + return createError(errorcode, msg).toResponse(); } - /** * Get an error name matching the given code * @param errorcode Error code diff --git a/src/main/java/fi/methics/webapp/musaplink/util/ExpirableSet.java b/src/main/java/fi/methics/webapp/musaplink/util/ExpirableSet.java new file mode 100644 index 0000000..d2d88a4 --- /dev/null +++ b/src/main/java/fi/methics/webapp/musaplink/util/ExpirableSet.java @@ -0,0 +1,45 @@ +package fi.methics.webapp.musaplink.util; + +@SuppressWarnings("unchecked") +public class ExpirableSet { + + private ExpirableMap internalMap; + + /** + * Constructs a expiring wrapper for a set. + * Uses a standard ExpirableMap as the underlying map implementation. + * @param lifetime the maximum life time the objects should have + * in the map. (milliseconds) + */ + public ExpirableSet(final long lifetime) { + this.internalMap = new ExpirableMap(lifetime); + } + + /** + * Constructs a expiring wrapper for a set. + * Uses a standard ExpirableMap as the underlying map implementation. + * @param lifetime the maximum life time the objects should have + * in the map. (milliseconds) + */ + public ExpirableSet(final Interval lifetime) { + this.internalMap = new ExpirableMap(lifetime); + } + + /** + * Check if this set contains given key + * @param key Key to check + * @return true if contains + */ + public boolean containsKey(K key) { + return this.internalMap.containsKey(key); + } + + /** + * Add an entry to this set + * @param key Key to add + */ + public void add(K key) { + this.internalMap.put(key, null); + } + +} diff --git a/src/main/java/fi/methics/webapp/musaplink/util/MusapLinkConf.java b/src/main/java/fi/methics/webapp/musaplink/util/MusapLinkConf.java index 6131bd0..3d7359f 100644 --- a/src/main/java/fi/methics/webapp/musaplink/util/MusapLinkConf.java +++ b/src/main/java/fi/methics/webapp/musaplink/util/MusapLinkConf.java @@ -41,10 +41,8 @@ private MusapLinkConf(String filename) { this.filename = filename; this.initialized = true; this.properties = readProperties(this.filename); - if (this.properties != null) { - this.fcmConfig = new FcmConfig(this.properties, PREFIX); - this.apnsConfig = new ApnsConfig(this.properties, PREFIX); - } + this.fcmConfig = new FcmConfig(this.properties, PREFIX); + this.apnsConfig = new ApnsConfig(this.properties, PREFIX); } public static MusapLinkConf getInstance() { @@ -110,6 +108,14 @@ public boolean isInitialized() { return this.initialized; } + /** + * Should we treat transport encryption as mandatory? + * @return true if transport encryption is always mandatory in the Coupling API. + */ + public boolean isTransportEncryptionRequired() { + return Boolean.getBoolean(this.properties.getProperty(PREFIX + "transport.encryption.required", "false")); + } + /** * Get the configuration file path * @return conf file path diff --git a/src/main/java/fi/methics/webapp/musaplink/util/MusapTransportEncryption.java b/src/main/java/fi/methics/webapp/musaplink/util/MusapTransportEncryption.java index eac6de5..0abca4c 100644 --- a/src/main/java/fi/methics/webapp/musaplink/util/MusapTransportEncryption.java +++ b/src/main/java/fi/methics/webapp/musaplink/util/MusapTransportEncryption.java @@ -2,6 +2,8 @@ import java.io.IOException; import java.security.GeneralSecurityException; +import java.time.Duration; +import java.time.Instant; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -11,7 +13,10 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import fi.methics.webapp.musaplink.MusapLinkAccount; import fi.methics.webapp.musaplink.coupling.json.CouplingApiMessage; +import fi.methics.webapp.musaplink.coupling.json.CouplingApiPayload; +import fi.methics.webapp.musaplink.util.db.AccountStorage; public class MusapTransportEncryption { @@ -24,6 +29,8 @@ public class MusapTransportEncryption { private static final ExpirableMap EXP_MAP = new ExpirableMap<>(Interval.ofMinutes(10).toMillis()); private static final Map CACHE = Collections.synchronizedMap(EXP_MAP); + private static final ExpirableSet NONCE_SET = new ExpirableSet<>(Interval.ofMinutes(60).toMillis()); + private MusapLinkConf config; /** @@ -86,7 +93,7 @@ public CouplingApiMessage encrypt(final CouplingApiMessage msg, final TransportK msg.setMac(this.calculateMac(tkeys, msg)); return msg; } catch (Exception e) { - log.debug("Message encryption failed for MSISDN " + tkeys, e); + log.debug("Message encryption failed for " + tkeys, e); throw new IOException("Transport security error", e); } } @@ -95,7 +102,7 @@ public CouplingApiMessage encrypt(final CouplingApiMessage msg, final TransportK * Decrypt a message *

Also calculates and verifies MAC * @param msg Message to encrypt - * @param tkeys UUID + * @param tkeys Transport Keys * @return Decrypted message * @throws IOException * @throws GeneralSecurityException @@ -133,59 +140,74 @@ public CouplingApiMessage decrypt(final CouplingApiMessage msg, final TransportK } /** - * Clear transport encryption key cache for the given MSISDN - * @param uuid + * Clear transport encryption key cache for the given MUSAP ID + * @param musapid MUSAP ID */ - public void clearCache(final String uuid) { - CACHE.remove(uuid); + public void clearCache(final String musapid) { + CACHE.remove(musapid); } /** - * Resolve encryption key for given MSISDN - * @param uuid UUID + * Resolve encryption key for given MUSAP ID + * @param musapid MUSAP ID * @return Encryption keys (contains null keys if not found) */ - public TransportKeys resolveKeys(final String uuid) { - if (uuid == null) { + public TransportKeys resolveKeys(final String musapid) { + if (musapid == null) { return null; } - log.trace("Resolving transport keys for UUID " + uuid); + log.trace("Resolving transport keys for MUSAP ID " + musapid); - final TransportKeys cached = CACHE.get(uuid); + final TransportKeys cached = CACHE.get(musapid); if (cached != null) { log.trace("Resolved transport keys from cache"); return cached; } try { - //final AppKeys keys = this.config.getAppKeys(); - //try (Connection conn = keys.getConnectionRO("AppTransportEncryption.FETCH")) { - // final List keyRows = keys.fetchKeysByUUID(conn, uuid); - // if (keyRows.size() > 0) { - // AppKeys.Row enc = null; - // AppKeys.Row mac = null; - // for (AppKeys.Row k : keyRows) { - // if (k.keytype == AppKeys.KEYTYPE_AES) enc = k; - // if (k.keytype == AppKeys.KEYTYPE_MAC) mac = k; - // } - // if (enc != null && mac != null) { - // final TransportKeys txnKeys = new TransportKeys(uuid, enc.key, mac.key); - // if (this.config.isTransportEncryptionCacheEnabled()) { - // CACHE.put(uuid, txnKeys); - // } - // return txnKeys; - // } - // } - // log.debug("No transport keys found"); - // return null; - //} - return null; - + MusapLinkAccount account = AccountStorage.findAccountByMusapId(musapid); + return account.getTransportKeys(); } catch (Exception e) { log.debug("Failed to fetch transport keys", e); return null; } } + /** + * Check if the nonce in the given message is acceptable. + * This compares the nonce to a used nonce list, and verifies that the message + * timestamp is not too old. + * @param msg Message to check + * @return true if nonce is acceptable + */ + public boolean isNonceValid(CouplingApiMessage msg) { + + if (msg == null) return true; + if (msg.getBasePayload() == null) return true; + + CouplingApiPayload payload = msg.getBasePayload(); + String nonce = payload.nonce; + + if (nonce == null) return true; + if (NONCE_SET.containsKey(nonce)) { + log.warn("Potential replay attack: NONCE already used"); + return false; + } + NONCE_SET.add(nonce); + + if (payload.getTimestamp() == null) { + log.warn("Potential replay attack: No timestamp"); + return false; + } + + // Check if the timestamp is within an hour + if (Instant.now().minus(Duration.ofHours(1)).isBefore(payload.getTimestamp())) { + return true; + } else { + log.warn("Potential replay attack: Timestamp too old (" + payload.getTimestamp() + ")"); + return false; + } + } + /** * Calculate MAC for a message * @param keys Key set of user @@ -200,19 +222,22 @@ public String calculateMac(final TransportKeys keys, final CouplingApiMessage ms return msg.calculateMac(keys.mac); } + /** + * Class that contains MUSAP transport encryption keys + */ public static class TransportKeys { - public String uuid; + public String musapid; public byte[] enc; public byte[] mac; - public TransportKeys(final String uuid, final byte[] enc, final byte[] mac) { - this.uuid = uuid; + public TransportKeys(final String musapid, final byte[] enc, final byte[] mac) { + this.musapid = musapid; this.enc = enc; this.mac = mac; } @Override public int hashCode() { final int prime = 31; - int result = this.uuid != null ? this.uuid.hashCode() : 0; + int result = this.musapid != null ? this.musapid.hashCode() : 0; if (this.enc != null) result = prime * result + Arrays.hashCode(this.enc); if (this.mac != null) result = prime * result + Arrays.hashCode(this.mac); return result; @@ -223,9 +248,8 @@ public boolean equals(Object obj) { if (obj == null) return false; if (getClass() != obj.getClass()) return false; TransportKeys other = (TransportKeys) obj; - if (this.uuid != null) { - // MSISDN mismatch - if (!this.uuid.equals(other.uuid)) { + if (this.musapid != null) { + if (!this.musapid.equals(other.musapid)) { return false; } } @@ -242,8 +266,8 @@ public boolean equals(Object obj) { @Override public String toString() { - final StringBuilder sb = new StringBuilder("TKey{uuid="); - sb.append(this.uuid); + final StringBuilder sb = new StringBuilder("TKey{musapid="); + sb.append(this.musapid); sb.append(", enc"); sb.append(", mac"); sb.append("}"); diff --git a/src/main/java/fi/methics/webapp/musaplink/AccountStorage.java b/src/main/java/fi/methics/webapp/musaplink/util/db/AccountStorage.java similarity index 80% rename from src/main/java/fi/methics/webapp/musaplink/AccountStorage.java rename to src/main/java/fi/methics/webapp/musaplink/util/db/AccountStorage.java index 9c6e631..dd0ab09 100644 --- a/src/main/java/fi/methics/webapp/musaplink/AccountStorage.java +++ b/src/main/java/fi/methics/webapp/musaplink/util/db/AccountStorage.java @@ -1,4 +1,4 @@ -package fi.methics.webapp.musaplink; +package fi.methics.webapp.musaplink.util.db; import java.sql.Connection; import java.sql.PreparedStatement; @@ -9,17 +9,14 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; -import java.util.Timer; -import java.util.TimerTask; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import fi.methics.webapp.musaplink.MusapLinkAccount; import fi.methics.webapp.musaplink.MusapLinkAccount.MusapKey; -import fi.methics.webapp.musaplink.util.CouplingCode; import fi.methics.webapp.musaplink.util.MusapException; import fi.methics.webapp.musaplink.util.MusapLinkConf; -import fi.methics.webapp.musaplink.util.db.MusapDb; /** * Database class for MUSAP Link Account Storage. @@ -29,13 +26,10 @@ public class AccountStorage extends MusapDb { private static final Log log = LogFactory.getLog(AccountStorage.class); - private static final String INSERT_COUPLING_CODE = "INSERT INTO coupling_codes (couplingcode, linkid, created_dt) VALUES (?,?, ?)"; - private static final String SELECT_COUPLING_CODE = "SELECT linkid, couplingcode FROM coupling_codes WHERE couplingcode=?"; - private static final String DELETE_OLD_COUPLING_CODES = "DELETE FROM coupling_codes WHERE created_dt(listLinkIds(musapid)); + fillTransportKeys(conn, account); return account; } } @@ -149,31 +127,6 @@ public static MusapLinkAccount findByMusapId(String musapid) { return null; } - /** - * Check if a Link ID is found for the given coupling code. Removes it from the list if found. - * @param couplingCode Coupling Code - * @return Link ID if found. Null otherwise. - */ - public static String findLinkId(String couplingCode) { - - String linkid = null; - - try (Connection conn = getConnection(); - PreparedStatement ps = conn.prepareStatement(SELECT_COUPLING_CODE)) - { - ps.setString(1, couplingCode); - try (ResultSet result = ps.executeQuery()) { - if (result.next()) { - linkid = result.getString(1); - } - } - } catch (SQLException e) { - log.error("Failed find Link ID", e); - throw new MusapException(e); - } - return linkid; - } - /** * Get KeyID based on the given keyname * @param account MUSAP account @@ -319,56 +272,13 @@ public static Collection listKeyDetails(MusapLinkAccount account) { } - /** - * Generate a new Coupling Code for given LinkID. Store the combination. - * @param linkid Link ID - * @return Coupling Code - */ - public static synchronized CouplingCode newCouplingCode(String linkid) { - CouplingCode couplingCode = new CouplingCode(); - - while (findLinkId(couplingCode.getCode()) != null) { - couplingCode = new CouplingCode(); - } - - try (Connection conn = getConnection(); - PreparedStatement ps = conn.prepareStatement(INSERT_COUPLING_CODE)) - { - ps.setString(1, couplingCode.getCode()); - ps.setString(2, linkid); - ps.setTimestamp(3, new Timestamp(System.currentTimeMillis())); - ps.executeUpdate(); - } catch (SQLException e) { - log.error("Failed insert Coupling Code", e); - throw new MusapException(e); - } - return couplingCode; - } - - /** - * Schedule a transaction cleanup task - * @param interval Task run interval (milliseconds) - * @return a handle to the timer - */ - public static Timer scheduleCleaner(long interval) { - - Timer timer = new Timer(); - new Timer().scheduleAtFixedRate(new TimerTask() { - @Override - public void run() { - AccountStorage.cleanCouplingCodes(); - } - }, interval, interval); - return timer; - } - /** * Store a MUSAP account * @param account New account */ public static void storeAccount(MusapLinkAccount account) { if (account == null || account.musapid == null) { - log.error("Ignoring account with null musapid"); + log.error("Ignoring account with null MUSAP ID"); return; } @@ -380,11 +290,14 @@ public static void storeAccount(MusapLinkAccount account) { ps.setString(3, account.apnsToken); ps.setTimestamp(4, new Timestamp(System.currentTimeMillis())); ps.executeUpdate(); + + storeTransportKeys(conn, account); } catch (SQLException e) { log.error("Failed insert MUSAP account", e); throw new MusapException(e); } } + /** * Update a MUSAP account * @param musapid MUSAP ID @@ -484,4 +397,57 @@ public static void upsertKeyDetails(MusapLinkAccount account, MusapKey key) { } + + /** + * Store transport encryption keys. + * Does nothing if given account object has no keys. + * @param conn DB connection + * @param account Account that contains the keys + */ + private static void storeTransportKeys(Connection conn, MusapLinkAccount account) { + if (account == null) return; + if (account.aesKey == null) return; + if (account.macKey == null) return; + + log.debug("Storing transport keys for MUSAP ID " + account.musapid); + try (PreparedStatement ps = conn.prepareStatement(INSERT_KEYS)) { + ps.setString(1, account.musapid); + ps.setBytes(2, account.macKey); + ps.setBytes(3, account.aesKey); + ps.executeUpdate(); + } catch (SQLException e) { + log.error("Failed insert MUSAP transport keys", e); + throw new MusapException(e); + } + } + + /** + * Fetch and fill transport encryption keys to given MUSAP account + * @param conn DB Connection + * @param account Account that should be filled + * @return Account with keys + */ + private static MusapLinkAccount fillTransportKeys(Connection conn, MusapLinkAccount account) { + + log.debug("Looking for transport keys"); + if (account == null) return null; + + try (PreparedStatement ps = conn.prepareStatement(SELECT_KEYS)) { + ps.setString(1, account.musapid); + try (ResultSet result = ps.executeQuery()) { + if (result.next()) { + account.macKey = result.getBytes(1); + account.aesKey = result.getBytes(2); + if (account.aesKey != null) log.debug("Found AES key of " + account.aesKey.length + " bytes"); + if (account.macKey != null) log.debug("Found MAC key of " + account.macKey.length + " bytes"); + } + } + } catch (SQLException e) { + log.error("Failed get transport keys", e); + throw new MusapException(e); + } + return account; + } + + } diff --git a/src/main/java/fi/methics/webapp/musaplink/util/db/CouplingStorage.java b/src/main/java/fi/methics/webapp/musaplink/util/db/CouplingStorage.java new file mode 100644 index 0000000..21e4d38 --- /dev/null +++ b/src/main/java/fi/methics/webapp/musaplink/util/db/CouplingStorage.java @@ -0,0 +1,113 @@ +package fi.methics.webapp.musaplink.util.db; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.Timer; +import java.util.TimerTask; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import fi.methics.webapp.musaplink.util.CouplingCode; +import fi.methics.webapp.musaplink.util.MusapException; +import fi.methics.webapp.musaplink.util.MusapLinkConf; + +/** + * Database class for MUSAP Link Coupling Codes. + */ +public class CouplingStorage extends MusapDb { + + private static final Log log = LogFactory.getLog(CouplingStorage.class); + + private static final String INSERT_COUPLING_CODE = "INSERT INTO coupling_codes (couplingcode, linkid, created_dt) VALUES (?,?, ?)"; + private static final String SELECT_COUPLING_CODE = "SELECT linkid, couplingcode FROM coupling_codes WHERE couplingcode=?"; + private static final String DELETE_OLD_COUPLING_CODES = "DELETE FROM coupling_codes WHERE created_dt attrs) { + String eventid = attrs.get("eventid"); + return eventid != null ? eventid : transid; + } + + /** + * Resolve NoSpamCode from attribute "nospamcode". + * @param attrs Attributes + * @return NoSpamCode to use - or null if not found + */ + public String resolveNospamCode(Map attrs) { + return attrs.get("nospamcode"); + } + public static enum ClientType { REST("rest"), SOAP("soap"); diff --git a/src/main/java/fi/methics/webapp/musaplink/util/etsi204/Etsi204Response.java b/src/main/java/fi/methics/webapp/musaplink/util/etsi204/Etsi204Response.java index e8cfd6f..ed99668 100644 --- a/src/main/java/fi/methics/webapp/musaplink/util/etsi204/Etsi204Response.java +++ b/src/main/java/fi/methics/webapp/musaplink/util/etsi204/Etsi204Response.java @@ -3,7 +3,12 @@ import java.security.cert.CertificateEncodingException; import java.util.Base64; import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.bouncycastle.asn1.cms.ContentInfo; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cms.CMSSignedData; @@ -13,10 +18,13 @@ public class Etsi204Response { + private static final Log log = LogFactory.getLog(Etsi204Response.class); private byte[] signature; private byte[] publickey; private byte[] certificate; + private List certChain; + /** * Create a response from a Laverca SOAP response * @param resp Laverca SOAP response @@ -27,9 +35,10 @@ protected Etsi204Response(EtsiResponse resp) { if (resp.getPkcs7Signature() != null && resp.getPkcs7Signature().getSignerCert() != null) { this.certificate = resp.getPkcs7Signature().getSignerCert().getEncoded(); this.publickey = resp.getPkcs7Signature().getSignerCert().getPublicKey().getEncoded(); + this.certChain = this.getCertificateChain(resp); } } catch (Exception e) { - // Ignore + log.error("Failed to parse response details", e); } } @@ -62,6 +71,14 @@ public byte[] getSignature() { return this.signature; } + /** + * Get the certificate chain + * @return certificate chain + */ + public List getCertificateChain() { + return this.certChain; + } + /** * Get the signature as base64 String * @return signature base64 @@ -112,4 +129,19 @@ public String getCertificateB64() { return Base64.getEncoder().encodeToString(cert); } + /** + * Get the certificate chain + * @param resp ETSI TS 102 204 response + * @return Certificate chain + */ + private List getCertificateChain(EtsiResponse resp) { + return resp.getPkcs7Signature().getCertificateChain().stream().map(c -> { + try { + return c.getEncoded(); + } catch (CertificateEncodingException e) { + return null; + } + }).filter(Objects::nonNull).collect(Collectors.toList()); + } + } diff --git a/src/main/java/fi/methics/webapp/musaplink/util/etsi204/Etsi204RestClient.java b/src/main/java/fi/methics/webapp/musaplink/util/etsi204/Etsi204RestClient.java index 32917ad..fea5efa 100644 --- a/src/main/java/fi/methics/webapp/musaplink/util/etsi204/Etsi204RestClient.java +++ b/src/main/java/fi/methics/webapp/musaplink/util/etsi204/Etsi204RestClient.java @@ -1,11 +1,18 @@ package fi.methics.webapp.musaplink.util.etsi204; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import fi.laverca.ficom.FiComAdditionalServices; +import fi.laverca.jaxb.mss.AdditionalServiceType; import fi.methics.laverca.rest.MssClient; +import fi.methics.laverca.rest.json.AdditionalService; +import fi.methics.laverca.rest.util.DTBS; +import fi.methics.laverca.rest.util.MSS_SignatureReqBuilder; import fi.methics.laverca.rest.util.MssRestException; import fi.methics.laverca.rest.util.SignatureProfile; @@ -48,6 +55,25 @@ public Etsi204Response sign(final String msisdn, SignatureProfile sigprof = SignatureProfile.of(this.getSignatureProfile()); log.debug("Sending a signature request for MSISDN " + msisdn + " and SignatureProfile " + sigprof.getUri()); + + MSS_SignatureReqBuilder builder = new MSS_SignatureReqBuilder(); + builder.withMsisdn(msisdn); + builder.withDtbd(dtbd); + builder.withDtbs(new DTBS(dtbs, "base64", mimeType)); + builder.withSignatureProfile(sigprof); + + if (this.enableEventId) { + String eventid = this.resolveEventId(transid, attrs); + builder.withAdditionalService(AdditionalService.createEventIdService(eventid)); + } + if (this.enableNoSpamCode) { + String nospamcode = this.resolveNospamCode(attrs); + boolean validate = nospamcode != null; + builder.withAdditionalService(AdditionalService.createNoSpamCodeService(validate, nospamcode)); + } + + this.client.sign(builder.build()); + byte[] signature = this.client.sign(msisdn, dtbd, dtbs, mimeType, sigprof); return new Etsi204Response(signature); } catch (MssRestException e) { diff --git a/src/main/java/fi/methics/webapp/musaplink/util/etsi204/Etsi204SoapClient.java b/src/main/java/fi/methics/webapp/musaplink/util/etsi204/Etsi204SoapClient.java index bb227b5..544ff54 100644 --- a/src/main/java/fi/methics/webapp/musaplink/util/etsi204/Etsi204SoapClient.java +++ b/src/main/java/fi/methics/webapp/musaplink/util/etsi204/Etsi204SoapClient.java @@ -45,10 +45,13 @@ public Etsi204Response sign(final String msisdn, List additionalServices = new ArrayList<>(); if (this.enableEventId) { - additionalServices.add(FiComAdditionalServices.createEventIdService(transid)); + String eventid = this.resolveEventId(transid, attrs); + additionalServices.add(FiComAdditionalServices.createEventIdService(eventid)); } if (this.enableNoSpamCode) { - additionalServices.add(FiComAdditionalServices.createNoSpamService(null, false)); + String nospamcode = this.resolveNospamCode(attrs); + boolean validate = nospamcode != null; + additionalServices.add(FiComAdditionalServices.createNoSpamService(nospamcode, validate)); } String mimeType = attrs.get(ATTR_MIMETYPE); @@ -69,4 +72,5 @@ public Etsi204Response sign(final String msisdn, } } + }