Skip to content

add v3 impl compatible to UVF draft #51

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

Draft
wants to merge 35 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b07e43b
add v3 impl with UVF compatible file header and hardcoded key id
overheadhunter Feb 9, 2024
f35f9ac
Merge branch 'develop' into feature/uvf-draft
overheadhunter Nov 2, 2024
f47b27b
split Masterkey API into Perpetual + Revolving
overheadhunter Nov 29, 2024
f07ef0e
allow empty chunks, so UVF's EOF-chunks can be added
overheadhunter Nov 29, 2024
56dc34e
java 8 api sucks...
overheadhunter Nov 29, 2024
dd3ac84
fixed test after changing f07ef0e
overheadhunter Nov 29, 2024
485a7bb
fix javadoc
overheadhunter Dec 5, 2024
084e78a
added primitives for file name encryption
overheadhunter Dec 6, 2024
2445d1c
allow encrypting empty chunks
overheadhunter Jan 10, 2025
3924abc
Merge branch 'develop' into feature/uvf-draft
overheadhunter Jan 10, 2025
940857f
allow empty chunks (third attempt)
overheadhunter Jan 11, 2025
4db62e9
fix UVF file header
overheadhunter Jan 17, 2025
e8aeec4
use same test vectors as in typescript impl
overheadhunter Jan 17, 2025
47a26a2
fix build with Java 8
overheadhunter Jan 18, 2025
1170de4
Merge branch 'develop' into feature/uvf-draft
overheadhunter Jan 24, 2025
dcea94d
Introduce new `DirectoryContentCryptor` API
overheadhunter Jan 24, 2025
dcc1aa0
Merge branch 'develop' into feature/uvf-draft
overheadhunter Jan 24, 2025
361b3b0
typo
overheadhunter Mar 5, 2025
1e9bd32
UVF: use 64 bit keys for HMAC-SHA256
overheadhunter Mar 6, 2025
4fa5861
remove generic types
overheadhunter Mar 7, 2025
a431cf4
cleanup
overheadhunter Mar 7, 2025
688845d
API: allow file encryption w/ specific revision
overheadhunter Mar 7, 2025
8865144
API: add `Masterkey.rootDirId()`
overheadhunter Mar 7, 2025
d41b6e7
add convenience method `dirPath(dirUvfMetadata)`
overheadhunter Mar 7, 2025
d8c567b
add test to generate reference directory structure
overheadhunter Mar 7, 2025
f2745ea
fix missing `flush` before returning ciphertext
overheadhunter Mar 12, 2025
fd8ac29
Merge branch 'develop' into feature/uvf-draft
overheadhunter Mar 14, 2025
3c29fb6
implement `DirectoryContentCryptor` API for v1/v2
overheadhunter Mar 28, 2025
28dfcaa
Merge branch 'develop' into feature/uvf-draft
overheadhunter Mar 28, 2025
ad924b1
Merge branch 'develop' into feature/uvf-draft
overheadhunter Mar 28, 2025
030e3e4
use base64url in `vault.uvf` file
overheadhunter Apr 3, 2025
767b088
Merge branch 'develop' into feature/uvf-draft
overheadhunter Apr 3, 2025
577bf0e
deploy SNAPSHOTs when commit message contains
overheadhunter Jun 5, 2025
fdb58d0
fix workflow syntax
overheadhunter Jun 5, 2025
4b9ffa9
fix incorrectly merged fd8ac29
overheadhunter Jun 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
added primitives for file name encryption
  • Loading branch information
overheadhunter committed Dec 6, 2024
commit 084e78a213d7fd5f193d8f545cd7a366988ba996
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<!-- dependencies -->
<gson.version>2.11.0</gson.version>
<guava.version>33.2.1-jre</guava.version>
<siv-mode.version>1.5.2</siv-mode.version>
<siv-mode.version>1.6.0</siv-mode.version>
<bouncycastle.version>1.78.1</bouncycastle.version>
<slf4j.version>2.0.13</slf4j.version>

