Skip to content

Commit

Permalink
added primitives for file name encryption
Browse files Browse the repository at this point in the history
  • Loading branch information
overheadhunter committed Dec 6, 2024
1 parent 485a7bb commit 084e78a
Show file tree
Hide file tree
Showing 15 changed files with 291 additions and 85 deletions.
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

0 comments on commit 084e78a

Please sign in to comment.