Skip to content

Commit 788d25a

Browse files
authored
Merge pull request #61 from shift7-ch/feature/uvf-draft
UVFVault implementation.
2 parents 542a3f9 + 00af91e commit 788d25a

File tree

11 files changed

+277
-164
lines changed

11 files changed

+277
-164
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
java-version: 21
2828
cache: maven
2929
- name: Build with Maven
30-
run: mvn --no-transfer-progress --batch-mode verify
30+
run: mvn --no-transfer-progress --batch-mode verify -U
3131
env:
3232
VAULT_TOKEN: ${{ secrets.VAULT_TOKEN }}
3333
TESTCONTAINERS_RYUK_DISABLED: true

hub/src/main/java/ch/iterate/hub/crypto/uvf/UvfMetadataPayload.java

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,10 @@
88
import ch.cyberduck.core.AlphanumericRandomStringService;
99
import ch.cyberduck.core.cryptomator.random.FastSecureRandomProvider;
1010

11-
import org.bouncycastle.crypto.Digest;
12-
import org.bouncycastle.crypto.digests.SHA256Digest;
13-
import org.bouncycastle.crypto.macs.HMac;
14-
import org.bouncycastle.crypto.params.KeyParameter;
15-
import org.bouncycastle.util.encoders.Base32;
11+
import org.apache.commons.lang3.StringUtils;
12+
import org.cryptomator.cryptolib.api.Cryptor;
13+
import org.cryptomator.cryptolib.api.CryptorProvider;
14+
import org.cryptomator.cryptolib.api.UVFMasterkey;
1615
import org.cryptomator.cryptolib.common.P384KeyPair;
1716
import org.openapitools.jackson.nullable.JsonNullableModule;
1817

@@ -26,8 +25,8 @@
2625
import java.util.Map;
2726
import java.util.Objects;
2827
import java.util.UUID;
28+
import java.util.stream.Collectors;
2929

30-
import at.favre.lib.hkdf.HKDF;
3130
import ch.iterate.hub.crypto.exceptions.NotECKeyException;
3231
import ch.iterate.hub.model.JWEPayload;
3332
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@@ -93,8 +92,14 @@ public static UvfMetadataPayload fromJWE(final String jwe) throws JsonProcessing
9392
return mapper.readValue(jwe, UvfMetadataPayload.class);
9493
}
9594

