-
Notifications
You must be signed in to change notification settings - Fork 0
Review UVFMetadata compliance. #216
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,7 @@ | |
| import java.text.ParseException; | ||
| import java.util.Arrays; | ||
| import java.util.Base64; | ||
| import java.util.Collections; | ||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
|
|
@@ -32,6 +33,7 @@ | |
|
|
||
| import cloud.katta.crypto.exceptions.NotECKeyException; | ||
| import cloud.katta.model.JWEPayload; | ||
| import cloud.katta.workflows.exceptions.SecurityFailure; | ||
| import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | ||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||
| import com.fasterxml.jackson.core.JsonProcessingException; | ||
|
|
@@ -56,9 +58,27 @@ | |
| /** | ||
| * Represents payload of <a href="https://github.com/encryption-alliance/unified-vault-format/blob/develop/vault%20metadata/README.md"><code>vault.uvf</code> metadata</a>. | ||
| * Counterpart of <a href="https://github.com/shift7-ch/katta-server/blob/feature/cipherduck-uvf/frontend/src/common/universalVaultFormat.ts"><code>MetadataPayload</code></a>. | ||
| * <p> | ||
| * It has two custom fields: | ||
| * <ul> | ||
| * <li>org.cryptomator.automaticAccessGrant (upstream)</li> | ||
| * <li>cloud.katta.storage</li> | ||
| * </ul> | ||
| * It has at two recipients: | ||
| * <ul> | ||
| * <li>org.cryptomator.hub.memberkey shared with vault members (having access to the member key)</li> | ||
| * <li>org.cryptomator.hub.recoverykey. shared with vault owners (having access to the recovery key)</li> | ||
| * </ul> | ||
| */ | ||
| @JsonIgnoreProperties(ignoreUnknown = true) | ||
| public class UvfMetadataPayload extends JWEPayload { | ||
| private static final String UVF_SPEC_VERSION_KEY_PARAM = "uvf.spec.version"; | ||
|
|
||
| private static final String KID_MEMBERKEY = "org.cryptomator.hub.memberkey"; | ||
| private static final String KID_RECOVERYKEY_PREFIX = "org.cryptomator.hub.recoverykey.%s"; | ||
|
|
||
| private static final String UVF_FILEFORMAT = "AES-256-GCM-32k"; | ||
| private static final String UVF_NAME_FORMAT = "AES-SIV-512-B64URL"; | ||
|
|
||
| @JsonProperty(value = "fileFormat", required = true) | ||
| String fileFormat; | ||
|
|
@@ -106,9 +126,11 @@ public static UvfMetadataPayload create() { | |
| FastSecureRandomProvider.get().provide().nextBytes(rawSeed); | ||
| final byte[] kdfSalt = new byte[32]; | ||
| FastSecureRandomProvider.get().provide().nextBytes(kdfSalt); | ||
|
|
||
|
|
||
| return new UvfMetadataPayload() | ||
| .withFileFormat("AES-256-GCM-32k") | ||
| .withNameFormat("AES-SIV-512-B64URL") | ||
| .withFileFormat(UVF_FILEFORMAT) | ||
| .withNameFormat(UVF_NAME_FORMAT) | ||
| .withSeeds(new HashMap<String, String>() {{ | ||
| put(kid, Base64.getUrlEncoder().encodeToString(rawSeed)); | ||
| }}) | ||
|
|
@@ -151,7 +173,7 @@ public OctetSequenceKey memberKey() { | |
|
|
||
| private UniversalVaultFormatJWKS() throws JOSEException { | ||
| memberKey = new OctetSequenceKeyGenerator(256) | ||
| .keyID("org.cryptomator.hub.memberkey") | ||
| .keyID(KID_MEMBERKEY) | ||
| .algorithm(JWEAlgorithm.A256KW) | ||
| .generate(); | ||
|
|
||
|
|
@@ -161,10 +183,11 @@ private UniversalVaultFormatJWKS() throws JOSEException { | |
| recoveryKey.getPublic()) | ||
| .build(); | ||
|
|
||
|
|
||
| recoveryKeyJWK = new ECKey.Builder(Curve.P_384, | ||
| recoveryKey.getPublic()) | ||
| .algorithm(JWEAlgorithm.ECDH_ES_A256KW) | ||
| .keyID(String.format("org.cryptomator.hub.recoverykey.%s", recoveryKeyJWKWithoutThumbprint.computeThumbprint())) | ||
| .keyID(String.format("%s%s", KID_RECOVERYKEY_PREFIX, recoveryKeyJWKWithoutThumbprint.computeThumbprint())) | ||
| .privateKey(recoveryKey.getPrivate()) | ||
| .build(); | ||
| } | ||
|
|
@@ -188,8 +211,9 @@ public UvfAccessTokenPayload toOwnerAccessToken() { | |
| } | ||
|
|
||
| public static OctetSequenceKey memberKeyFromRawKey(final byte[] raw) { | ||
|
|
||
| return new OctetSequenceKey.Builder(raw) | ||
| .keyID("org.cryptomator.hub.memberkey") | ||
| .keyID(KID_MEMBERKEY) | ||
| .algorithm(JWEAlgorithm.A256KW) | ||
| .build(); | ||
| } | ||
|
|
@@ -294,9 +318,19 @@ public UvfMetadataPayload withStorage(final VaultMetadataJWEBackendDto backend) | |
| * @param jwe The jwe | ||
| * @param jwk The jwk | ||
| */ | ||
| public static UvfMetadataPayload decryptWithJWK(final String jwe, final JWK jwk) throws ParseException, JOSEException, JsonProcessingException { | ||
| public static UvfMetadataPayload decryptWithJWK(final String jwe, final JWK jwk) throws ParseException, JOSEException, JsonProcessingException, SecurityFailure { | ||
| final JWEObjectJSON jweObject = JWEObjectJSON.parse(jwe); | ||
| jweObject.decrypt(new MultiDecrypter(jwk)); | ||
| jweObject.decrypt(new MultiDecrypter(jwk, Collections.singleton(UVF_SPEC_VERSION_KEY_PARAM))); | ||
|
|
||
| // https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.11 | ||
| // Recipients MAY consider the JWS to be invalid if the critical | ||
| // list contains any Header Parameter names defined by this | ||
| // specification or [JWA] for use with JWS or if any other constraints on its use are violated. | ||
| final Object uvfSpecVersion = jweObject.getHeader().getCustomParams().get(UVF_SPEC_VERSION_KEY_PARAM); | ||
| if(!"1".equals(uvfSpecVersion)) { | ||
| throw new SecurityFailure(String.format("Unexpected value for critical header %s: found %s, expected \"1\"", UVF_SPEC_VERSION_KEY_PARAM, uvfSpecVersion)); | ||
| } | ||
|
Comment on lines
+329
to
+332
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @overheadhunter do we need/want to verify the spec version? Same then web. https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, needs to be checked. Also it needs to be an integer, not a String! |
||
|
|
||
| final Payload payload = jweObject.getPayload(); | ||
| return UvfMetadataPayload.fromJWE(payload.toString()); | ||
| } | ||
|
|
@@ -309,10 +343,16 @@ public static UvfMetadataPayload decryptWithJWK(final String jwe, final JWK jwk) | |
| * @param keys recipient keys for whom to encrypt | ||
| */ | ||
| public String encrypt(final String apiURL, final UUID vaultId, final JWKSet keys) throws JOSEException { | ||
| // spec: https://github.com/encryption-alliance/unified-vault-format/tree/develop/vault%20metadata#jose-header | ||
| // web frontend implementation: https://github.com/shift7-ch/katta-server/blob/feature/cipherduck-uvf/frontend/src/common/universalVaultFormat.ts#L343-L346 | ||
| final JWEObjectJSON builder = new JWEObjectJSON( | ||
| new JWEHeader.Builder(EncryptionMethod.A256GCM) | ||
| .customParam("origin", String.format("%s/vaults/%s/uvf/vault.uvf", apiURL, vaultId.toString())) | ||
| // kid goes into recipient-specific header | ||
| .customParam("origin", URI.create(String.format("%s/vaults/%s/uvf/vault.uvf", apiURL, vaultId.toString())).normalize().toString()) | ||
| .jwkURL(URI.create("jwks.json")) | ||
| .contentType("json") | ||
| .criticalParams(Collections.singleton(UVF_SPEC_VERSION_KEY_PARAM)) | ||
| .customParam(UVF_SPEC_VERSION_KEY_PARAM, "1") | ||
| .build(), | ||
| new Payload(new HashMap<String, Object>() {{ | ||
| put("fileFormat", fileFormat); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ | |
| import ch.cyberduck.core.ssl.DisabledX509TrustManager; | ||
|
|
||
| import org.cryptomator.cryptolib.api.UVFMasterkey; | ||
| import org.junit.jupiter.api.Disabled; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| import java.nio.charset.StandardCharsets; | ||
|
|
@@ -30,8 +31,10 @@ | |
| import cloud.katta.crypto.exceptions.NotECKeyException; | ||
| import cloud.katta.protocols.hub.HubProtocol; | ||
| import cloud.katta.protocols.hub.HubSession; | ||
| import cloud.katta.workflows.exceptions.SecurityFailure; | ||
| import com.fasterxml.jackson.core.JsonProcessingException; | ||
| import com.nimbusds.jose.JOSEException; | ||
| import com.nimbusds.jose.JWEObjectJSON; | ||
| import com.nimbusds.jose.jwk.Curve; | ||
| import com.nimbusds.jose.jwk.ECKey; | ||
| import com.nimbusds.jose.jwk.JWKSet; | ||
|
|
@@ -75,7 +78,7 @@ void recoveryKeyToOwnerAccessTokenAndBack() throws JOSEException, ParseException | |
| } | ||
|
|
||
| @Test | ||
| void encryptDecrypt() throws JOSEException, JsonProcessingException, ParseException { | ||
| void encryptDecrypt() throws JOSEException, JsonProcessingException, ParseException, SecurityFailure { | ||
| final byte[] rawMasterKey = new byte[32]; | ||
| FastSecureRandomProvider.get().provide().nextBytes(rawMasterKey); | ||
| final HashMap<String, String> keys = new HashMap<String, String>() {{ | ||
|
|
@@ -103,17 +106,20 @@ void encryptDecrypt() throws JOSEException, JsonProcessingException, ParseExcept | |
| final OctetSequenceKey memberKey = jwks.memberKey(); | ||
| final ECKey recoveryKey = jwks.recoveryKey(); | ||
|
|
||
| final String encrypted = orig.encrypt("https://example.com/api/", UUID.randomUUID(), jwks.toJWKSet()); | ||
| final UUID vaultId = UUID.randomUUID(); | ||
| final String encrypted = orig.encrypt("https://example.com/api/", vaultId, jwks.toJWKSet()); | ||
|
|
||
| // decrypt with memberKey | ||
| { | ||
| final UvfMetadataPayload decrypted = UvfMetadataPayload.decryptWithJWK(encrypted, memberKey); | ||
| assertEquals(String.format("https://example.com/api/vaults/%s/uvf/vault.uvf", vaultId), JWEObjectJSON.parse(encrypted).getHeader().getCustomParams().get("origin")); | ||
| assertEquals(orig, decrypted); | ||
| } | ||
|
|
||
| // decrypt with recoveryKey | ||
| { | ||
| final UvfMetadataPayload decrypted = UvfMetadataPayload.decryptWithJWK(encrypted, recoveryKey); | ||
| assertEquals(String.format("https://example.com/api/vaults/%s/uvf/vault.uvf", vaultId), JWEObjectJSON.parse(encrypted).getHeader().getCustomParams().get("origin")); | ||
| assertEquals(orig, decrypted); | ||
| } | ||
|
|
||
|
|
@@ -126,7 +132,8 @@ void encryptDecrypt() throws JOSEException, JsonProcessingException, ParseExcept | |
| } | ||
|
|
||
| @Test | ||
| void decryptWithRecoveryKey() throws ParseException, JOSEException, NoSuchAlgorithmException, InvalidKeySpecException, NotECKeyException, JsonProcessingException { | ||
| @Disabled("TODO uvf.spec.version missing in protected.") | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Resolve.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| void decryptWithRecoveryKey() throws ParseException, JOSEException, NoSuchAlgorithmException, InvalidKeySpecException, NotECKeyException, JsonProcessingException, SecurityFailure { | ||
| // https://datatracker.ietf.org/doc/html/rfc7516#section-7.2.1 | ||
| final String jwe = "{\"protected\":\"eyJvcmlnaW4iOiJodHRwczovL2V4YW1wbGUuY29tL2FwaS92YXVsdHMvVE9ETy91dmYvdmF1bHQudXZmIiwiamt1Ijoiandrcy5qc29uIiwiZW5jIjoiQTI1NkdDTSJ9\",\"recipients\":[{\"header\":{\"kid\":\"org.cryptomator.hub.memberkey\",\"alg\":\"A256KW\"},\"encrypted_key\":\"XLoNIWvDKQqaDurrGt7VK9s2aggSMir7fS4ZdBUxdTxceCOHndo4kA\"},{\"header\":{\"kid\":\"org.cryptomator.hub.recoverykey.v2nb-mGX4POKMWCQKOogMWTlAn7DDqEOjjEGCsPEeco\",\"alg\":\"ECDH-ES+A256KW\",\"epk\":{\"key_ops\":[],\"ext\":true,\"kty\":\"EC\",\"x\":\"j6Retxx-L-rURQ4WNc8LvoqjbdPtGS6n9pCJgcm1U-NAWuWEvwJ_qi2tlrv_4w4p\",\"y\":\"wS-Emo-Q9qdtkHMJiDfVDAaxhF2-nSkDRn2Eg9CbG0pVwGEpaDybx_YYJwIaYooO\",\"crv\":\"P-384\"},\"apu\":\"\",\"apv\":\"\"},\"encrypted_key\":\"iNGgybMqmiXn_lbKLMMTpg38i1f00O6Zj65d5nzsLw3hyzuylGWpvA\"}],\"iv\":\"Pfy90C9SSq2gJr6B\",\"ciphertext\":\"ogYR1pZN9k97zEgO9Fj3ePQramtaUdHWq95geXD7FH1oB6T7fEOvdU2AEGWOcbIbQihn-eOqG2_5oTol16O_nQ4HcDOJ9w4R9EdpByuWG-kVNh_fpWeQjIuH4kO-Rtbf05JRVG2jexWopbIA8uHuoiOXSNpSYPTzTKirp2hU7w3sE01zycsu06HiasUX-tKZH_hbyiUEdTlFFLcvKpRwnYOQf6QMw0uY1IbUTX1cJY9LO5SpD8bZFZOd6hg_Qnsdcq52I8KkZyxocgqdW7P5OSUrv5z8DCLMPdByEpaz9cCOzQQvtZwHxJy82O4vDAh89QA_AzfK8J7TI5zJRlTGQgrNhiaVBC85fN3tMSv8sLfJs7rC_5LiVW5ZeqbQ52sAZQw0lfwgGpMmxsdMzPoVOLD8OxvX\",\"tag\":\"3Jiv6kI4Qoso60T0dRv9vIlca-P4UFyHqh-TEZvargM\"}"; | ||
| final ECKey key = new ECKey.Builder( | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.