diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e24c6c4a..913cc1e6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,7 @@ jobs: java-version: 21 cache: maven - name: Build with Maven - run: mvn --no-transfer-progress --batch-mode verify + run: mvn --no-transfer-progress --batch-mode verify -U env: VAULT_TOKEN: ${{ secrets.VAULT_TOKEN }} TESTCONTAINERS_RYUK_DISABLED: true diff --git a/hub/src/main/java/ch/iterate/hub/crypto/uvf/UvfMetadataPayload.java b/hub/src/main/java/ch/iterate/hub/crypto/uvf/UvfMetadataPayload.java index d15e3675..03b5aac9 100644 --- a/hub/src/main/java/ch/iterate/hub/crypto/uvf/UvfMetadataPayload.java +++ b/hub/src/main/java/ch/iterate/hub/crypto/uvf/UvfMetadataPayload.java @@ -8,11 +8,10 @@ import ch.cyberduck.core.AlphanumericRandomStringService; import ch.cyberduck.core.cryptomator.random.FastSecureRandomProvider; -import org.bouncycastle.crypto.Digest; -import org.bouncycastle.crypto.digests.SHA256Digest; -import org.bouncycastle.crypto.macs.HMac; -import org.bouncycastle.crypto.params.KeyParameter; -import org.bouncycastle.util.encoders.Base32; +import org.apache.commons.lang3.StringUtils; +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.CryptorProvider; +import org.cryptomator.cryptolib.api.UVFMasterkey; import org.cryptomator.cryptolib.common.P384KeyPair; import org.openapitools.jackson.nullable.JsonNullableModule; @@ -26,8 +25,8 @@ import java.util.Map; import java.util.Objects; import java.util.UUID; +import java.util.stream.Collectors; -import at.favre.lib.hkdf.HKDF; import ch.iterate.hub.crypto.exceptions.NotECKeyException; import ch.iterate.hub.model.JWEPayload; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -93,8 +92,14 @@ public static UvfMetadataPayload fromJWE(final String jwe) throws JsonProcessing return mapper.readValue(jwe, UvfMetadataPayload.class); } + public String toJSON() throws JsonProcessingException { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JsonNullableModule()); + return mapper.writeValueAsString(this); + } + public static UvfMetadataPayload create() { - final String kid = new AlphanumericRandomStringService(4).random(); + final String kid = Base64URL.encode(new AlphanumericRandomStringService(4).random()).toString(); final byte[] rawSeed = new byte[32]; FastSecureRandomProvider.get().provide().nextBytes(rawSeed); final byte[] kdfSalt = new byte[32]; @@ -103,28 +108,21 @@ public static UvfMetadataPayload create() { .withFileFormat("AES-256-GCM-32k") .withNameFormat("AES-SIV-512-B64URL") .withSeeds(new HashMap() {{ - put(kid, Base64URL.encode(rawSeed).toString()); + put(kid, Base64.getEncoder().encodeToString(rawSeed)); }}) .withLatestSeed(kid) .withinitialSeed(kid) .withKdf("HKDF-SHA512") - .withKdfSalt(Base64URL.encode(kdfSalt).toString()); - } - - public byte[] computeRootDirId() { - return HKDF.fromHmacSha512().extractAndExpand(Base64URL.from(kdfSalt()).decode(), Base64URL.from(seeds().get(initialSeed())).decode(), "rootDirId".getBytes(), 256 / 8); + .withKdfSalt(Base64.getEncoder().encodeToString(kdfSalt)); } - public String computeRootDirIdHash(final byte[] rootDirId) { - final byte[] hmacKey = HKDF.fromHmacSha512() - .extractAndExpand(Base64URL.from(kdfSalt()).decode(), Base64URL.from(seeds().get(initialSeed())).decode(), "hmac".getBytes(), 512 / 8); - final Digest digest = new SHA256Digest(); - final HMac hMac = new HMac(digest); - hMac.init(new KeyParameter(hmacKey)); - hMac.update(rootDirId, 0, rootDirId.length); - final byte[] hmacOut = new byte[hMac.getMacSize()]; - hMac.doFinal(hmacOut, 0); - return Base32.toBase32String(Arrays.copyOfRange(hmacOut, 0, 20)); + public String computeRootDirIdHash() throws JsonProcessingException { + final UVFMasterkey masterKey = UVFMasterkey.fromDecryptedPayload(this.toJSON()); + final CryptorProvider provider = CryptorProvider.forScheme(CryptorProvider.Scheme.UVF_DRAFT); + final Cryptor cryptor = provider.provide(masterKey, FastSecureRandomProvider.get().provide()); + final byte[] rootDirId = masterKey.rootDirId(); + final String hashedRootDirId = cryptor.fileNameCryptor(masterKey.firstRevision()).hashDirectoryId(rootDirId); + return hashedRootDirId; } @@ -328,11 +326,11 @@ public String toString() { return "UvfMetadataPayload{" + "fileFormat='" + fileFormat + '\'' + ", nameFormat='" + nameFormat + '\'' + - ", seeds=" + seeds + + ", seeds={" + seeds.entrySet().stream().map(e -> e.getKey() + "=" + StringUtils.repeat("*", Integer.min(8, StringUtils.length(e.getValue())))).collect(Collectors.joining(", ")) + "}" + ", initialSeed='" + initialSeed + '\'' + ", latestSeed='" + latestSeed + '\'' + ", kdf='" + kdf + '\'' + - ", kdfSalt='" + kdfSalt + '\'' + + ", kdfSalt='" + StringUtils.repeat("*", Integer.min(8, StringUtils.length(kdf))) + '\'' + ", automaticAccessGrant=" + automaticAccessGrant + ", storage=" + storage + '}'; diff --git a/hub/src/main/java/ch/iterate/hub/protocols/hub/HubCryptoVault.java b/hub/src/main/java/ch/iterate/hub/protocols/hub/HubCryptoVault.java index 27639f53..44b55951 100644 --- a/hub/src/main/java/ch/iterate/hub/protocols/hub/HubCryptoVault.java +++ b/hub/src/main/java/ch/iterate/hub/protocols/hub/HubCryptoVault.java @@ -8,12 +8,10 @@ import ch.cyberduck.core.AbstractPath; import ch.cyberduck.core.DisabledListProgressListener; import ch.cyberduck.core.ListService; -import ch.cyberduck.core.LoginOptions; -import ch.cyberduck.core.PasswordCallback; import ch.cyberduck.core.Path; import ch.cyberduck.core.Session; import ch.cyberduck.core.cryptomator.ContentWriter; -import ch.cyberduck.core.cryptomator.CryptoVault; +import ch.cyberduck.core.cryptomator.UVFVault; import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.features.Directory; import ch.cyberduck.core.preferences.PreferencesFactory; @@ -22,47 +20,50 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.cryptomator.cryptolib.api.CryptorProvider; -import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.api.UVFMasterkey; import java.nio.charset.StandardCharsets; -import java.util.Base64; import java.util.EnumSet; /** * Cryptomator vault implementation for Cipherduck (without masterkey file). */ -public class HubCryptoVault extends CryptoVault { +public class HubCryptoVault extends UVFVault { private static final Logger log = LogManager.getLogger(HubCryptoVault.class); + private final String decryptedPayload; - // See https://github.com/cryptomator/hub/blob/develop/frontend/src/common/vaultconfig.ts - //const jwtPayload: VaultConfigPayload = { - // jti: vaultId, - // format: 8, - // cipherCombo: 'SIV_GCM', - // shorteningThreshold: 220 - //}; - //const header = JSON.stringify({ - // kid: kid, - // typ: 'jwt', - // alg: 'HS256', - // hub: hubConfig - //}); - private static final VaultConfig VAULT_CONFIG = new VaultConfig(8, 220, CryptorProvider.Scheme.SIV_GCM, "HS256", null); public HubCryptoVault(final Path home) { - super(home); + this(home, null, null, null); // TODO cleanup } - public HubCryptoVault(final Path home, final String masterkey, final String config, final byte[] pepper) { - super(home); + public HubCryptoVault(final Path home, final String decryptedPayload, final String config, final byte[] pepper) { + super(home, decryptedPayload, config, pepper); + this.decryptedPayload = decryptedPayload; } + public Path encrypt(Session session, Path file, byte[] directoryId, boolean metadata) throws BackgroundException { + log.debug("HubCryptoVault.encrypt. Use directory ID '{}' for folder {}", directoryId, file); + return super.encrypt(session, file, directoryId, metadata); + } + + + @Override + public Path getHome() { + final Path home = super.getHome(); + final UVFMasterkey masterKey = UVFMasterkey.fromDecryptedPayload(this.decryptedPayload); + byte[] directoryId = masterKey.rootDirId(); + assert directoryId != null; + home.attributes().setDirectoryId(directoryId); + return home; + } + + /** * Upload vault template into existing bucket (permanent credentials) */ // TODO https://github.com/shift7-ch/cipherduck-hub/issues/19 review @dko check method signature? - public synchronized Path create(final Session session, final String region, final VaultCredentials credentials, final int version, final String metadata, final String rootDirHash) throws BackgroundException { + public synchronized Path create(final Session session, final String region, final VaultCredentials credentials, final int version, final String metadata, final String hashedRootDirId) throws BackgroundException { final Path home = new Path(session.getHost().getDefaultPath(), EnumSet.of(AbstractPath.Type.directory)); log.debug("Uploading vault template {} in {} ", home, session.getHost()); @@ -75,11 +76,11 @@ public synchronized Path create(final Session session, final String region, f // zip.file('vault.cryptomator', this.vaultConfigToken); // zip.folder('d')?.folder(this.rootDirHash.substring(0, 2))?.folder(this.rootDirHash.substring(2)); (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)); - Directory directory = (Directory) session._getFeature(Directory.class); + Directory directory = (Directory) session._getFeature(Directory.class); // TODO https://github.com/shift7-ch/cipherduck-hub/issues/19 implement CryptoDirectory for uvf // Path secondLevel = this.directoryProvider.toEncrypted(session, this.home.attributes().getDirectoryId(), this.home); - 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)); + 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)); final Path firstLevel = secondLevel.getParent(); final Path dataDir = firstLevel.getParent(); log.debug("Create vault root directory at {}", secondLevel); @@ -91,19 +92,6 @@ public synchronized Path create(final Session session, final String region, f return home; } - @Override - public HubCryptoVault load(final Session session, final PasswordCallback prompt) throws BackgroundException { - // no-interactive prompt in Cipherduck - final String masterkey = prompt.prompt(session.getHost(), "", "", new LoginOptions()).getPassword(); - try { - this.open(VAULT_CONFIG, new Masterkey(Base64.getDecoder().decode(masterkey))); - } - catch(IllegalArgumentException e) { - throw new BackgroundException(e); - } - return this; - } - public Path getMasterkey() { // No master key in vault return null; diff --git a/hub/src/main/java/ch/iterate/hub/protocols/s3/S3AutoLoadVaultSession.java b/hub/src/main/java/ch/iterate/hub/protocols/s3/S3AutoLoadVaultSession.java index 167f1785..185d1cc9 100644 --- a/hub/src/main/java/ch/iterate/hub/protocols/s3/S3AutoLoadVaultSession.java +++ b/hub/src/main/java/ch/iterate/hub/protocols/s3/S3AutoLoadVaultSession.java @@ -45,8 +45,7 @@ import ch.iterate.hub.workflows.VaultServiceImpl; import ch.iterate.hub.workflows.exceptions.AccessException; import ch.iterate.hub.workflows.exceptions.SecurityFailure; -import com.google.common.primitives.Bytes; -import com.nimbusds.jose.util.Base64URL; +import com.fasterxml.jackson.core.JsonProcessingException; public class S3AutoLoadVaultSession extends S3AssumeRoleSession { private static final Logger log = LogManager.getLogger(S3AutoLoadVaultSession.class); @@ -84,25 +83,21 @@ public void login(final LoginCallback prompt, final CancelCallback cancel) throw super.login(prompt, cancel); final Path home = new DelegatingHomeFeature(new DefaultPathHomeFeature(host)).find(); log.debug("Attempting to locate vault in {}", home); - final Vault vault = VaultFactory.get(home); - // TODO https://github.com/shift7-ch/cipherduck-hub/issues/19 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! MUST NEVER BE RELEASED LIKE THIS - // TODO https://github.com/shift7-ch/cipherduck-hub/issues/19 use rawFileKey,rawNameKey as vault key for now (going into cryptolib's Masterkey) // Retrieve from keychain final DeviceKeys deviceKeys = new DeviceKeysServiceImpl(keychain).getDeviceKeys(backend.getHost()); final UvfMetadataPayload vaultMetadata = new VaultServiceImpl(backend).getVaultMetadataJWE( UUID.fromString(host.getUuid()), new UserKeysServiceImpl(backend).getUserKeys(backend.getHost(), backend.getMe(), deviceKeys)); - final byte[] rawFileKey = Base64URL.from(vaultMetadata.seeds().get(vaultMetadata.latestSeed())).decode(); - final byte[] rawNameKey = Base64URL.from(vaultMetadata.seeds().get(vaultMetadata.latestSeed())).decode(); - final byte[] vaultKey = Bytes.concat(rawFileKey, rawNameKey); + final String decryptedPayload = vaultMetadata.toJSON(); + final Vault vault = VaultFactory.get(home, decryptedPayload, "", new byte[0]); registry.add(vault.load(this, new DisabledPasswordCallback() { @Override public Credentials prompt(final Host bookmark, final String title, final String reason, final LoginOptions options) { - return new VaultCredentials(Base64.getEncoder().encodeToString(vaultKey)); + return new VaultCredentials(decryptedPayload); } })); backend.close(); } - catch(ApiException | SecurityFailure | AccessException e) { + catch(ApiException | SecurityFailure | AccessException | JsonProcessingException e) { throw new LoginFailureException(LocaleFactory.localizedString("Login failed", "Credentials"), e); } } diff --git a/hub/src/main/java/ch/iterate/hub/workflows/CreateVaultService.java b/hub/src/main/java/ch/iterate/hub/workflows/CreateVaultService.java index eae33655..625c351c 100644 --- a/hub/src/main/java/ch/iterate/hub/workflows/CreateVaultService.java +++ b/hub/src/main/java/ch/iterate/hub/workflows/CreateVaultService.java @@ -69,51 +69,51 @@ public CreateVaultService(final HubSession hubSession) { public void createVault(final UserKeys userKeys, final StorageProfileDtoWrapper storageProfileWrapper, final CreateVaultModel vaultModel) throws ApiException, AccessException, SecurityFailure, BackgroundException { try { final UvfMetadataPayload.UniversalVaultFormatJWKS jwks = UvfMetadataPayload.createKeys(); - final VaultMetadataJWEBackendDto backendDto = new VaultMetadataJWEBackendDto() - .provider(storageProfileWrapper.getId().toString()) - .defaultPath(storageProfileWrapper.getStsEndpoint() != null ? storageProfileWrapper.getBucketPrefix() + vaultModel.vaultId() : vaultModel.bucketName()) - .nickname(vaultModel.vaultName()) - .username(vaultModel.accessKeyId()) - .password(vaultModel.secretKey()); - final VaultMetadataJWEAutomaticAccessGrantDto accessGrantDto = new VaultMetadataJWEAutomaticAccessGrantDto() - .enabled(vaultModel.automaticAccessGrant()) - .maxWotDepth(vaultModel.maxWotLevel()); - final UvfMetadataPayload metadataJWE = UvfMetadataPayload.create() - .withStorage(backendDto) - .withAutomaticAccessGrant(accessGrantDto); - log.debug("Created metadata JWE {}", metadataJWE); - final String uvfMetadataFile = metadataJWE.encrypt( + final UvfMetadataPayload metadataPayload = UvfMetadataPayload.create() + .withStorage(new VaultMetadataJWEBackendDto() + .provider(storageProfileWrapper.getId().toString()) + .defaultPath(storageProfileWrapper.getStsEndpoint() != null ? storageProfileWrapper.getBucketPrefix() + vaultModel.vaultId() : vaultModel.bucketName()) + .nickname(vaultModel.vaultName()) + .username(vaultModel.accessKeyId()) + .password(vaultModel.secretKey())) + .withAutomaticAccessGrant(new VaultMetadataJWEAutomaticAccessGrantDto() + .enabled(vaultModel.automaticAccessGrant()) + .maxWotDepth(vaultModel.maxWotLevel()) + ); + log.debug("Created metadata JWE {}", metadataPayload); + final String uvfMetadataFile = metadataPayload.encrypt( String.format("%s/api", new HostUrlProvider(false, true).get(hubSession.getHost())), vaultModel.vaultId(), jwks.toJWKSet() ); final VaultDto vaultDto = new VaultDto() .id(vaultModel.vaultId()) - .name(metadataJWE.storage().getNickname()) + .name(metadataPayload.storage().getNickname()) .description(vaultModel.vaultDescription()) .archived(false) .creationTime(DateTime.now()) .uvfMetadataFile(uvfMetadataFile) .uvfKeySet(jwks.serializePublicRecoverykey()); + + final String hashedRootDirId = metadataPayload.computeRootDirIdHash(); final CreateS3STSBucketDto storageDto = new CreateS3STSBucketDto() .vaultId(vaultModel.vaultId().toString()) .storageConfigId(storageProfileWrapper.getId()) - // TODO https://github.com/shift7-ch/cipherduck-hub/issues/19 do we need to store here as well? Only in VaultDto? .vaultUvf(uvfMetadataFile) - // TODO https://github.com/shift7-ch/cipherduck-hub/issues/19 do we need to store here? - .rootDirHash(metadataJWE.computeRootDirIdHash(metadataJWE.computeRootDirId())) - .region(metadataJWE.storage().getRegion()); + .rootDirHash(hashedRootDirId) + .region(metadataPayload.storage().getRegion()); log.debug("Created storage dto {}", storageDto); - final Host bookmark = HubStorageVaultSyncSchedulerService.toBookmark(hubSession.getHost(), vaultDto.getId(), metadataJWE.storage()); + final Host bookmark = HubStorageVaultSyncSchedulerService.toBookmark(hubSession.getHost(), vaultDto.getId(), metadataPayload.storage()); if(storageProfileWrapper.getStsEndpoint() == null) { - // permanent: template upload into existing bucket + // permanent: template upload into existing bucket from client (not backend) // TODO https://github.com/shift7-ch/cipherduck-hub/issues/19 review @dko final S3Session session = new S3Session(bookmark); session.open(new DisabledProxyFinder(), new DisabledHostKeyCallback(), new DisabledLoginCallback(), new DisabledCancelCallback()); session.login(new DisabledLoginCallback(), new DisabledCancelCallback()); + // upload vault template - new HubCryptoVault(new Path(metadataJWE.storage().getDefaultPath(), EnumSet.of(AbstractPath.Type.directory, AbstractPath.Type.vault))) - .create(session, metadataJWE.storage().getRegion(), null, 42, storageDto.getVaultUvf(), storageDto.getRootDirHash()); + new HubCryptoVault(new Path(metadataPayload.storage().getDefaultPath(), EnumSet.of(AbstractPath.Type.directory, AbstractPath.Type.vault))) + .create(session, metadataPayload.storage().getRegion(), null, 42, storageDto.getVaultUvf(), hashedRootDirId); session.close(); } else { @@ -149,6 +149,7 @@ public void createVault(final UserKeys userKeys, final StorageProfileDtoWrapper } } + private static TemporaryAccessTokens getSTSTokensFromAccessTokenWithCreateBucketInlinePoliy(final String token, final String roleArn, final String roleSessionName, final String stsEndpoint, final String bucketName, final Boolean bucketAcceleration) throws IOException { log.debug("Get STS tokens from {} to pass to backend {} with role {} and session name {}", token, stsEndpoint, roleArn, roleSessionName); diff --git a/hub/src/test/java/ch/iterate/hub/core/AbstractHubSynchronizeTest.java b/hub/src/test/java/ch/iterate/hub/core/AbstractHubSynchronizeTest.java index eb392f94..31c45076 100644 --- a/hub/src/test/java/ch/iterate/hub/core/AbstractHubSynchronizeTest.java +++ b/hub/src/test/java/ch/iterate/hub/core/AbstractHubSynchronizeTest.java @@ -4,38 +4,49 @@ package ch.iterate.hub.core; -import ch.cyberduck.core.AbstractPath; -import ch.cyberduck.core.AttributedList; -import ch.cyberduck.core.DisabledCancelCallback; -import ch.cyberduck.core.DisabledHostKeyCallback; -import ch.cyberduck.core.DisabledListProgressListener; -import ch.cyberduck.core.DisabledLoginCallback; -import ch.cyberduck.core.DisabledPasswordCallback; -import ch.cyberduck.core.Host; -import ch.cyberduck.core.ListService; -import ch.cyberduck.core.Path; -import ch.cyberduck.core.Protocol; -import ch.cyberduck.core.ProtocolFactory; -import ch.cyberduck.core.Session; -import ch.cyberduck.core.SimplePathPredicate; +import ch.cyberduck.core.*; +import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.features.Bulk; +import ch.cyberduck.core.features.Delete; +import ch.cyberduck.core.features.Directory; import ch.cyberduck.core.features.Home; +import ch.cyberduck.core.features.Move; +import ch.cyberduck.core.features.Read; import ch.cyberduck.core.features.Vault; +import ch.cyberduck.core.features.Write; +import ch.cyberduck.core.io.StatusOutputStream; import ch.cyberduck.core.preferences.PreferencesFactory; import ch.cyberduck.core.proxy.DisabledProxyFinder; import ch.cyberduck.core.ssl.DefaultX509KeyManager; import ch.cyberduck.core.ssl.DisabledX509TrustManager; +import ch.cyberduck.core.transfer.Transfer; +import ch.cyberduck.core.transfer.TransferItem; +import ch.cyberduck.core.transfer.TransferStatus; import ch.cyberduck.core.vault.DefaultVaultRegistry; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.RandomUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.openapitools.jackson.nullable.JsonNullableModule; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.UUID; +import static ch.iterate.hub.testsetup.HubTestUtilities.getAdminApiClient; +import static org.junit.jupiter.api.Assertions.*; + import ch.iterate.hub.client.ApiClient; import ch.iterate.hub.client.ApiException; import ch.iterate.hub.client.api.StorageProfileResourceApi; @@ -61,12 +72,20 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; -import static ch.iterate.hub.testsetup.HubTestUtilities.getAdminApiClient; -import static org.junit.jupiter.api.Assertions.*; - public abstract class AbstractHubSynchronizeTest extends AbstractHubTest { private static final Logger log = LogManager.getLogger(AbstractHubSynchronizeTest.class.getName()); + /** + * Start with unattended setup (e.g. UnattendedMinio) and then run tests with corresponding attended setup (e.g. AttendedMinio) to save startup times at every test execution. + */ + @Test + @Disabled + public void startUnattendedSetupToUseAttended() throws InterruptedException { + log.info("Unattended setup ready to be used in attended test runs."); + // run forever + Thread.sleep(924982347); + } + /** * Verify storage profiles are synced from hub bookmark. */ @@ -245,28 +264,138 @@ public void test03AddVault(final HubTestConfig config) throws Exception { session.open(new DisabledProxyFinder(), new DisabledHostKeyCallback(), new DisabledLoginCallback(), new DisabledCancelCallback()); session.login(new DisabledLoginCallback(), new DisabledCancelCallback()); + // listing decrypted file names assertFalse(vaultRegistry.isEmpty()); assertEquals(1, vaultRegistry.size()); - final Path bucket = new Path(vaultBookmark.getDefaultPath(), EnumSet.of(Path.Type.directory, Path.Type.volume)); + + final Path bucket = new Path(vaultBookmark.getDefaultPath(), EnumSet.of(Path.Type.directory, Path.Type.volume, Path.Type.vault)); assertNotSame(Vault.DISABLED, vaultRegistry.find(session, bucket)); -// { -// final AttributedList list = session.getFeature(ListService.class).list(bucket, new DisabledListProgressListener()); -// assertTrue(list.isEmpty()); -// } + { + // encrypted file listing + final AttributedList list = session.getFeature(ListService.class).list(bucket, new DisabledListProgressListener()); + assertTrue(list.isEmpty()); + } + { + // encrypted file upload + final Path home = vaultRegistry.find(session, bucket).getHome(); + final Path file = new Path(home, new AlphanumericRandomStringService(25).random(), EnumSet.of(AbstractPath.Type.file)); + byte[] content = writeRandomFile(session, file, 234); + final AttributedList list = session.getFeature(ListService.class).list(bucket, new DisabledListProgressListener()); + assertEquals(1, list.size()); + assertEquals(file.getName(), list.get(0).getName()); - vaultRegistry.close(bucket); - assertTrue(vaultRegistry.isEmpty()); + byte[] actual = new byte[300]; + try (final InputStream inputStream = session.getFeature(Read.class).read(file, new TransferStatus(), new DisabledConnectionCallback())) { + int l = inputStream.read(actual); + assert l == 234; + assertArrayEquals(content, Arrays.copyOfRange(actual, 0, l)); + } + } { + // encrypted directory creation and listing + final Path home = vaultRegistry.find(session, bucket).getHome(); + final Path folder = new Path(home, new AlphanumericRandomStringService(25).random(), EnumSet.of(AbstractPath.Type.directory)); + + session.getFeature(Directory.class).mkdir(folder, new TransferStatus()); final AttributedList list = session.getFeature(ListService.class).list(bucket, new DisabledListProgressListener()); - assertFalse(list.isEmpty()); - assertEquals(2, list.size()); - assertNotNull(list.find(new SimplePathPredicate(new Path(bucket, "d", EnumSet.of(Path.Type.directory, AbstractPath.Type.placeholder))))); - assertNotNull(list.find(new SimplePathPredicate(new Path(bucket, PreferencesFactory.get().getProperty("cryptomator.vault.config.filename"), EnumSet.of(Path.Type.file))))); + assertEquals(2, list.size()); // a file and a folder + + { + // encrypted file upload in subfolder + final Path file = new Path(folder, new AlphanumericRandomStringService(25).random(), EnumSet.of(AbstractPath.Type.file)); + final byte[] content = writeRandomFile(session, file, 555); + final AttributedList sublist = session.getFeature(ListService.class).list(folder, new DisabledListProgressListener()); + assertEquals(1, sublist.size()); + assertEquals(file.getName(), sublist.get(0).getName()); + + byte[] actual = new byte[600]; + try (final InputStream inputStream = session.getFeature(Read.class).read(file, new TransferStatus(), new DisabledConnectionCallback())) { + int l = inputStream.read(actual); + assert l == 555; + assertArrayEquals(content, Arrays.copyOfRange(actual, 0, l)); + } + + // move operation to root folder and read again + session.getFeature(Move.class).move(file, new Path(home, file.getName(), EnumSet.of(AbstractPath.Type.file)), new TransferStatus(), new Delete.DisabledCallback(), new DisabledConnectionCallback()); + + final AttributedList list2 = session.getFeature(ListService.class).list(home, new DisabledListProgressListener()); + assertEquals(3, list2.size()); // 1 subfolder and 2 files + + assertEquals(1, list2.toStream().map(Path::isDirectory).filter(Boolean::booleanValue).count()); + assertEquals(2, list2.toStream().map(Path::isFile).filter(Boolean::booleanValue).count()); + } + } + { + // raw listing encrypted file names + // aka. ciphertext directory structure + // see https://github.com/encryption-alliance/unified-vault-format/blob/develop/file%20name%20encryption/AES-SIV-512-B64URL.md#ciphertext-directory-structure + vaultRegistry.close(bucket); + assertSame(Vault.DISABLED, vaultRegistry.find(session, bucket)); + assertTrue(vaultRegistry.isEmpty()); + + { + final AttributedList list = session.getFeature(ListService.class).list(bucket, new DisabledListProgressListener()); + assertFalse(list.isEmpty()); + assertEquals(2, list.size()); + // //d/ + assertNotNull(list.find(new SimplePathPredicate(new Path(bucket, "d", EnumSet.of(Path.Type.directory, AbstractPath.Type.placeholder))))); + // //vault.uvf + assertNotNull(list.find(new SimplePathPredicate(new Path(bucket, PreferencesFactory.get().getProperty("cryptomator.vault.config.filename"), EnumSet.of(Path.Type.file))))); + } + { + // level 2: //d/ + final AttributedList level2List = session.getFeature(ListService.class).list(new Path(bucket, "d", EnumSet.of(Path.Type.directory, AbstractPath.Type.placeholder)), new DisabledListProgressListener()); + assertFalse(level2List.isEmpty()); + assertEquals(2, level2List.size()); + for(final Path level3 : level2List) { + // level 3: //d/<2-letter-folder>/ + final AttributedList level3List = session.getFeature(ListService.class).list(level3, new DisabledListProgressListener()); + // by hashing, only 1 sub-folder expected + assertEquals(1, level3List.size()); + for(final Path level4 : level3List) { + // level 4: //d/<2-letter-folder>/<30-letter-folder/ + final AttributedList level4List = session.getFeature(ListService.class).list(level4, new DisabledListProgressListener()); + assertTrue(level4List.toStream().map(Path::getName).allMatch(n -> n.endsWith(".uvf"))); + // empty sub-folder + log.info("level4List.size()={}", level4List.size()); + assert (level4List.size() >= 2); + // root folder contains two files and a sub-folder + assertTrue(level4List.size() <= 3); + if(level4List.size() == 2) { + // MiniO versioned API returns a first version with the file content and a second empty version upon deletion + assertTrue(level4List.toStream().allMatch(p -> p.attributes().isDuplicate())); + } + else if(level4List.size() == 3) { + // the root directory -> contains two files... + assertEquals(2, level4List.toStream().map(p -> p.isFile() && p.getName().endsWith(".uvf")).filter(Boolean::booleanValue).count()); + assertEquals(1, level4List.toStream().map(p -> p.isDirectory() && p.getName().endsWith(".uvf")).filter(Boolean::booleanValue).count()); + // ... and a subfolder with a dir.uvf in it + final Path level5 = level4List.toStream().filter(Path::isDirectory).findFirst().get(); + final AttributedList level5list = session.getFeature(ListService.class).list(level5, new DisabledListProgressListener()); + assertEquals(1, level5list.size()); + final Path level6 = level5list.get(0); + assertEquals("dir.uvf", level6.getName()); + assertTrue(level6.isFile()); + } + } + } + } } } finally { hubSession.close(); } } + + private static byte @NotNull [] writeRandomFile(final Session session, final Path file, int size) throws BackgroundException, IOException { + final byte[] content = RandomUtils.nextBytes(size); + final TransferStatus transferStatus = new TransferStatus().withLength(content.length); + transferStatus.setChecksum(session.getFeature(Write.class).checksum(file, transferStatus).compute(new ByteArrayInputStream(content), transferStatus)); + session.getFeature(Bulk.class).pre(Transfer.Type.upload, Collections.singletonMap(new TransferItem(file), transferStatus), new DisabledConnectionCallback()); + final StatusOutputStream out = session.getFeature(Write.class).write(file, transferStatus, new DisabledConnectionCallback()); + IOUtils.copyLarge(new ByteArrayInputStream(content), out); + out.close(); + return content; + } } diff --git a/hub/src/test/java/ch/iterate/hub/core/HubSynchronizeTest.java b/hub/src/test/java/ch/iterate/hub/core/HubSynchronizeTest.java index f9b645d5..25c32536 100644 --- a/hub/src/test/java/ch/iterate/hub/core/HubSynchronizeTest.java +++ b/hub/src/test/java/ch/iterate/hub/core/HubSynchronizeTest.java @@ -12,10 +12,10 @@ import java.util.stream.Stream; -import ch.iterate.hub.testsetup.HubTestSetupDockerExtension; - import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; +import ch.iterate.hub.testsetup.HubTestSetupDockerExtension; + /** * Test synchronization of profiles, adding profiles and adding vaults. * Same local context (profiles, hub host collection) shared across storage profile and tests. @@ -32,22 +32,21 @@ private Stream arguments() { } @Nested - @ExtendWith({HubTestSetupDockerExtension.UnattendedLocalKeycloakDev.class}) @TestInstance(PER_CLASS) - @Disabled("TODO https://github.com/shift7-ch/cipherduck-hub/issues/12 implemented unattended keycloak dev with aws in ci, dedicated keycloak?") - public class UnattendedLocalKeycloakDevOnlyStatic extends AbstractHubSynchronizeTest { + @Disabled("run standalone against already running hub started by runForever test for unattended configuration.") + public class AttendedMinio extends AbstractHubSynchronizeTest { private Stream arguments() { - return Stream.of(); + return Stream.of(minioStaticUnattendedLocalOnly, minioSTSUnattendedLocalOnly); } } - @Nested + @ExtendWith({HubTestSetupDockerExtension.UnattendedLocalKeycloakDev.class}) @TestInstance(PER_CLASS) - @Disabled("run standalone against already running hub") - public class AttendedMinio extends AbstractHubSynchronizeTest { + @Disabled("TODO https://github.com/shift7-ch/cipherduck-hub/issues/12 implemented unattended keycloak dev with aws in ci, dedicated keycloak?") + public class UnattendedLocalKeycloakDevOnlyStatic extends AbstractHubSynchronizeTest { private Stream arguments() { - return Stream.of(minioStaticAttendedLocalOnly, minioSTSAttendedLocalOnly); + return Stream.of(); } } diff --git a/hub/src/test/java/ch/iterate/hub/crypto/uvf/UvfMetadataPayloadTest.java b/hub/src/test/java/ch/iterate/hub/crypto/uvf/UvfMetadataPayloadTest.java index 81437b22..1a45dad6 100644 --- a/hub/src/test/java/ch/iterate/hub/crypto/uvf/UvfMetadataPayloadTest.java +++ b/hub/src/test/java/ch/iterate/hub/crypto/uvf/UvfMetadataPayloadTest.java @@ -4,21 +4,26 @@ package ch.iterate.hub.crypto.uvf; +import ch.cyberduck.core.AbstractPath; +import ch.cyberduck.core.AlphanumericRandomStringService; +import ch.cyberduck.core.Path; +import ch.cyberduck.core.cryptomator.UVFVault; import ch.cyberduck.core.cryptomator.random.FastSecureRandomProvider; +import ch.cyberduck.core.exception.BackgroundException; -import org.cryptomator.cryptolib.common.ECKeyPair; -import org.cryptomator.cryptolib.common.P384KeyPair; +import org.cryptomator.cryptolib.api.UVFMasterkey; import org.junit.jupiter.api.Test; import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; import java.text.ParseException; +import java.util.Arrays; import java.util.Base64; +import java.util.EnumSet; import java.util.HashMap; import java.util.UUID; -import ch.iterate.hub.crypto.UserKeys; import ch.iterate.hub.crypto.exceptions.NotECKeyException; import com.fasterxml.jackson.core.JsonProcessingException; import com.nimbusds.jose.JOSEException; @@ -28,7 +33,6 @@ import com.nimbusds.jose.jwk.OctetSequenceKey; import com.nimbusds.jose.util.Base64URL; -import static ch.iterate.hub.crypto.KeyHelper.decodeKeyPair; import static ch.iterate.hub.crypto.KeyHelper.decodePrivateKey; import static org.junit.jupiter.api.Assertions.*; @@ -121,6 +125,7 @@ public void encryptDecrypt() throws JOSEException, JsonProcessingException, Pars final ECKey fake = new ECKey.Builder(recoveryKey).keyID("kiddo").build(); assertThrows(JOSEException.class, () -> UvfMetadataPayload.decryptWithJWK(encrypted, fake)); } + assertTrue(orig.toString().startsWith("UvfMetadataPayload{fileFormat='AES-256-GCM-32k', nameFormat='AES-256-SIV', seeds={key02=********, key01=********}, initialSeed='key1', latestSeed='key0', kdf='1STEP-HMAC-SHA512', kdfSalt='********', automaticAccessGrant=class AutomaticAccessGrant {")); } @Test @@ -151,30 +156,26 @@ public void decryptWithRecoveryKey() throws ParseException, JOSEException, NoSuc } @Test - public void computeRootDirId() throws JOSEException, ParseException, JsonProcessingException, InvalidKeySpecException { - final ECKeyPair ecKeyPair = decodeKeyPair(Base64.getEncoder().encodeToString(alice.toECPublicKey().getEncoded()), (Base64.getEncoder().encodeToString(alice.toECPrivateKey().getEncoded()))); - final UserKeys userKeys = new UserKeys(ecKeyPair, P384KeyPair.generate()); - - final String uvf = "{\"protected\":\"eyJvcmlnaW4iOiJodHRwczovL2V4YW1wbGUuY29tL2FwaS92YXVsdHMvVE9ETy91dmYvdmF1bHQudXZmIiwiamt1Ijoiandrcy5qc29uIiwiZW5jIjoiQTI1NkdDTSJ9\",\"recipients\":[{\"header\":{\"kid\":\"org.cryptomator.hub.memberkey\",\"alg\":\"A256KW\"},\"encrypted_key\":\"7FtABJ5BpSM9Ft8wUPfLQc-12WF57kX0tWtRWiVwA_N_gJBa9iwhzw\"},{\"header\":{\"kid\":\"org.cryptomator.hub.recoverykey.1h24rxLxIlNRPQAn5NBP0fL3VKNTmqS6NEnt2clI5ko\",\"alg\":\"ECDH-ES+A256KW\",\"epk\":{\"key_ops\":[],\"ext\":true,\"kty\":\"EC\",\"x\":\"oNu46YFrgrGSvl98HyDD3_iPkfZBnpYgEHmPL3qbO4AdwBsycpIqcHwKhT8Lt7B8\",\"y\":\"P80VgJFml85v_F2-aPYdgDQX_DPGZr1s_p8gWF4Idkp13QfKdhi32C7Zoy5kzPWO\",\"crv\":\"P-384\"},\"apu\":\"\",\"apv\":\"\"},\"encrypted_key\":\"QaJn0TP7mGAc5ukOpZ0gNAuBtCW7hPCkj8Jp4bhMftQfJefHNyqE7Q\"}],\"iv\":\"Wgif0WP21-MAwvWs\",\"ciphertext\":\"-n5CePmmN99I4KqlnR64Fuu5b2Md9s4CGxLMm7KQqu65H0ug7Fs5HHnrx_gkpFiv1Mn-jwrkoEtiixyQcYX6UcoyT2dY1MkLQB7QU9mdMpZU3n19Q2sAx1-gfTCd7IzVXef7SEfuscdQL1QTKJW454Dy8L3WwPiDpUgt9ED7mMFdJ6lJ3_EFYstN0VFAVf_jwtIILmQrjkM_LI0FFKfqkOCH2nuE9xG8ihPH9X9OStllPp00G9_onYu9mrg-smiNNK2Ib19CZJ2E6mAp7F_LGiz6p203fsprj4XY9J6t8zl5Vpc61NmFvzvY4j3_5FpD_BmpVr8tyyVT9zqWn4vsBAHORQ1V_b9v68O7CekCebpQvpmzEPZwZN1Ma_T6oI7Ydn1rtBnDruVrpWm01RL8XpHnFbko\",\"tag\":\"vPLd65IEcexmhGbYPM0cYI53H4Pp1OfTaAq_QGrneLM\"}"; - final String accessToken = "eyJlbmMiOiJBMjU2R0NNIiwia2lkIjoib3JnLmNyeXB0b21hdG9yLmh1Yi51c2Vya2V5IiwiYWxnIjoiRUNESC1FUytBMjU2S1ciLCJlcGsiOnsia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwia3R5IjoiRUMiLCJ4IjoiUXZRWUpUd3dSVEg2MWRFS3ZoNDI4ZG9nN3pRTFFxY3I0NUhwZTRqZFQ5Qno2bjcyVzQ4dTJ3WXk0UXlyZ0kxciIsInkiOiJZS1RtQ04zZXNKNDJVbUpzLU44NTFKamsyUFVPUU0zZXpCTkJvZGk4RnRNUDlUeUhoXzc0aHpxTC1EYTZkMXlwIiwiY3J2IjoiUC0zODQifSwiYXB1IjoiIiwiYXB2IjoiIn0.rdysEEQN0FidglDtK5yyaEpQtv4CsYLOQd__y7REkb_3BLP9nD4Blw.dFb9JOdveiw3LmIs.rSMkz8VoB_LspnvxvmRzCWNVLShTWfbzHfqe5lwrWwumYCdeRPM.xsS2tDUr2khJrLxHex8gZhBgO_CMA_PxFlR-ku3JiT8"; + public void testWorkaround() { + // example of byte array -> UTF-8 -> byte array not working + final byte[] rootDirId = Base64.getDecoder().decode("L3CoPPdXaaDgrM5YhBujn2t2LFTE5XjYUzC1htzk6tY="); + assertFalse(Arrays.equals(rootDirId, new String(rootDirId, StandardCharsets.UTF_8).getBytes(StandardCharsets.UTF_8))); + // restricting to alphanumeric does work + final byte[] rootDirId2 = new AlphanumericRandomStringService(4).random().getBytes(StandardCharsets.UTF_8); + assertTrue(Arrays.equals(rootDirId2, new String(rootDirId2, StandardCharsets.UTF_8).getBytes(StandardCharsets.UTF_8))); + } - final UvfAccessTokenPayload accessTokenDecrypted = userKeys.decryptAccessToken(accessToken); - final UvfMetadataPayload meta = UvfMetadataPayload.decryptWithJWK(uvf, accessTokenDecrypted.memberKeyRecipient()); - assertEquals("24UBEDeGu5taq7U4GqyA0MXUXb9HTYS6p3t9vvHGJAc=", Base64.getEncoder().encodeToString(meta.computeRootDirId())); + @Test + public void testUVFMasterkeyFromUvfMetadataPayload() throws JsonProcessingException { + final UvfMetadataPayload uvmetadataPayload = UvfMetadataPayload.create(); + UVFMasterkey.fromDecryptedPayload(uvmetadataPayload.toJSON()); } @Test - public void computeRootDirIdHash() throws ParseException, JOSEException, JsonProcessingException, InvalidKeySpecException { - final ECKeyPair ecKeyPair = decodeKeyPair(Base64.getEncoder().encodeToString(alice.toECPublicKey().getEncoded()), (Base64.getEncoder().encodeToString(alice.toECPrivateKey().getEncoded()))); - final UserKeys userKeys = new UserKeys(ecKeyPair, P384KeyPair.generate()); - - final byte[] rootDirId = Base64.getDecoder().decode("24UBEDeGu5taq7U4GqyA0MXUXb9HTYS6p3t9vvHGJAc="); - final String uvf = "{\"protected\":\"eyJvcmlnaW4iOiJodHRwczovL2V4YW1wbGUuY29tL2FwaS92YXVsdHMvVE9ETy91dmYvdmF1bHQudXZmIiwiamt1Ijoiandrcy5qc29uIiwiZW5jIjoiQTI1NkdDTSJ9\",\"recipients\":[{\"header\":{\"kid\":\"org.cryptomator.hub.memberkey\",\"alg\":\"A256KW\"},\"encrypted_key\":\"7FtABJ5BpSM9Ft8wUPfLQc-12WF57kX0tWtRWiVwA_N_gJBa9iwhzw\"},{\"header\":{\"kid\":\"org.cryptomator.hub.recoverykey.1h24rxLxIlNRPQAn5NBP0fL3VKNTmqS6NEnt2clI5ko\",\"alg\":\"ECDH-ES+A256KW\",\"epk\":{\"key_ops\":[],\"ext\":true,\"kty\":\"EC\",\"x\":\"oNu46YFrgrGSvl98HyDD3_iPkfZBnpYgEHmPL3qbO4AdwBsycpIqcHwKhT8Lt7B8\",\"y\":\"P80VgJFml85v_F2-aPYdgDQX_DPGZr1s_p8gWF4Idkp13QfKdhi32C7Zoy5kzPWO\",\"crv\":\"P-384\"},\"apu\":\"\",\"apv\":\"\"},\"encrypted_key\":\"QaJn0TP7mGAc5ukOpZ0gNAuBtCW7hPCkj8Jp4bhMftQfJefHNyqE7Q\"}],\"iv\":\"Wgif0WP21-MAwvWs\",\"ciphertext\":\"-n5CePmmN99I4KqlnR64Fuu5b2Md9s4CGxLMm7KQqu65H0ug7Fs5HHnrx_gkpFiv1Mn-jwrkoEtiixyQcYX6UcoyT2dY1MkLQB7QU9mdMpZU3n19Q2sAx1-gfTCd7IzVXef7SEfuscdQL1QTKJW454Dy8L3WwPiDpUgt9ED7mMFdJ6lJ3_EFYstN0VFAVf_jwtIILmQrjkM_LI0FFKfqkOCH2nuE9xG8ihPH9X9OStllPp00G9_onYu9mrg-smiNNK2Ib19CZJ2E6mAp7F_LGiz6p203fsprj4XY9J6t8zl5Vpc61NmFvzvY4j3_5FpD_BmpVr8tyyVT9zqWn4vsBAHORQ1V_b9v68O7CekCebpQvpmzEPZwZN1Ma_T6oI7Ydn1rtBnDruVrpWm01RL8XpHnFbko\",\"tag\":\"vPLd65IEcexmhGbYPM0cYI53H4Pp1OfTaAq_QGrneLM\"}"; - final String accessToken = "eyJlbmMiOiJBMjU2R0NNIiwia2lkIjoib3JnLmNyeXB0b21hdG9yLmh1Yi51c2Vya2V5IiwiYWxnIjoiRUNESC1FUytBMjU2S1ciLCJlcGsiOnsia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwia3R5IjoiRUMiLCJ4IjoiUXZRWUpUd3dSVEg2MWRFS3ZoNDI4ZG9nN3pRTFFxY3I0NUhwZTRqZFQ5Qno2bjcyVzQ4dTJ3WXk0UXlyZ0kxciIsInkiOiJZS1RtQ04zZXNKNDJVbUpzLU44NTFKamsyUFVPUU0zZXpCTkJvZGk4RnRNUDlUeUhoXzc0aHpxTC1EYTZkMXlwIiwiY3J2IjoiUC0zODQifSwiYXB1IjoiIiwiYXB2IjoiIn0.rdysEEQN0FidglDtK5yyaEpQtv4CsYLOQd__y7REkb_3BLP9nD4Blw.dFb9JOdveiw3LmIs.rSMkz8VoB_LspnvxvmRzCWNVLShTWfbzHfqe5lwrWwumYCdeRPM.xsS2tDUr2khJrLxHex8gZhBgO_CMA_PxFlR-ku3JiT8"; - - final UvfAccessTokenPayload accessTokenDecrypted = userKeys.decryptAccessToken(accessToken); - final UvfMetadataPayload meta = UvfMetadataPayload.decryptWithJWK(uvf, accessTokenDecrypted.memberKeyRecipient()); - final String hash = meta.computeRootDirIdHash(rootDirId); - assertEquals("6DYU3E5BTPAZ4DWEQPQK3AIHX2DXSPHG", hash); + public void testUvfVaultLoadFromMetadataPayload() throws JsonProcessingException, BackgroundException { + final UvfMetadataPayload uvfMetadataPayload = UvfMetadataPayload.create(); + final String decryptedPayload = uvfMetadataPayload.toJSON(); + final UVFVault uvfVault = new UVFVault(new Path("/", EnumSet.of(AbstractPath.Type.directory)), decryptedPayload, null, null); + uvfVault.load(null, null); } } diff --git a/hub/src/test/java/ch/iterate/hub/testsetup/AbstractHubTest.java b/hub/src/test/java/ch/iterate/hub/testsetup/AbstractHubTest.java index 1c67ad3d..de98247c 100644 --- a/hub/src/test/java/ch/iterate/hub/testsetup/AbstractHubTest.java +++ b/hub/src/test/java/ch/iterate/hub/testsetup/AbstractHubTest.java @@ -53,6 +53,7 @@ public abstract class AbstractHubTest extends VaultTest { static { + // VaultTest is Junit 4 with @BeforeClass annotation, call statically in Jupiter setup. credentials(); } @@ -146,6 +147,7 @@ protected void configureLogging(final String level) { }); preferences.setLogging("debug"); preferences.setProperty("cryptomator.vault.config.filename", "vault.uvf"); + preferences.setProperty("cryptomator.vault.autodetect", "false"); preferences.setProperty("factory.vault.class", HubCryptoVault.class.getName()); preferences.setProperty("factory.supportdirectoryfinder.class", ch.cyberduck.core.preferences.TemporarySupportDirectoryFinder.class.getName()); preferences.setProperty("factory.passwordstore.class", UnsecureHostPasswordStore.class.getName()); diff --git a/hub/src/test/resources/docker-compose-minio-localhost-hub.yml b/hub/src/test/resources/docker-compose-minio-localhost-hub.yml index 7973aae3..b0073280 100644 --- a/hub/src/test/resources/docker-compose-minio-localhost-hub.yml +++ b/hub/src/test/resources/docker-compose-minio-localhost-hub.yml @@ -134,7 +134,7 @@ services: /usr/bin/mc idp openid info myminio # if container is restarted, the bucket already exists... - /usr/bin/mc mb myminio/handmade || true + /usr/bin/mc mb myminio/handmade --with-versioning || true /usr/bin/mc rm --recursive --force myminio/handmade echo "createbuckets successful" diff --git a/pom.xml b/pom.xml index 9acb9b27..94969eae 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ 5.12.0 1.12.0 - 9.1.3 + 9.1.3.uvfdraft-SNAPSHOT 1.20.6