Expand Down
29 changes: 21 additions & 8 deletions src/main/java/org/cryptomator/cryptolib/api/Cryptor.java
Original file line number Diff line number Diff line change
@@ -1,23 +1,36 @@
/*******************************************************************************
* Copyright (c) 2016 Sebastian Stenzel and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE.txt.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.cryptolib.api;

import javax.security.auth.Destroyable;

public interface Cryptor extends Destroyable, AutoCloseable {

/**
* Encryption and decryption of file content.
* @return utility for encrypting and decrypting file content
*/
FileContentCryptor fileContentCryptor();

/**
* Encryption and decryption of file headers.
* @return utility for encrypting and decrypting file headers
*/
FileHeaderCryptor fileHeaderCryptor();

/**
* Encryption and decryption of file names in Cryptomator Vault Format.
* @return utility for encrypting and decrypting file names
* @apiNote Only relevant for Cryptomator Vault Format, for Universal Vault Format see {@link #fileNameCryptor(int)}
*/
FileNameCryptor fileNameCryptor();

/**
* Encryption and decryption of file names in Universal Vault Format.
* @param revision The revision of the seed to {@link RevolvingMasterkey#subKey(int, int, byte[], String) derive subkeys}.
* @return utility for encrypting and decrypting file names
* @apiNote Only relevant for Universal Vault Format, for Cryptomator Vault Format see {@link #fileNameCryptor()}
*/
FileNameCryptor fileNameCryptor(int revision);

@Override
void destroy();

Expand Down
16 changes: 15 additions & 1 deletion src/main/java/org/cryptomator/cryptolib/api/FileNameCryptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

import com.google.common.io.BaseEncoding;

import java.nio.charset.StandardCharsets;