95+
public String toJSON() throws JsonProcessingException {
96+
ObjectMapper mapper = new ObjectMapper();
97+
mapper.registerModule(new JsonNullableModule());
98+
return mapper.writeValueAsString(this);
99+
}
100+
96101
public static UvfMetadataPayload create() {
97-
final String kid = new AlphanumericRandomStringService(4).random();
102+
final String kid = Base64URL.encode(new AlphanumericRandomStringService(4).random()).toString();
98103
final byte[] rawSeed = new byte[32];
99104
FastSecureRandomProvider.get().provide().nextBytes(rawSeed);
100105
final byte[] kdfSalt = new byte[32];
@@ -103,28 +108,21 @@ public static UvfMetadataPayload create() {
103108
.withFileFormat("AES-256-GCM-32k")
104109
.withNameFormat("AES-SIV-512-B64URL")
105110
.withSeeds(new HashMap<String, String>() {{
106-
put(kid, Base64URL.encode(rawSeed).toString());
111+
put(kid, Base64.getEncoder().encodeToString(rawSeed));
107112
}})
108113
.withLatestSeed(kid)
109114
.withinitialSeed(kid)
110115
.withKdf("HKDF-SHA512")
111-
.withKdfSalt(Base64URL.encode(kdfSalt).toString());
112-
}
113-
114-
public byte[] computeRootDirId() {
115-
return HKDF.fromHmacSha512().extractAndExpand(Base64URL.from(kdfSalt()).decode(), Base64URL.from(seeds().get(initialSeed())).decode(), "rootDirId".getBytes(), 256 / 8);
116+
.withKdfSalt(Base64.getEncoder().encodeToString(kdfSalt));
116117
}
117118

118-
public String computeRootDirIdHash(final byte[] rootDirId) {
119-
final byte[] hmacKey = HKDF.fromHmacSha512()
120-
.extractAndExpand(Base64URL.from(kdfSalt()).decode(), Base64URL.from(seeds().get(initialSeed())).decode(), "hmac".getBytes(), 512 / 8);
121-
final Digest digest = new SHA256Digest();
122-
final HMac hMac = new HMac(digest);
123-
hMac.init(new KeyParameter(hmacKey));
124-
hMac.update(rootDirId, 0, rootDirId.length);
125-
final byte[] hmacOut = new byte[hMac.getMacSize()];
126-
hMac.doFinal(hmacOut, 0);
127-
return Base32.toBase32String(Arrays.copyOfRange(hmacOut, 0, 20));
119+
public String computeRootDirIdHash() throws JsonProcessingException {
120+
final UVFMasterkey masterKey = UVFMasterkey.fromDecryptedPayload(this.toJSON());
121+
final CryptorProvider provider = CryptorProvider.forScheme(CryptorProvider.Scheme.UVF_DRAFT);
122+
final Cryptor cryptor = provider.provide(masterKey, FastSecureRandomProvider.get().provide());
123+
final byte[] rootDirId = masterKey.rootDirId();
124+
final String hashedRootDirId = cryptor.fileNameCryptor(masterKey.firstRevision()).hashDirectoryId(rootDirId);
125+
return hashedRootDirId;
128126
}
129127

130128

@@ -328,11 +326,11 @@ public String toString() {
328326
return "UvfMetadataPayload{" +
329327
"fileFormat='" + fileFormat + '\'' +
330328
", nameFormat='" + nameFormat + '\'' +
331-
", seeds=" + seeds +
329+
", seeds={" + seeds.entrySet().stream().map(e -> e.getKey() + "=" + StringUtils.repeat("*", Integer.min(8, StringUtils.length(e.getValue())))).collect(Collectors.joining(", ")) + "}" +
332330
", initialSeed='" + initialSeed + '\'' +
333331
", latestSeed='" + latestSeed + '\'' +
334332
", kdf='" + kdf + '\'' +
335-
", kdfSalt='" + kdfSalt + '\'' +
333+
", kdfSalt='" + StringUtils.repeat("*", Integer.min(8, StringUtils.length(kdf))) + '\'' +
336334
", automaticAccessGrant=" + automaticAccessGrant +
337335
", storage=" + storage +
338336
'}';

hub/src/main/java/ch/iterate/hub/protocols/hub/HubCryptoVault.java

Lines changed: 28 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,10 @@
88
import ch.cyberduck.core.AbstractPath;
99
import ch.cyberduck.core.DisabledListProgressListener;
1010
import ch.cyberduck.core.ListService;
11-
import ch.cyberduck.core.LoginOptions;
12-
import ch.cyberduck.core.PasswordCallback;
1311
import ch.cyberduck.core.Path;
1412
import ch.cyberduck.core.Session;
1513
import ch.cyberduck.core.cryptomator.ContentWriter;
16-
import ch.cyberduck.core.cryptomator.CryptoVault;
14+
import ch.cyberduck.core.cryptomator.UVFVault;
1715
import ch.cyberduck.core.exception.BackgroundException;
1816
import ch.cyberduck.core.features.Directory;
1917
import ch.cyberduck.core.preferences.PreferencesFactory;
@@ -22,47 +20,50 @@
2220

2321
import org.apache.logging.log4j.LogManager;
2422
import org.apache.logging.log4j.Logger;
25-
import org.cryptomator.cryptolib.api.CryptorProvider;
26-
import org.cryptomator.cryptolib.api.Masterkey;
23+
import org.cryptomator.cryptolib.api.UVFMasterkey;
2724

2825
import java.nio.charset.StandardCharsets;
29-
import java.util.Base64;
3026
import java.util.EnumSet;
3127

3228
/**
3329
* Cryptomator vault implementation for Cipherduck (without masterkey file).
3430
*/
35-
public class HubCryptoVault extends CryptoVault {
31+
public class HubCryptoVault extends UVFVault {
3632
private static final Logger log = LogManager.getLogger(HubCryptoVault.class);
33+
private final String decryptedPayload;
3734

38-
// See https://github.com/cryptomator/hub/blob/develop/frontend/src/common/vaultconfig.ts
39-
//const jwtPayload: VaultConfigPayload = {
40-
// jti: vaultId,
41-
// format: 8,
42-
// cipherCombo: 'SIV_GCM',
43-
// shorteningThreshold: 220
44-
//};
45-
//const header = JSON.stringify({
46-
// kid: kid,
47-
// typ: 'jwt',
48-
// alg: 'HS256',
49-
// hub: hubConfig
50-
//});
51-
private static final VaultConfig VAULT_CONFIG = new VaultConfig(8, 220, CryptorProvider.Scheme.SIV_GCM, "HS256", null);
5235

5336
public HubCryptoVault(final Path home) {
54-
super(home);
37+
this(home, null, null, null); // TODO cleanup
5538
}
5639

57-
public HubCryptoVault(final Path home, final String masterkey, final String config, final byte[] pepper) {
58-
super(home);
40+
public HubCryptoVault(final Path home, final String decryptedPayload, final String config, final byte[] pepper) {
41+
super(home, decryptedPayload, config, pepper);
42+
this.decryptedPayload = decryptedPayload;
5943
}
6044

45+
public Path encrypt(Session<?> session, Path file, byte[] directoryId, boolean metadata) throws BackgroundException {
46+
log.debug("HubCryptoVault.encrypt. Use directory ID '{}' for folder {}", directoryId, file);
47+
return super.encrypt(session, file, directoryId, metadata);
48+
}
49+
50+
51+
@Override
52+
public Path getHome() {
53+
final Path home = super.getHome();
54+
final UVFMasterkey masterKey = UVFMasterkey.fromDecryptedPayload(this.decryptedPayload);
55+
byte[] directoryId = masterKey.rootDirId();
56+
assert directoryId != null;
57+
home.attributes().setDirectoryId(directoryId);
58+
return home;
59+
}
60+
61+
6162
/**
6263
* Upload vault template into existing bucket (permanent credentials)
6364
*/
6465
// TODO https://github.com/shift7-ch/cipherduck-hub/issues/19 review @dko check method signature?
65-
public synchronized Path create(final Session<?> session, final String region, final VaultCredentials credentials, final int version, final String metadata, final String rootDirHash) throws BackgroundException {
66+
public synchronized Path create(final Session<?> session, final String region, final VaultCredentials credentials, final int version, final String metadata, final String hashedRootDirId) throws BackgroundException {
6667
final Path home = new Path(session.getHost().getDefaultPath(), EnumSet.of(AbstractPath.Type.directory));
6768
log.debug("Uploading vault template {} in {} ", home, session.getHost());
6869

@@ -75,11 +76,11 @@ public synchronized Path create(final Session<?> session, final String region, f
7576
// zip.file('vault.cryptomator', this.vaultConfigToken);
7677
// zip.folder('d')?.folder(this.rootDirHash.substring(0, 2))?.folder(this.rootDirHash.substring(2));
7778
(new ContentWriter(session)).write(new Path(home, PreferencesFactory.get().getProperty("cryptomator.vault.config.filename"), EnumSet.of(AbstractPath.Type.file, AbstractPath.Type.vault)), metadata.getBytes(StandardCharsets.US_ASCII));
78-
Directory<?> directory = (Directory) session._getFeature(Directory.class);
79+
Directory<?> directory = (Directory<?>) session._getFeature(Directory.class);
7980

8081
// TODO https://github.com/shift7-ch/cipherduck-hub/issues/19 implement CryptoDirectory for uvf
8182
// Path secondLevel = this.directoryProvider.toEncrypted(session, this.home.attributes().getDirectoryId(), this.home);
82-
final Path secondLevel = new Path(String.format("/%s/d/%s/%s/", session.getHost().getDefaultPath(), rootDirHash.substring(0, 2), rootDirHash.substring(2)), EnumSet.of(AbstractPath.Type.directory));
83+
final Path secondLevel = new Path(String.format("/%s/d/%s/%s/", session.getHost().getDefaultPath(), hashedRootDirId.substring(0, 2), hashedRootDirId.substring(2)), EnumSet.of(AbstractPath.Type.directory));
8384
final Path firstLevel = secondLevel.getParent();
8485
final Path dataDir = firstLevel.getParent();
8586
log.debug("Create vault root directory at {}", secondLevel);
@@ -91,19 +92,6 @@ public synchronized Path create(final Session<?> session, final String region, f
9192
return home;
9293
}
9394

94-
@Override
95-
public HubCryptoVault load(final Session<?> session, final PasswordCallback prompt) throws BackgroundException {
96-
// no-interactive prompt in Cipherduck
97-
final String masterkey = prompt.prompt(session.getHost(), "", "", new LoginOptions()).getPassword();
98-
try {
99-
this.open(VAULT_CONFIG, new Masterkey(Base64.getDecoder().decode(masterkey)));
100-
}
101-
catch(IllegalArgumentException e) {
102-
throw new BackgroundException(e);
103-
}
104-
return this;
105-
}
106-
10795
public Path getMasterkey() {
10896
// No master key in vault
10997
return null;

hub/src/main/java/ch/iterate/hub/protocols/s3/S3AutoLoadVaultSession.java

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,7 @@
4545
import ch.iterate.hub.workflows.VaultServiceImpl;
4646
import ch.iterate.hub.workflows.exceptions.AccessException;
4747
import ch.iterate.hub.workflows.exceptions.SecurityFailure;
48-
import com.google.common.primitives.Bytes;
49-
import com.nimbusds.jose.util.Base64URL;
48+
import com.fasterxml.jackson.core.JsonProcessingException;
5049

5150
public class S3AutoLoadVaultSession extends S3AssumeRoleSession {
5251
private static final Logger log = LogManager.getLogger(S3AutoLoadVaultSession.class);
@@ -84,25 +83,21 @@ public void login(final LoginCallback prompt, final CancelCallback cancel) throw
8483
super.login(prompt, cancel);
8584
final Path home = new DelegatingHomeFeature(new DefaultPathHomeFeature(host)).find();
8685
log.debug("Attempting to locate vault in {}", home);
87-
final Vault vault = VaultFactory.get(home);
88-
// TODO https://github.com/shift7-ch/cipherduck-hub/issues/19 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! MUST NEVER BE RELEASED LIKE THIS
89-
// TODO https://github.com/shift7-ch/cipherduck-hub/issues/19 use rawFileKey,rawNameKey as vault key for now (going into cryptolib's Masterkey)
9086
// Retrieve from keychain
9187
final DeviceKeys deviceKeys = new DeviceKeysServiceImpl(keychain).getDeviceKeys(backend.getHost());
9288
final UvfMetadataPayload vaultMetadata = new VaultServiceImpl(backend).getVaultMetadataJWE(
9389
UUID.fromString(host.getUuid()), new UserKeysServiceImpl(backend).getUserKeys(backend.getHost(), backend.getMe(), deviceKeys));
94-
final byte[] rawFileKey = Base64URL.from(vaultMetadata.seeds().get(vaultMetadata.latestSeed())).decode();
95-
final byte[] rawNameKey = Base64URL.from(vaultMetadata.seeds().get(vaultMetadata.latestSeed())).decode();
96-
final byte[] vaultKey = Bytes.concat(rawFileKey, rawNameKey);
90+
final String decryptedPayload = vaultMetadata.toJSON();
91+
final Vault vault = VaultFactory.get(home, decryptedPayload, "", new byte[0]);
9792
registry.add(vault.load(this, new DisabledPasswordCallback() {
9893
@Override
9994
public Credentials prompt(final Host bookmark, final String title, final String reason, final LoginOptions options) {
100-
return new VaultCredentials(Base64.getEncoder().encodeToString(vaultKey));
95+
return new VaultCredentials(decryptedPayload);
10196
}
10297
}));
10398
backend.close();
10499
}
105-
catch(ApiException | SecurityFailure | AccessException e) {
100+
catch(ApiException | SecurityFailure | AccessException | JsonProcessingException e) {
106101
throw new LoginFailureException(LocaleFactory.localizedString("Login failed", "Credentials"), e);
107102
}
108103
}

0 commit comments

Comments
 (0)