Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,15 @@ enum Scheme {
* AES-SIV for file name encryption
* AES-GCM for content encryption
*/
SIV_GCM
SIV_GCM,

/**
* Experimental implementation of UVF draft
* @deprecated may be removed any time
* @see <a href="https://github.com/encryption-alliance/unified-vault-format">UVF</a>
*/
@Deprecated
UVF_DRAFT,
}

/**
Expand Down
28 changes: 28 additions & 0 deletions src/main/java/org/cryptomator/cryptolib/v3/Constants.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*******************************************************************************
* 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.v3;

import java.nio.charset.StandardCharsets;

final class Constants {

private Constants() {
}

static final String CONTENT_ENC_ALG = "AES";

static final byte[] UVF_MAGIC_BYTES = "UVF0".getBytes(StandardCharsets.US_ASCII);
static final byte[] KEY_ID = "KEY0".getBytes(StandardCharsets.US_ASCII);

static final int GCM_NONCE_SIZE = 12; // 96 bit IVs strongly recommended for GCM
static final int PAYLOAD_SIZE = 32 * 1024;
static final int GCM_TAG_SIZE = 16;
static final int CHUNK_SIZE = GCM_NONCE_SIZE + PAYLOAD_SIZE + GCM_TAG_SIZE;

}
74 changes: 74 additions & 0 deletions src/main/java/org/cryptomator/cryptolib/v3/CryptorImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*******************************************************************************
* 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.v3;

import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.v1.CryptorProviderImpl;

import java.security.SecureRandom;

class CryptorImpl implements Cryptor {

private final Masterkey masterkey;
private final FileContentCryptorImpl fileContentCryptor;
private final FileHeaderCryptorImpl fileHeaderCryptor;
private final FileNameCryptorImpl fileNameCryptor;

/**
* Package-private constructor.
* Use {@link CryptorProviderImpl#provide(Masterkey, SecureRandom)} to obtain a Cryptor instance.
*/
CryptorImpl(Masterkey masterkey, SecureRandom random) {
this.masterkey = masterkey;
this.fileHeaderCryptor = new FileHeaderCryptorImpl(masterkey, random);
this.fileContentCryptor = new FileContentCryptorImpl(random);
this.fileNameCryptor = new FileNameCryptorImpl(masterkey);
}

@Override
public FileContentCryptorImpl fileContentCryptor() {
assertNotDestroyed();
return fileContentCryptor;
}

@Override
public FileHeaderCryptorImpl fileHeaderCryptor() {
assertNotDestroyed();
return fileHeaderCryptor;
}

@Override
public FileNameCryptorImpl fileNameCryptor() {
assertNotDestroyed();
return fileNameCryptor;
}

@Override
public boolean isDestroyed() {
return masterkey.isDestroyed();
}

@Override
public void close() {
destroy();
}

@Override
public void destroy() {
masterkey.destroy();
}

private void assertNotDestroyed() {
if (isDestroyed()) {
throw new IllegalStateException("Cryptor destroyed.");
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*******************************************************************************
* 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.v3;

import org.cryptomator.cryptolib.api.CryptorProvider;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.common.ReseedingSecureRandom;

import java.security.SecureRandom;

public class CryptorProviderImpl implements CryptorProvider {

@Override
public Scheme scheme() {
return Scheme.UVF_DRAFT;
}

@Override
public CryptorImpl provide(Masterkey masterkey, SecureRandom random) {
return new CryptorImpl(masterkey, ReseedingSecureRandom.create(random));
}

}
158 changes: 158 additions & 0 deletions src/main/java/org/cryptomator/cryptolib/v3/FileContentCryptorImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*******************************************************************************
* 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.v3;

import org.cryptomator.cryptolib.api.AuthenticationFailedException;
import org.cryptomator.cryptolib.api.FileContentCryptor;
import org.cryptomator.cryptolib.api.FileHeader;
import org.cryptomator.cryptolib.common.CipherSupplier;
import org.cryptomator.cryptolib.common.DestroyableSecretKey;
import org.cryptomator.cryptolib.common.ObjectPool;

import javax.crypto.AEADBadTagException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.GCMParameterSpec;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.SecureRandom;

import static org.cryptomator.cryptolib.v3.Constants.CHUNK_SIZE;
import static org.cryptomator.cryptolib.v3.Constants.GCM_NONCE_SIZE;
import static org.cryptomator.cryptolib.v3.Constants.GCM_TAG_SIZE;
import static org.cryptomator.cryptolib.v3.Constants.PAYLOAD_SIZE;

class FileContentCryptorImpl implements FileContentCryptor {

private final SecureRandom random;

FileContentCryptorImpl(SecureRandom random) {
this.random = random;
}

@Override
public boolean canSkipAuthentication() {
return false;
}

@Override
public int cleartextChunkSize() {
return PAYLOAD_SIZE;
}

@Override
public int ciphertextChunkSize() {
return CHUNK_SIZE;
}

@Override
public ByteBuffer encryptChunk(ByteBuffer cleartextChunk, long chunkNumber, FileHeader header) {
ByteBuffer ciphertextChunk = ByteBuffer.allocate(CHUNK_SIZE);
encryptChunk(cleartextChunk, ciphertextChunk, chunkNumber, header);
ciphertextChunk.flip();
return ciphertextChunk;
}

@Override
public void encryptChunk(ByteBuffer cleartextChunk, ByteBuffer ciphertextChunk, long chunkNumber, FileHeader header) {
if (cleartextChunk.remaining() <= 0 || cleartextChunk.remaining() > PAYLOAD_SIZE) {
throw new IllegalArgumentException("Invalid cleartext chunk size: " + cleartextChunk.remaining() + ", expected range [1, " + PAYLOAD_SIZE + "]");
}
if (ciphertextChunk.remaining() < CHUNK_SIZE) {
throw new IllegalArgumentException("Invalid cipehrtext chunk size: " + ciphertextChunk.remaining() + ", must fit up to " + CHUNK_SIZE + " bytes.");
}
FileHeaderImpl headerImpl = FileHeaderImpl.cast(header);
encryptChunk(cleartextChunk, ciphertextChunk, chunkNumber, headerImpl.getNonce(), headerImpl.getContentKey());
}

@Override
public ByteBuffer decryptChunk(ByteBuffer ciphertextChunk, long chunkNumber, FileHeader header, boolean authenticate) throws AuthenticationFailedException {
// FileHeaderImpl.Payload.SIZE + GCM_TAG_SIZE is required to fix a bug in Android API level pre 29, see https://issuetracker.google.com/issues/197534888 and #35
ByteBuffer cleartextChunk = ByteBuffer.allocate(PAYLOAD_SIZE + GCM_TAG_SIZE);
decryptChunk(ciphertextChunk, cleartextChunk, chunkNumber, header, authenticate);
cleartextChunk.flip();
return cleartextChunk;
}

@Override
public void decryptChunk(ByteBuffer ciphertextChunk, ByteBuffer cleartextChunk, long chunkNumber, FileHeader header, boolean authenticate) throws AuthenticationFailedException {
if (ciphertextChunk.remaining() < GCM_NONCE_SIZE + GCM_TAG_SIZE || ciphertextChunk.remaining() > CHUNK_SIZE) {
throw new IllegalArgumentException("Invalid ciphertext chunk size: " + ciphertextChunk.remaining() + ", expected range [" + (GCM_NONCE_SIZE + GCM_TAG_SIZE) + ", " + CHUNK_SIZE + "]");
}
if (cleartextChunk.remaining() < PAYLOAD_SIZE) {
throw new IllegalArgumentException("Invalid cleartext chunk size: " + cleartextChunk.remaining() + ", must fit up to " + PAYLOAD_SIZE + " bytes.");
}
if (!authenticate) {
throw new UnsupportedOperationException("authenticate can not be false");
}
FileHeaderImpl headerImpl = FileHeaderImpl.cast(header);
decryptChunk(ciphertextChunk, cleartextChunk, chunkNumber, headerImpl.getNonce(), headerImpl.getContentKey());
}

// visible for testing
void encryptChunk(ByteBuffer cleartextChunk, ByteBuffer ciphertextChunk, long chunkNumber, byte[] headerNonce, DestroyableSecretKey fileKey) {
try (DestroyableSecretKey fk = fileKey.copy()) {
// nonce:
byte[] nonce = new byte[GCM_NONCE_SIZE];
random.nextBytes(nonce);

// payload:
try (ObjectPool.Lease<Cipher> cipher = CipherSupplier.AES_GCM.encryptionCipher(fk, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce))) {
final byte[] chunkNumberBigEndian = longToBigEndianByteArray(chunkNumber);
cipher.get().updateAAD(chunkNumberBigEndian);
cipher.get().updateAAD(headerNonce);
ciphertextChunk.put(nonce);
assert ciphertextChunk.remaining() >= cipher.get().getOutputSize(cleartextChunk.remaining());
cipher.get().doFinal(cleartextChunk, ciphertextChunk);
}
} catch (ShortBufferException e) {
throw new IllegalStateException("Buffer allocated for reported output size apparently not big enough.", e);
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new IllegalStateException("Unexpected exception during GCM encryption.", e);
}
}

// visible for testing
void decryptChunk(ByteBuffer ciphertextChunk, ByteBuffer cleartextChunk, long chunkNumber, byte[] headerNonce, DestroyableSecretKey fileKey) throws AuthenticationFailedException {
assert ciphertextChunk.remaining() >= GCM_NONCE_SIZE + GCM_TAG_SIZE;

try (DestroyableSecretKey fk = fileKey.copy()) {
// nonce:
final byte[] nonce = new byte[GCM_NONCE_SIZE];
ciphertextChunk.get(nonce, 0, GCM_NONCE_SIZE);

// payload:
final ByteBuffer payloadBuf = ciphertextChunk.duplicate();
payloadBuf.position(GCM_NONCE_SIZE);
assert payloadBuf.remaining() >= GCM_TAG_SIZE;

// payload:
try (ObjectPool.Lease<Cipher> cipher = CipherSupplier.AES_GCM.decryptionCipher(fk, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce))) {
final byte[] chunkNumberBigEndian = longToBigEndianByteArray(chunkNumber);
cipher.get().updateAAD(chunkNumberBigEndian);
cipher.get().updateAAD(headerNonce);
assert cleartextChunk.remaining() >= cipher.get().getOutputSize(payloadBuf.remaining());
cipher.get().doFinal(payloadBuf, cleartextChunk);
}
} catch (AEADBadTagException e) {
throw new AuthenticationFailedException("Content tag mismatch.", e);
} catch (ShortBufferException e) {
throw new IllegalStateException("Buffer allocated for reported output size apparently not big enough.", e);
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new IllegalStateException("Unexpected exception during GCM decryption.", e);
}
}

private byte[] longToBigEndianByteArray(long n) {
return ByteBuffer.allocate(Long.SIZE / Byte.SIZE).order(ByteOrder.BIG_ENDIAN).putLong(n).array();
}

}
Loading