/**
* Provides deterministic encryption capabilities as filenames must not change on subsequent encryption attempts,
* otherwise each change results in major directory structure changes which would be a terrible idea for cloud storage encryption.
Expand All @@ -18,11 +20,23 @@
*/
public interface FileNameCryptor {

/**
* @param cleartextDirectoryIdStr a UTF-8-encoded arbitrary directory id to be passed to one-way hash function
* @return constant length string, that is unlikely to collide with any other name.
* @apiNote Only relevant for Cryptomator Vault Format, not for Universal Vault Format
* @deprecated Use {@link #hashDirectoryId(byte[])} instead
*/
@Deprecated
default String hashDirectoryId(String cleartextDirectoryIdStr) {
return hashDirectoryId(cleartextDirectoryIdStr.getBytes(StandardCharsets.UTF_8));
}

/**
* @param cleartextDirectoryId an arbitrary directory id to be passed to one-way hash function
* @return constant length string, that is unlikely to collide with any other name.
* @apiNote Only relevant for Cryptomator Vault Format, not for Universal Vault Format
*/
String hashDirectoryId(String cleartextDirectoryId);
String hashDirectoryId(byte[] cleartextDirectoryId);

/**
* @param encoding Encoding to use to encode the returned ciphertext
Expand Down
10 changes: 8 additions & 2 deletions src/main/java/org/cryptomator/cryptolib/api/UVFMasterkey.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
import org.cryptomator.cryptolib.common.HKDFHelper;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
Expand All @@ -21,6 +21,8 @@
*/
public class UVFMasterkey implements RevolvingMasterkey {

private static final byte[] ROOT_DIRID_KDF_CONTEXT = "rootDirId".getBytes(StandardCharsets.US_ASCII);

@VisibleForTesting final Map<Integer, byte[]> seeds;
@VisibleForTesting final byte[] kdfSalt;
@VisibleForTesting final int initialSeed;
Expand Down Expand Up @@ -67,6 +69,10 @@ public int currentRevision() {
return latestSeed;
}

public byte[] rootDirId() {
return HKDFHelper.hkdfSha512(kdfSalt, seeds.get(initialSeed), ROOT_DIRID_KDF_CONTEXT, 32);
}

@Override
public DestroyableSecretKey subKey(int revision, int length, byte[] context, String algorithm) {
if (isDestroyed()) {
Expand All @@ -79,7 +85,7 @@ public DestroyableSecretKey subKey(int revision, int length, byte[] context, Str
try {
return new DestroyableSecretKey(subkey, algorithm);
} finally {
//Arrays.fill(subkey, (byte) 0x00);
Arrays.fill(subkey, (byte) 0x00);
}
}

Expand Down
14 changes: 6 additions & 8 deletions src/main/java/org/cryptomator/cryptolib/v1/CryptorImpl.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
/*******************************************************************************
* Copyright (c) 2016 Sebastian Stenzel and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE.txt.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.cryptolib.v1;

import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.FileNameCryptor;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.PerpetualMasterkey;

Expand Down Expand Up @@ -50,6 +43,11 @@ public FileNameCryptorImpl fileNameCryptor() {
return fileNameCryptor;
}

@Override
public FileNameCryptor fileNameCryptor(int revision) {
throw new UnsupportedOperationException();
}

@Override
public boolean isDestroyed() {
return masterkey.isDestroyed();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,11 @@ class FileNameCryptorImpl implements FileNameCryptor {
}

@Override
public String hashDirectoryId(String cleartextDirectoryId) {
public String hashDirectoryId(byte[] cleartextDirectoryId) {
try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey();
ObjectPool.Lease<MessageDigest> sha1 = MessageDigestSupplier.SHA1.instance();
ObjectPool.Lease<SivMode> siv = AES_SIV.get()) {
byte[] cleartextBytes = cleartextDirectoryId.getBytes(UTF_8);
byte[] encryptedBytes = siv.get().encrypt(ek, mk, cleartextBytes);
byte[] encryptedBytes = siv.get().encrypt(ek, mk, cleartextDirectoryId);
byte[] hashedBytes = sha1.get().digest(encryptedBytes);
return BASE32.encode(hashedBytes);
}
Expand Down
14 changes: 6 additions & 8 deletions src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
/*******************************************************************************
* Copyright (c) 2016 Sebastian Stenzel and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE.txt.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.cryptolib.v2;

import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.FileNameCryptor;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.PerpetualMasterkey;
import org.cryptomator.cryptolib.v1.CryptorProviderImpl;
Expand Down Expand Up @@ -51,6 +44,11 @@ public FileNameCryptorImpl fileNameCryptor() {
return fileNameCryptor;
}

@Override
public FileNameCryptor fileNameCryptor(int revision) {
throw new UnsupportedOperationException();
}

@Override
public boolean isDestroyed() {
return masterkey.isDestroyed();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,11 @@ class FileNameCryptorImpl implements FileNameCryptor {
}

@Override
public String hashDirectoryId(String cleartextDirectoryId) {
public String hashDirectoryId(byte[] cleartextDirectoryId) {
try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey();
ObjectPool.Lease<MessageDigest> sha1 = MessageDigestSupplier.SHA1.instance();
ObjectPool.Lease<SivMode> siv = AES_SIV.get()) {
byte[] cleartextBytes = cleartextDirectoryId.getBytes(UTF_8);
byte[] encryptedBytes = siv.get().encrypt(ek, mk, cleartextBytes);
byte[] encryptedBytes = siv.get().encrypt(ek, mk, cleartextDirectoryId);
byte[] hashedBytes = sha1.get().digest(encryptedBytes);
return BASE32.encode(hashedBytes);
}
Expand Down
10 changes: 7 additions & 3 deletions src/main/java/org/cryptomator/cryptolib/v3/CryptorImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
package org.cryptomator.cryptolib.v3;

import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.FileNameCryptor;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.RevolvingMasterkey;
import org.cryptomator.cryptolib.v1.CryptorProviderImpl;
Expand All @@ -20,7 +21,6 @@ class CryptorImpl implements Cryptor {
private final RevolvingMasterkey masterkey;
private final FileContentCryptorImpl fileContentCryptor;
private final FileHeaderCryptorImpl fileHeaderCryptor;
private final FileNameCryptorImpl fileNameCryptor;

/**
* Package-private constructor.
Expand All @@ -30,7 +30,6 @@ class CryptorImpl implements Cryptor {
this.masterkey = masterkey;
this.fileHeaderCryptor = new FileHeaderCryptorImpl(masterkey, random);
this.fileContentCryptor = new FileContentCryptorImpl(random);
this.fileNameCryptor = new FileNameCryptorImpl(masterkey);
}

@Override
Expand All @@ -47,8 +46,13 @@ public FileHeaderCryptorImpl fileHeaderCryptor() {

@Override
public FileNameCryptorImpl fileNameCryptor() {
throw new UnsupportedOperationException();
}

@Override
public FileNameCryptor fileNameCryptor(int revision) {
assertNotDestroyed();
return fileNameCryptor;
return new FileNameCryptorImpl(masterkey, revision);
}

@Override
Expand Down
52 changes: 23 additions & 29 deletions src/main/java/org/cryptomator/cryptolib/v3/FileNameCryptorImpl.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
/*******************************************************************************
* Copyright (c) 2015, 2016 Sebastian Stenzel and others.
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.cryptolib.v3;

import com.google.common.io.BaseEncoding;
Expand All @@ -14,14 +6,17 @@
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.RevolvingMasterkey;
import org.cryptomator.cryptolib.common.DestroyableSecretKey;
import org.cryptomator.cryptolib.common.MacSupplier;
import org.cryptomator.cryptolib.common.MessageDigestSupplier;
import org.cryptomator.cryptolib.common.ObjectPool;
import org.cryptomator.siv.SivMode;
import org.cryptomator.siv.UnauthenticCiphertextException;

import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Arrays;

import static java.nio.charset.StandardCharsets.UTF_8;

Expand All @@ -30,44 +25,43 @@ class FileNameCryptorImpl implements FileNameCryptor {
private static final BaseEncoding BASE32 = BaseEncoding.base32();
private static final ObjectPool<SivMode> AES_SIV = new ObjectPool<>(SivMode::new);

private final RevolvingMasterkey masterkey;
private final DestroyableSecretKey sivKey;
private final DestroyableSecretKey hmacKey;

FileNameCryptorImpl(RevolvingMasterkey masterkey) {
this.masterkey = masterkey;
}

private DestroyableSecretKey todo() {
return masterkey.subKey(0, 64, "TODO".getBytes(StandardCharsets.US_ASCII), "AES");
/**
* Create a file name encryption/decryption tool for a certain masterkey revision.
* @param masterkey The masterkey from which to derive subkeys
* @param revision Which masterkey revision to use
* @throws IllegalArgumentException If no subkey could be derived for the given revision
*/
FileNameCryptorImpl(RevolvingMasterkey masterkey, int revision) throws IllegalArgumentException {
this.sivKey = masterkey.subKey(revision, 64, "siv".getBytes(StandardCharsets.US_ASCII), "AES");
this.hmacKey = masterkey.subKey(revision, 32, "hmac".getBytes(StandardCharsets.US_ASCII), "HMAC");
}

@Override
public String hashDirectoryId(String cleartextDirectoryId) {
try (DestroyableSecretKey ek = todo(); DestroyableSecretKey mk = todo(); //FIXME
ObjectPool.Lease<MessageDigest> sha1 = MessageDigestSupplier.SHA1.instance();
ObjectPool.Lease<SivMode> siv = AES_SIV.get()) {
byte[] cleartextBytes = cleartextDirectoryId.getBytes(UTF_8);
byte[] encryptedBytes = siv.get().encrypt(ek, mk, cleartextBytes);
byte[] hashedBytes = sha1.get().digest(encryptedBytes);
return BASE32.encode(hashedBytes);
public String hashDirectoryId(byte[] cleartextDirectoryId) {
try (DestroyableSecretKey key = this.hmacKey.copy();
ObjectPool.Lease<Mac> hmacSha256 = MacSupplier.HMAC_SHA256.keyed(key)) {
byte[] hash = hmacSha256.get().doFinal(cleartextDirectoryId);
return BASE32.encode(hash, 0, 20); // only use first 160 bits
}
}

@Override
public String encryptFilename(BaseEncoding encoding, String cleartextName, byte[]... associatedData) {
try (DestroyableSecretKey ek = todo(); DestroyableSecretKey mk = todo(); //FIXME
ObjectPool.Lease<SivMode> siv = AES_SIV.get()) {
try (DestroyableSecretKey key = this.sivKey.copy(); ObjectPool.Lease<SivMode> siv = AES_SIV.get()) {
byte[] cleartextBytes = cleartextName.getBytes(UTF_8);
byte[] encryptedBytes = siv.get().encrypt(ek, mk, cleartextBytes, associatedData);
byte[] encryptedBytes = siv.get().encrypt(key, cleartextBytes, associatedData);
return encoding.encode(encryptedBytes);
}
}

@Override
public String decryptFilename(BaseEncoding encoding, String ciphertextName, byte[]... associatedData) throws AuthenticationFailedException {
try (DestroyableSecretKey ek = todo(); DestroyableSecretKey mk = todo(); //FIXME
ObjectPool.Lease<SivMode> siv = AES_SIV.get()) {
try (DestroyableSecretKey key = this.sivKey.copy(); ObjectPool.Lease<SivMode> siv = AES_SIV.get()) {
byte[] encryptedBytes = encoding.decode(ciphertextName);
byte[] cleartextBytes = siv.get().decrypt(ek, mk, encryptedBytes, associatedData);
byte[] cleartextBytes = siv.get().decrypt(key, encryptedBytes, associatedData);
return new String(cleartextBytes, UTF_8);
} catch (IllegalArgumentException | UnauthenticCiphertextException | IllegalBlockSizeException e) {
throw new AuthenticationFailedException("Invalid Ciphertext.", e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,13 @@ public void testSubkey() {
}
}

@Test
public void testRootDirId() {
Map<Integer, byte[]> seeds = Collections.singletonMap(-1540072521, Base64.getDecoder().decode("fP4V4oAjsUw5DqackAvLzA0oP1kAQZ0f5YFZQviXSuU="));
byte[] kdfSalt = Base64.getDecoder().decode("HE4OP+2vyfLLURicF1XmdIIsWv0Zs6MobLKROUIEhQY=");
try (UVFMasterkey masterkey = new UVFMasterkey(seeds, kdfSalt, -1540072521, -1540072521)) {
Assertions.assertEquals("24UBEDeGu5taq7U4GqyA0MXUXb9HTYS6p3t9vvHGJAc=", Base64.getEncoder().encodeToString(masterkey.rootDirId()));
}
}

}
11 changes: 10 additions & 1 deletion src/test/java/org/cryptomator/cryptolib/v1/CryptorImplTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
*******************************************************************************/
package org.cryptomator.cryptolib.v1;

import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.PerpetualMasterkey;
import org.cryptomator.cryptolib.common.SecureRandomMock;
import org.hamcrest.CoreMatchers;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mockito;

import java.security.SecureRandom;
Expand Down Expand Up @@ -52,6 +53,14 @@ public void testGetFileNameCryptor() {
}
}

@ParameterizedTest
@ValueSource(ints = {-1, 0, 1, 42, 1337})
public void testGetFileNameCryptorWithRevisions(int revision) {
try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) {
Assertions.assertThrows(UnsupportedOperationException.class, () -> cryptor.fileNameCryptor(revision));
}
}

@Test
public void testExplicitDestruction() {
PerpetualMasterkey masterkey = Mockito.mock(PerpetualMasterkey.class);
Expand Down
Loading
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy