cipher = CipherSupplier.RFC3394_KEYWRAP.keyUnwrapCipher(kekCopy)) {
51 | return DestroyableSecretKey.from(cipher.get().unwrap(wrappedKey, wrappedKeyAlgorithm, wrappedKeyType));
52 | } catch (NoSuchAlgorithmException e) {
53 | throw new IllegalArgumentException("Invalid algorithm: " + wrappedKeyAlgorithm, e);
54 | }
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/src/main/java/org/cryptomator/cryptolib/common/ByteBuffers.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2015, 2016 Sebastian Stenzel and others.
3 | * This file is licensed under the terms of the MIT license.
4 | * See the LICENSE.txt file for more info.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.common;
10 |
11 | import java.io.IOException;
12 | import java.nio.ByteBuffer;
13 | import java.nio.channels.ReadableByteChannel;
14 |
15 | public class ByteBuffers {
16 |
17 | private ByteBuffers() {
18 | }
19 |
20 | /**
21 | * Copies as many bytes as possible from the given source to the destination buffer.
22 | * The position of both buffers will be incremented by as many bytes as have been copied.
23 | *
24 | * @param source ByteBuffer from which bytes are read
25 | * @param destination ByteBuffer into which bytes are written
26 | * @return number of bytes copied, i.e. {@link ByteBuffer#remaining() source.remaining()} or {@link ByteBuffer#remaining() destination.remaining()}, whatever is less.
27 | */
28 | public static int copy(ByteBuffer source, ByteBuffer destination) {
29 | final int numBytes = Math.min(source.remaining(), destination.remaining());
30 | final ByteBuffer tmp = source.asReadOnlyBuffer();
31 | tmp.limit(tmp.position() + numBytes);
32 | destination.put(tmp);
33 | source.position(tmp.position()); // until now only tmp pos has been incremented, so we need to adjust the position
34 | return numBytes;
35 | }
36 |
37 | /**
38 | * Fills the given buffer
by reading from the given source until either reaching EOF
39 | * or buffer
has no more {@link ByteBuffer#hasRemaining() remaining space}.
40 | *
41 | * @param source The channel to read from
42 | * @param buffer The buffer to fill
43 | * @return Number of bytes read. Will only be less than remaining space in buffer
if reaching EOF.
44 | * @throws IOException In case of I/O errors
45 | */
46 | public static int fill(ReadableByteChannel source, ByteBuffer buffer) throws IOException {
47 | final int requested = buffer.remaining();
48 | while (buffer.hasRemaining()) {
49 | int read = source.read(buffer);
50 | if (read == -1) { // EOF
51 | break;
52 | }
53 | }
54 | return requested - buffer.remaining();
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/src/main/java/org/cryptomator/cryptolib/common/DecryptingReadableByteChannel.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.common;
10 |
11 | import org.cryptomator.cryptolib.api.AuthenticationFailedException;
12 | import org.cryptomator.cryptolib.api.Cryptor;
13 | import org.cryptomator.cryptolib.api.FileHeader;
14 |
15 | import java.io.EOFException;
16 | import java.io.IOException;
17 | import java.nio.ByteBuffer;
18 | import java.nio.channels.ReadableByteChannel;
19 |
20 | public class DecryptingReadableByteChannel implements ReadableByteChannel {
21 |
22 | private final ReadableByteChannel delegate;
23 | private final Cryptor cryptor;
24 | private final boolean authenticate;
25 | private ByteBuffer cleartextChunk;
26 | private FileHeader header;
27 | private boolean reachedEof;
28 | private long chunk;
29 |
30 | /**
31 | * Creates a DecryptingReadableByteChannel that decrypts a whole ciphertext file beginning at its first byte.
32 | *
33 | * @param src A ciphertext channel positioned at the begin of the file header
34 | * @param cryptor The cryptor to use
35 | * @param authenticate Set to false
to skip ciphertext authentication (may not be supported)
36 | */
37 | public DecryptingReadableByteChannel(ReadableByteChannel src, Cryptor cryptor, boolean authenticate) {
38 | this(src, cryptor, authenticate, null, 0);
39 | }
40 |
41 | /**
42 | * Creates a DecryptingReadableByteChannel with a previously read header, allowing to start decryption at any chunk.
43 | *
44 | * @param src A ciphertext channel positioned at the beginning of the given firstChunk
45 | * @param cryptor The cryptor to use
46 | * @param authenticate Set to false
to skip ciphertext authentication (may not be supported)
47 | * @param header The file's header
48 | * @param firstChunk The index of the chunk at which the src
channel is positioned
49 | */
50 | public DecryptingReadableByteChannel(ReadableByteChannel src, Cryptor cryptor, boolean authenticate, FileHeader header, long firstChunk) {
51 | this.delegate = src;
52 | this.cryptor = cryptor;
53 | this.authenticate = authenticate;
54 | this.cleartextChunk = ByteBuffer.allocate(0); // empty buffer will trigger loadNextCleartextChunk() on first access.
55 | this.header = header;
56 | this.reachedEof = false;
57 | this.chunk = firstChunk;
58 | }
59 |
60 | @Override
61 | public boolean isOpen() {
62 | return delegate.isOpen();
63 | }
64 |
65 | @Override
66 | public void close() throws IOException {
67 | delegate.close();
68 | }
69 |
70 | @Override
71 | public synchronized int read(ByteBuffer dst) throws IOException {
72 | try {
73 | loadHeaderIfNecessary();
74 | if (reachedEof) {
75 | return -1;
76 | } else {
77 | return readInternal(dst);
78 | }
79 | } catch (AuthenticationFailedException e) {
80 | throw new IOException("Unauthentic ciphertext", e);
81 | }
82 | }
83 |
84 | private int readInternal(ByteBuffer dst) throws IOException, AuthenticationFailedException {
85 | assert header != null : "header must be initialized";
86 |
87 | int result = 0;
88 | while (dst.hasRemaining() && !reachedEof) {
89 | if (cleartextChunk.hasRemaining() || loadNextCleartextChunk()) {
90 | result += ByteBuffers.copy(cleartextChunk, dst);
91 | } else {
92 | assert reachedEof : "no further cleartext available";
93 | }
94 | }
95 | return result;
96 | }
97 |
98 | private void loadHeaderIfNecessary() throws IOException, AuthenticationFailedException {
99 | if (header == null) {
100 | ByteBuffer headerBuf = ByteBuffer.allocate(cryptor.fileHeaderCryptor().headerSize());
101 | int read = ByteBuffers.fill(delegate, headerBuf);
102 | if (read != headerBuf.capacity()) {
103 | throw new EOFException("Unable to read header from channel.");
104 | }
105 | headerBuf.flip();
106 | header = cryptor.fileHeaderCryptor().decryptHeader(headerBuf);
107 | }
108 | }
109 |
110 | private boolean loadNextCleartextChunk() throws IOException, AuthenticationFailedException {
111 | ByteBuffer ciphertextChunk = ByteBuffer.allocate(cryptor.fileContentCryptor().ciphertextChunkSize());
112 | int read = ByteBuffers.fill(delegate, ciphertextChunk);
113 | if (read == 0) {
114 | reachedEof = true;
115 | return false;
116 | } else {
117 | ciphertextChunk.flip();
118 | cleartextChunk = cryptor.fileContentCryptor().decryptChunk(ciphertextChunk, chunk++, header, authenticate);
119 | return true;
120 | }
121 | }
122 |
123 | }
124 |
--------------------------------------------------------------------------------
/src/main/java/org/cryptomator/cryptolib/common/DestroyableSecretKey.java:
--------------------------------------------------------------------------------
1 | package org.cryptomator.cryptolib.common;
2 |
3 | import com.google.common.base.Preconditions;
4 |
5 | import javax.crypto.SecretKey;
6 | import javax.security.auth.Destroyable;
7 | import java.security.Key;
8 | import java.security.MessageDigest;
9 | import java.security.SecureRandom;
10 | import java.util.Arrays;
11 | import java.util.Objects;
12 |
13 | /**
14 | * A {@link SecretKey} that (other than JDK's SecretKeySpec)
15 | * actually implements {@link Destroyable}.
16 | *
17 | * Furthermore, this implementation will not create copies when accessing {@link #getEncoded()}.
18 | * Instead it implements {@link #copy} and {@link AutoCloseable} in an exception-free manner. To prevent mutation of the exposed key,
19 | * you would want to make sure to always work on scoped copies, such as in this example:
20 | *
21 | *
22 | * // copy "key" to protect it from unwanted modifications:
23 | * try (DestroyableSecretKey k = key.copy()) {
24 | * // use "k":
25 | * Cipher cipher = Cipher.init(k, ...)
26 | * cipher.doFinal(...)
27 | * } // "k" will get destroyed here
28 | *
29 | */
30 | public class DestroyableSecretKey implements SecretKey, AutoCloseable {
31 |
32 | private static final String KEY_DESTROYED_ERROR = "Key has been destroyed";
33 |
34 | private final transient byte[] key;
35 | private final String algorithm;
36 | private boolean destroyed;
37 |
38 | /**
39 | * Convenience constructor for {@link #DestroyableSecretKey(byte[], int, int, String)}
40 | *
41 | * @param key The raw key data (will get copied)
42 | * @param algorithm The {@link #getAlgorithm() algorithm name}
43 | */
44 | public DestroyableSecretKey(byte[] key, String algorithm) {
45 | this(key, 0, key.length, algorithm);
46 | }
47 |
48 | /**
49 | * Creates a new destroyable secret key, copying of the provided raw key bytes.
50 | *
51 | * @param key A byte[] holding the key material (relevant part will get copied)
52 | * @param offset The offset within key
where the key starts
53 | * @param len The number of bytes beginning at offset
to read from key
54 | * @param algorithm The {@link #getAlgorithm() algorithm name}
55 | */
56 | public DestroyableSecretKey(byte[] key, int offset, int len, String algorithm) {
57 | Preconditions.checkArgument(offset >= 0, "Invalid offset");
58 | Preconditions.checkArgument(len >= 0, "Invalid length");
59 | Preconditions.checkArgument(key.length >= offset + len, "Invalid offset/len");
60 | this.key = new byte[len];
61 | this.algorithm = Preconditions.checkNotNull(algorithm, "Algorithm must not be null");
62 | this.destroyed = false;
63 | System.arraycopy(key, offset, this.key, 0, len);
64 | }
65 |
66 | /**
67 | * Casts or converts a given {@link SecretKey} to a DestroyableSecretKey
68 | *
69 | * @param secretKey The secret key
70 | * @return Either the provided or a new key, depending on whether the provided key is already a DestroyableSecretKey
71 | */
72 | public static DestroyableSecretKey from(Key secretKey) {
73 | if (secretKey instanceof DestroyableSecretKey) {
74 | return (DestroyableSecretKey) secretKey;
75 | } else {
76 | return new DestroyableSecretKey(secretKey.getEncoded(), secretKey.getAlgorithm());
77 | }
78 | }
79 |
80 | /**
81 | * Creates a new key of given length and for use with given algorithm using entropy from the given csprng.
82 | *
83 | * @param csprng A cryptographically secure random number source
84 | * @param algorithm The {@link #getAlgorithm() key algorithm}
85 | * @param keyLenBytes The length of the key (in bytes)
86 | * @return A new secret key
87 | */
88 | public static DestroyableSecretKey generate(SecureRandom csprng, String algorithm, int keyLenBytes) {
89 | byte[] key = new byte[keyLenBytes];
90 | try {
91 | csprng.nextBytes(key);
92 | return new DestroyableSecretKey(key, algorithm);
93 | } finally {
94 | Arrays.fill(key, (byte) 0x00);
95 | }
96 | }
97 |
98 | @Override
99 | public String getAlgorithm() {
100 | Preconditions.checkState(!destroyed, KEY_DESTROYED_ERROR);
101 | return algorithm;
102 | }
103 |
104 | @Override
105 | public String getFormat() {
106 | Preconditions.checkState(!destroyed, KEY_DESTROYED_ERROR);
107 | return "RAW";
108 | }
109 |
110 | /**
111 | * Returns the raw key bytes this instance wraps.
112 | *
113 | * Important: Any change to the returned array will reflect in this key. Make sure to
114 | * {@link #copy() make a local copy} if you can't rule out mutations.
115 | *
116 | * @return A byte array holding the secret key
117 | */
118 | @Override
119 | public byte[] getEncoded() {
120 | Preconditions.checkState(!destroyed, KEY_DESTROYED_ERROR);
121 | return key;
122 | }
123 |
124 | /**
125 | * Returns an independent copy of this key
126 | * @return New copy of this
127 | */
128 | public DestroyableSecretKey copy() {
129 | Preconditions.checkState(!destroyed, KEY_DESTROYED_ERROR);
130 | return new DestroyableSecretKey(key, algorithm); // key will get copied by the constructor as per contract
131 | }
132 |
133 | @Override
134 | public void destroy() {
135 | Arrays.fill(key, (byte) 0x00);
136 | destroyed = true;
137 | }
138 |
139 | @Override
140 | public boolean isDestroyed() {
141 | return destroyed;
142 | }
143 |
144 | /**
145 | * Same as {@link #destroy()}
146 | */
147 | @Override
148 | public void close() {
149 | destroy();
150 | }
151 |
152 | @Override
153 | public boolean equals(Object o) {
154 | if (this == o) return true;
155 | if (o == null || getClass() != o.getClass()) return false;
156 | DestroyableSecretKey that = (DestroyableSecretKey) o;
157 | return algorithm.equals(that.algorithm) && MessageDigest.isEqual(this.key, that.key);
158 | }
159 |
160 | @Override
161 | public int hashCode() {
162 | int result = Objects.hash(algorithm);
163 | result = 31 * result + Arrays.hashCode(key);
164 | return result;
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/src/main/java/org/cryptomator/cryptolib/common/Destroyables.java:
--------------------------------------------------------------------------------
1 | package org.cryptomator.cryptolib.common;
2 |
3 | import javax.security.auth.DestroyFailedException;
4 | import javax.security.auth.Destroyable;
5 |
6 | public class Destroyables {
7 |
8 | private Destroyables() {
9 | }
10 |
11 | public static void destroySilently(Destroyable destroyable) {
12 | if (destroyable == null) {
13 | return;
14 | }
15 | try {
16 | destroyable.destroy();
17 | } catch (DestroyFailedException e) {
18 | // no-op
19 | }
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/java/org/cryptomator/cryptolib/common/ECKeyPair.java:
--------------------------------------------------------------------------------
1 | package org.cryptomator.cryptolib.common;
2 |
3 | import com.google.common.base.Preconditions;
4 |
5 | import javax.security.auth.Destroyable;
6 | import java.math.BigInteger;
7 | import java.security.KeyPair;
8 | import java.security.MessageDigest;
9 | import java.security.PublicKey;
10 | import java.security.interfaces.ECPrivateKey;
11 | import java.security.interfaces.ECPublicKey;
12 | import java.security.spec.ECFieldFp;
13 | import java.security.spec.ECParameterSpec;
14 | import java.security.spec.ECPoint;
15 | import java.security.spec.EllipticCurve;
16 | import java.util.Arrays;
17 | import java.util.Objects;
18 |
19 | public class ECKeyPair implements Destroyable {
20 |
21 | private static final String INVALID_KEY_ERROR = "Invalid EC Key";
22 |
23 | private final KeyPair keyPair;
24 | private boolean destroyed;
25 |
26 | ECKeyPair(KeyPair keyPair, ECParameterSpec curveParams) {
27 | Preconditions.checkArgument(keyPair.getPrivate() instanceof ECPrivateKey);
28 | Preconditions.checkArgument(keyPair.getPublic() instanceof ECPublicKey);
29 | this.keyPair = verify(keyPair, curveParams);
30 | }
31 |
32 | public KeyPair keyPair() {
33 | return keyPair;
34 | }
35 |
36 | public ECPrivateKey getPrivate() {
37 | Preconditions.checkState(!destroyed);
38 | assert keyPair.getPrivate() instanceof ECPrivateKey;
39 | return (ECPrivateKey) keyPair.getPrivate();
40 | }
41 |
42 | public ECPublicKey getPublic() {
43 | Preconditions.checkState(!destroyed);
44 | assert keyPair.getPublic() instanceof ECPublicKey;
45 | return (ECPublicKey) keyPair.getPublic();
46 | }
47 |
48 | // validations taken from https://neilmadden.blog/2017/05/17/so-how-do-you-validate-nist-ecdh-public-keys/
49 | private static KeyPair verify(KeyPair keyPair, ECParameterSpec curveParams) {
50 | PublicKey pk = keyPair.getPublic();
51 | Preconditions.checkArgument(pk instanceof ECPublicKey, INVALID_KEY_ERROR);
52 | Preconditions.checkArgument(curveParams.getCofactor() == 1, "Verifying points on curves with cofactor not supported"); // see "Step 4" in linked post
53 | ECPublicKey publicKey = (ECPublicKey) pk;
54 | EllipticCurve curve = curveParams.getCurve();
55 |
56 | // Step 1: Verify public key is not point at infinity.
57 | Preconditions.checkArgument(!ECPoint.POINT_INFINITY.equals(publicKey.getW()), INVALID_KEY_ERROR);
58 |
59 | final BigInteger x = publicKey.getW().getAffineX();
60 | final BigInteger y = publicKey.getW().getAffineY();
61 | final BigInteger p = ((ECFieldFp) curve.getField()).getP();
62 |
63 | // Step 2: Verify x and y are in range [0,p-1]
64 | Preconditions.checkArgument(x.compareTo(BigInteger.ZERO) >= 0 && x.compareTo(p) < 0, INVALID_KEY_ERROR);
65 | Preconditions.checkArgument(y.compareTo(BigInteger.ZERO) >= 0 && y.compareTo(p) < 0, INVALID_KEY_ERROR);
66 |
67 | // Step 3: Verify that y^2 == x^3 + ax + b (mod p)
68 | final BigInteger a = curve.getA();
69 | final BigInteger b = curve.getB();
70 | final BigInteger ySquared = y.modPow(BigInteger.valueOf(2), p);
71 | final BigInteger xCubedPlusAXPlusB = x.modPow(BigInteger.valueOf(3), p).add(a.multiply(x)).add(b).mod(p);
72 | Preconditions.checkArgument(ySquared.equals(xCubedPlusAXPlusB), INVALID_KEY_ERROR);
73 |
74 | return keyPair;
75 | }
76 |
77 | @Override
78 | public boolean isDestroyed() {
79 | return destroyed;
80 | }
81 |
82 | @Override
83 | public void destroy() {
84 | Destroyables.destroySilently(keyPair.getPrivate());
85 | destroyed = true;
86 | }
87 |
88 | @Override
89 | public boolean equals(Object o) {
90 | if (this == o) return true;
91 | if (o == null || getClass() != o.getClass()) return false;
92 | ECKeyPair that = (ECKeyPair) o;
93 | return MessageDigest.isEqual(this.getPublic().getEncoded(), that.getPublic().getEncoded());
94 | }
95 |
96 | @Override
97 | public int hashCode() {
98 | int result = Objects.hash(keyPair.getPublic().getAlgorithm());
99 | result = 31 * result + Arrays.hashCode(keyPair.getPublic().getEncoded());
100 | return result;
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/main/java/org/cryptomator/cryptolib/common/EncryptingReadableByteChannel.java:
--------------------------------------------------------------------------------
1 | package org.cryptomator.cryptolib.common;
2 |
3 | import org.cryptomator.cryptolib.api.Cryptor;
4 | import org.cryptomator.cryptolib.api.FileHeader;
5 |
6 | import java.io.IOException;
7 | import java.nio.ByteBuffer;
8 | import java.nio.channels.ReadableByteChannel;
9 |
10 | public class EncryptingReadableByteChannel implements ReadableByteChannel {
11 |
12 | private final ReadableByteChannel delegate;
13 | private final Cryptor cryptor;
14 | private final FileHeader header;
15 |
16 | private ByteBuffer ciphertextBuffer;
17 | private long chunk = 0;
18 | private boolean reachedEof;
19 |
20 | /**
21 | * Creates an EncryptingReadableByteChannel that encrypts a whole cleartext file beginning at its first byte.
22 | *
23 | * @param src A cleartext channel positioned at its begin
24 | * @param cryptor The cryptor to use
25 | */
26 | public EncryptingReadableByteChannel(ReadableByteChannel src, Cryptor cryptor) {
27 | this.delegate = src;
28 | this.cryptor = cryptor;
29 | this.header = cryptor.fileHeaderCryptor().create();
30 | this.ciphertextBuffer = cryptor.fileHeaderCryptor().encryptHeader(header);
31 | }
32 |
33 | @Override
34 | public boolean isOpen() {
35 | return delegate.isOpen();
36 | }
37 |
38 | @Override
39 | public void close() throws IOException {
40 | delegate.close();
41 | }
42 |
43 | @Override
44 | public synchronized int read(ByteBuffer dst) throws IOException {
45 | if (reachedEof) {
46 | return -1;
47 | } else {
48 | return readInternal(dst);
49 | }
50 | }
51 |
52 | private int readInternal(ByteBuffer dst) throws IOException {
53 | int result = 0;
54 | while (dst.hasRemaining() && !reachedEof) {
55 | if (ciphertextBuffer.hasRemaining() || loadNextCiphertextChunk()) {
56 | result += ByteBuffers.copy(ciphertextBuffer, dst);
57 | } else {
58 | assert reachedEof : "no further ciphertext available";
59 | }
60 | }
61 | return result;
62 | }
63 |
64 | private boolean loadNextCiphertextChunk() throws IOException {
65 | ByteBuffer cleartextChunk = ByteBuffer.allocate(cryptor.fileContentCryptor().cleartextChunkSize());
66 | int read = ByteBuffers.fill(delegate, cleartextChunk);
67 | if (read == 0) {
68 | reachedEof = true;
69 | return false;
70 | } else {
71 | cleartextChunk.flip();
72 | ciphertextBuffer = cryptor.fileContentCryptor().encryptChunk(cleartextChunk, chunk++, header);
73 | return true;
74 | }
75 | }
76 |
77 | }
78 |
--------------------------------------------------------------------------------
/src/main/java/org/cryptomator/cryptolib/common/EncryptingWritableByteChannel.java:
--------------------------------------------------------------------------------
1 | package org.cryptomator.cryptolib.common;
2 |
3 | import org.cryptomator.cryptolib.api.Cryptor;
4 | import org.cryptomator.cryptolib.api.FileHeader;
5 |
6 | import java.io.IOException;
7 | import java.nio.ByteBuffer;
8 | import java.nio.channels.WritableByteChannel;
9 |
10 | public class EncryptingWritableByteChannel implements WritableByteChannel {
11 |
12 | private final WritableByteChannel delegate;
13 | private final Cryptor cryptor;
14 | private final FileHeader header;
15 | private final ByteBuffer cleartextBuffer;
16 |
17 | private boolean firstWrite = true;
18 | private long chunkNumber = 0;
19 |
20 | public EncryptingWritableByteChannel(WritableByteChannel destination, Cryptor cryptor) {
21 | this.delegate = destination;
22 | this.cryptor = cryptor;
23 | this.header = cryptor.fileHeaderCryptor().create();
24 | this.cleartextBuffer = ByteBuffer.allocate(cryptor.fileContentCryptor().cleartextChunkSize());
25 | }
26 |
27 | @Override
28 | public boolean isOpen() {
29 | return delegate.isOpen();
30 | }
31 |
32 | @Override
33 | public synchronized void close() throws IOException {
34 | writeHeaderOnFirstWrite();
35 | encryptAndFlushBuffer();
36 | delegate.close();
37 | }
38 |
39 | @Override
40 | public synchronized int write(ByteBuffer src) throws IOException {
41 | writeHeaderOnFirstWrite();
42 | int result = 0;
43 | while (src.hasRemaining()) {
44 | result += ByteBuffers.copy(src, cleartextBuffer);
45 | if (!cleartextBuffer.hasRemaining()) {
46 | encryptAndFlushBuffer();
47 | }
48 | }
49 | return result;
50 | }
51 |
52 | private void writeHeaderOnFirstWrite() throws IOException {
53 | if (firstWrite) {
54 | delegate.write(cryptor.fileHeaderCryptor().encryptHeader(header));
55 | }
56 | firstWrite = false;
57 | }
58 |
59 | private void encryptAndFlushBuffer() throws IOException {
60 | cleartextBuffer.flip();
61 | ByteBuffer ciphertextBuffer = cryptor.fileContentCryptor().encryptChunk(cleartextBuffer, chunkNumber++, header);
62 | delegate.write(ciphertextBuffer);
63 | cleartextBuffer.clear();
64 | }
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/src/main/java/org/cryptomator/cryptolib/common/MacSupplier.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.common;
10 |
11 | import javax.crypto.Mac;
12 | import javax.crypto.SecretKey;
13 | import java.security.InvalidKeyException;
14 | import java.security.NoSuchAlgorithmException;
15 |
16 | public final class MacSupplier {
17 |
18 | public static final MacSupplier HMAC_SHA256 = new MacSupplier("HmacSHA256");
19 |
20 | private final String macAlgorithm;
21 | private final ObjectPool macPool;
22 |
23 | public MacSupplier(String macAlgorithm) {
24 | this.macAlgorithm = macAlgorithm;
25 | this.macPool = new ObjectPool<>(this::createMac);
26 | try (ObjectPool.Lease lease = macPool.get()) {
27 | lease.get(); // eagerly initialize to provoke exceptions
28 | }
29 | }
30 |
31 | private Mac createMac() {
32 | try {
33 | return Mac.getInstance(macAlgorithm);
34 | } catch (NoSuchAlgorithmException e) {
35 | throw new IllegalArgumentException("Invalid MAC algorithm.", e);
36 | }
37 | }
38 |
39 | /**
40 | * Leases a reusable MAC object initialized with the given key.
41 | *
42 | * @param key Key to use in keyed MAC
43 | * @return A lease supplying a refurbished MAC
44 | */
45 | public ObjectPool.Lease keyed(SecretKey key) {
46 | ObjectPool.Lease lease = macPool.get();
47 | init(lease.get(), key);
48 | return lease;
49 | }
50 |
51 | /**
52 | * Creates a new MAC
53 | *
54 | * @param key Key to use in keyed MAC
55 | * @return New Mac instance
56 | * @deprecated Use {@link #keyed(SecretKey)} instead
57 | */
58 | @Deprecated
59 | public Mac withKey(SecretKey key) {
60 | final Mac mac = createMac();
61 | init(mac, key);
62 | return mac;
63 | }
64 |
65 | private void init(Mac mac, SecretKey key) {
66 | try {
67 | mac.init(key);
68 | } catch (InvalidKeyException e) {
69 | throw new IllegalArgumentException("Invalid key.", e);
70 | }
71 | }
72 |
73 | }
--------------------------------------------------------------------------------
/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFile.java:
--------------------------------------------------------------------------------
1 | package org.cryptomator.cryptolib.common;
2 |
3 | import com.google.common.io.BaseEncoding;
4 | import com.google.gson.Gson;
5 | import com.google.gson.GsonBuilder;
6 | import com.google.gson.JsonIOException;
7 | import com.google.gson.JsonParseException;
8 | import com.google.gson.TypeAdapter;
9 | import com.google.gson.annotations.SerializedName;
10 | import com.google.gson.stream.JsonReader;
11 | import com.google.gson.stream.JsonToken;
12 | import com.google.gson.stream.JsonWriter;
13 |
14 | import java.io.IOException;
15 | import java.io.Reader;
16 | import java.io.Writer;
17 |
18 | /**
19 | * Representation of encrypted masterkey json file. Used by {@link MasterkeyFileAccess} to load and persist keys.
20 | */
21 | public class MasterkeyFile {
22 |
23 | private static final Gson GSON = new GsonBuilder() //
24 | .setPrettyPrinting() //
25 | .disableHtmlEscaping() //
26 | .registerTypeHierarchyAdapter(byte[].class, new MasterkeyFile.ByteArrayJsonAdapter()) //
27 | .create();
28 |
29 | @SerializedName("version")
30 | public int version;
31 |
32 | @SerializedName("scryptSalt")
33 | public byte[] scryptSalt;
34 |
35 | @SerializedName("scryptCostParam")
36 | public int scryptCostParam;
37 |
38 | @SerializedName("scryptBlockSize")
39 | public int scryptBlockSize;
40 |
41 | @SerializedName("primaryMasterKey")
42 | public byte[] encMasterKey;
43 |
44 | @SerializedName("hmacMasterKey")
45 | public byte[] macMasterKey;
46 |
47 | @SerializedName("versionMac")
48 | public byte[] versionMac;
49 |
50 | public static MasterkeyFile read(Reader reader) throws IOException {
51 | try {
52 | MasterkeyFile result = GSON.fromJson(reader, MasterkeyFile.class);
53 | if (result == null) {
54 | throw new IOException("JSON EOF");
55 | } else {
56 | return result;
57 | }
58 | } catch (JsonParseException e) {
59 | throw new IOException("Unreadable JSON", e);
60 | } catch (IllegalArgumentException e) {
61 | throw new IOException("Invalid JSON content", e);
62 | }
63 | }
64 |
65 | public void write(Writer writer) throws IOException {
66 | try {
67 | GSON.toJson(this, writer);
68 | } catch (JsonIOException e) {
69 | throw new IOException(e);
70 | }
71 | }
72 |
73 | boolean isValid() {
74 | return version != 0
75 | && scryptSalt != null
76 | && scryptCostParam > 1
77 | && scryptBlockSize > 0
78 | && encMasterKey != null
79 | && macMasterKey != null
80 | && versionMac != null;
81 | }
82 |
83 | private static class ByteArrayJsonAdapter extends TypeAdapter {
84 |
85 | private static final BaseEncoding BASE64 = BaseEncoding.base64();
86 |
87 | @Override
88 | public void write(JsonWriter writer, byte[] value) throws IOException {
89 | if (value == null) {
90 | writer.nullValue();
91 | } else {
92 | writer.value(BASE64.encode(value));
93 | }
94 | }
95 |
96 | @Override
97 | public byte[] read(JsonReader reader) throws IOException {
98 | if (reader.peek() == JsonToken.NULL) {
99 | reader.nextNull();
100 | return null;
101 | } else {
102 | return BASE64.decode(reader.nextString());
103 | }
104 | }
105 | }
106 |
107 | }
108 |
--------------------------------------------------------------------------------
/src/main/java/org/cryptomator/cryptolib/common/MessageDigestSupplier.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.common;
10 |
11 | import java.security.MessageDigest;
12 | import java.security.NoSuchAlgorithmException;
13 |
14 | public final class MessageDigestSupplier {
15 |
16 | public static final MessageDigestSupplier SHA1 = new MessageDigestSupplier("SHA-1");
17 | public static final MessageDigestSupplier SHA256 = new MessageDigestSupplier("SHA-256");
18 |
19 | private final String digestAlgorithm;
20 | private final ObjectPool mdPool;
21 |
22 | public MessageDigestSupplier(String digestAlgorithm) {
23 | this.digestAlgorithm = digestAlgorithm;
24 | this.mdPool = new ObjectPool<>(this::createMessageDigest);
25 | try (ObjectPool.Lease lease = mdPool.get()) {
26 | lease.get(); // eagerly initialize to provoke exceptions
27 | }
28 | }
29 |
30 | private MessageDigest createMessageDigest() {
31 | try {
32 | return MessageDigest.getInstance(digestAlgorithm);
33 | } catch (NoSuchAlgorithmException e) {
34 | throw new IllegalArgumentException("Invalid digest algorithm.", e);
35 | }
36 | }
37 |
38 | /**
39 | * Leases a reusable MessageDigest.
40 | *
41 | * @return A ReusableMessageDigest instance holding a refurbished MessageDigest
42 | */
43 | public ObjectPool.Lease instance() {
44 | ObjectPool.Lease lease = mdPool.get();
45 | lease.get().reset();
46 | return lease;
47 | }
48 |
49 | /**
50 | * Creates a new MessageDigest.
51 | *
52 | * @return New MessageDigest instance
53 | * @deprecated Use {@link #instance()}
54 | */
55 | @Deprecated
56 | public MessageDigest get() {
57 | final MessageDigest result = createMessageDigest();
58 | result.reset();
59 | return result;
60 | }
61 |
62 | }
--------------------------------------------------------------------------------
/src/main/java/org/cryptomator/cryptolib/common/ObjectPool.java:
--------------------------------------------------------------------------------
1 | package org.cryptomator.cryptolib.common;
2 |
3 | import java.lang.ref.WeakReference;
4 | import java.util.Queue;
5 | import java.util.concurrent.ConcurrentLinkedQueue;
6 | import java.util.function.Supplier;
7 |
8 | /**
9 | * A simple object pool for resources that are expensive to create but are needed frequently.
10 | *
11 | * Example Usage:
12 | *
{@code
13 | * Supplier fooFactory = () -> new Foo();
14 | * ObjectPool fooPool = new ObjectPool(fooFactory);
15 | * try (ObjectPool.Lease lease = fooPool.get()) { // attempts to get a pooled Foo or invokes factory
16 | * lease.get().foo(); // exclusively use Foo instance
17 | * } // releases instance back to the pool when done
18 | * }
19 | *
20 | * @param Type of the pooled objects
21 | */
22 | public class ObjectPool {
23 |
24 | private final Queue> returnedInstances;
25 | private final Supplier factory;
26 |
27 | public ObjectPool(Supplier factory) {
28 | this.returnedInstances = new ConcurrentLinkedQueue<>();
29 | this.factory = factory;
30 | }
31 |
32 | public Lease get() {
33 | WeakReference ref;
34 | while ((ref = returnedInstances.poll()) != null) {
35 | T cached = ref.get();
36 | if (cached != null) {
37 | return new Lease<>(this, cached);
38 | }
39 | }
40 | return new Lease<>(this, factory.get());
41 | }
42 |
43 | /**
44 | * A holder for resource leased from an {@link ObjectPool}.
45 | * This is basically an {@link AutoCloseable autocloseable} {@link Supplier} that is intended to be used
46 | * via try-with-resource blocks.
47 | *
48 | * @param Type of the leased instance
49 | */
50 | public static class Lease implements AutoCloseable, Supplier {
51 |
52 | private final ObjectPool pool;
53 | private T obj;
54 |
55 | private Lease(ObjectPool pool, T obj) {
56 | this.pool = pool;
57 | this.obj = obj;
58 | }
59 |
60 | public T get() {
61 | return obj;
62 | }
63 |
64 | @Override
65 | public void close() {
66 | pool.returnedInstances.add(new WeakReference<>(obj));
67 | obj = null;
68 | }
69 | }
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/src/main/java/org/cryptomator/cryptolib/common/P384KeyPair.java:
--------------------------------------------------------------------------------
1 | package org.cryptomator.cryptolib.common;
2 |
3 | import java.io.IOException;
4 | import java.io.InputStream;
5 | import java.io.OutputStream;
6 | import java.nio.file.Files;
7 | import java.nio.file.Path;
8 | import java.nio.file.StandardCopyOption;
9 | import java.nio.file.StandardOpenOption;
10 | import java.security.AlgorithmParameters;
11 | import java.security.InvalidAlgorithmParameterException;
12 | import java.security.KeyFactory;
13 | import java.security.KeyPair;
14 | import java.security.KeyPairGenerator;
15 | import java.security.NoSuchAlgorithmException;
16 | import java.security.PrivateKey;
17 | import java.security.PublicKey;
18 | import java.security.spec.ECGenParameterSpec;
19 | import java.security.spec.ECParameterSpec;
20 | import java.security.spec.InvalidKeySpecException;
21 | import java.security.spec.InvalidParameterSpecException;
22 | import java.security.spec.PKCS8EncodedKeySpec;
23 | import java.security.spec.X509EncodedKeySpec;
24 |
25 | public class P384KeyPair extends ECKeyPair {
26 |
27 | private static final String EC_ALG = "EC";
28 | private static final String EC_CURVE_NAME = "secp384r1";
29 | private static final String SIGNATURE_ALG = "SHA384withECDSA";
30 |
31 | private P384KeyPair(KeyPair keyPair) {
32 | super(keyPair, getCurveParams());
33 | }
34 |
35 | public static P384KeyPair generate() {
36 | KeyPair keyPair = getKeyPairGenerator().generateKeyPair();
37 | return new P384KeyPair(keyPair);
38 | }
39 |
40 | /**
41 | * Creates a key pair from the given key specs.
42 | *
43 | * @param publicKeySpec DER formatted public key
44 | * @param privateKeySpec DER formatted private key
45 | * @return created key pair
46 | * @throws InvalidKeySpecException If the supplied key specs are unsuitable for {@value #EC_ALG} keys
47 | */
48 | public static P384KeyPair create(X509EncodedKeySpec publicKeySpec, PKCS8EncodedKeySpec privateKeySpec) throws InvalidKeySpecException {
49 | try {
50 | KeyFactory factory = KeyFactory.getInstance(EC_ALG);
51 | PublicKey publicKey = factory.generatePublic(publicKeySpec);
52 | PrivateKey privateKey = factory.generatePrivate(privateKeySpec);
53 | return new P384KeyPair(new KeyPair(publicKey, privateKey));
54 | } catch (NoSuchAlgorithmException e) {
55 | throw new IllegalStateException(EC_ALG + " not supported");
56 | }
57 | }
58 |
59 | /**
60 | * Loads a key pair from the given file
61 | *
62 | * @param p12File A .p12 file
63 | * @param passphrase The password to protect the key material
64 | * @return loaded key pair
65 | * @throws IOException In case of I/O errors
66 | * @throws Pkcs12PasswordException If the supplied password is incorrect
67 | * @throws Pkcs12Exception If any cryptographic operation fails
68 | */
69 | public static P384KeyPair load(Path p12File, char[] passphrase) throws IOException, Pkcs12PasswordException, Pkcs12Exception {
70 | try (InputStream in = Files.newInputStream(p12File, StandardOpenOption.READ)) {
71 | return load(in, passphrase);
72 | }
73 | }
74 |
75 | /**
76 | * Loads a key pair from the given input stream
77 | *
78 | * @param in An input stream providing PKCS#12 formatted data
79 | * @param passphrase The password to protect the key material
80 | * @return loaded key pair
81 | * @throws IOException In case of I/O errors
82 | * @throws Pkcs12PasswordException If the supplied password is incorrect
83 | * @throws Pkcs12Exception If any cryptographic operation fails
84 | */
85 | public static P384KeyPair load(InputStream in, char[] passphrase) throws IOException, Pkcs12PasswordException, Pkcs12Exception {
86 | KeyPair keyPair = Pkcs12Helper.importFrom(in, passphrase);
87 | return new P384KeyPair(keyPair);
88 | }
89 |
90 | /**
91 | * Stores this key pair in PKCS#12 format at the given path
92 | *
93 | * @param p12File The path of the .p12 file
94 | * @param passphrase The password to protect the key material
95 | * @throws IOException In case of I/O errors
96 | * @throws Pkcs12Exception If any cryptographic operation fails
97 | */
98 | public void store(Path p12File, char[] passphrase) throws IOException, Pkcs12Exception {
99 | Path tmpFile = p12File.resolveSibling(p12File.getFileName().toString() + ".tmp");
100 | try (OutputStream out = Files.newOutputStream(tmpFile, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
101 | store(out, passphrase);
102 | }
103 | Files.move(tmpFile, p12File, StandardCopyOption.REPLACE_EXISTING);
104 | }
105 |
106 | /**
107 | * Stores this key in PKCS#12 format to the given output stream
108 | *
109 | * @param out The output stream to which the data will be written
110 | * @param passphrase The password to protect the key material
111 | * @throws IOException In case of I/O errors
112 | * @throws Pkcs12Exception If any cryptographic operation fails
113 | */
114 | public void store(OutputStream out, char[] passphrase) throws IOException, Pkcs12Exception {
115 | Pkcs12Helper.exportTo(keyPair(), out, passphrase, SIGNATURE_ALG);
116 | }
117 |
118 | private static KeyPairGenerator getKeyPairGenerator() {
119 | try {
120 | KeyPairGenerator keyGen = KeyPairGenerator.getInstance(EC_ALG);
121 | keyGen.initialize(new ECGenParameterSpec(EC_CURVE_NAME));
122 | return keyGen;
123 | } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
124 | throw new IllegalStateException(EC_CURVE_NAME + " curve not supported");
125 | }
126 | }
127 |
128 | private static ECParameterSpec getCurveParams() {
129 | try {
130 | AlgorithmParameters parameters = AlgorithmParameters.getInstance(EC_ALG);
131 | parameters.init(new ECGenParameterSpec(EC_CURVE_NAME));
132 | return parameters.getParameterSpec(ECParameterSpec.class);
133 | } catch (NoSuchAlgorithmException | InvalidParameterSpecException e) {
134 | throw new IllegalStateException(EC_CURVE_NAME + " curve not supported");
135 | }
136 | }
137 |
138 |
139 |
140 | }
141 |
--------------------------------------------------------------------------------
/src/main/java/org/cryptomator/cryptolib/common/Pkcs12Exception.java:
--------------------------------------------------------------------------------
1 | package org.cryptomator.cryptolib.common;
2 |
3 | import org.cryptomator.cryptolib.api.CryptoException;
4 |
5 | /**
6 | * Loading from or exporting to PKCS12 format failed.
7 | */
8 | public class Pkcs12Exception extends CryptoException {
9 |
10 | protected Pkcs12Exception(String message, Throwable cause) {
11 | super(message, cause);
12 | }
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/org/cryptomator/cryptolib/common/Pkcs12Helper.java:
--------------------------------------------------------------------------------
1 | package org.cryptomator.cryptolib.common;
2 |
3 | import java.io.IOException;
4 | import java.io.InputStream;
5 | import java.io.OutputStream;
6 | import java.security.GeneralSecurityException;
7 | import java.security.KeyPair;
8 | import java.security.KeyStore;
9 | import java.security.KeyStoreException;
10 | import java.security.PrivateKey;
11 | import java.security.PublicKey;
12 | import java.security.UnrecoverableKeyException;
13 | import java.security.cert.X509Certificate;
14 | import java.time.Instant;
15 | import java.time.Period;
16 |
17 | class Pkcs12Helper {
18 |
19 | private static final String X509_ISSUER = "CN=Cryptomator";
20 | private static final String X509_SUBJECT = "CN=Self Signed Cert";
21 | private static final int X509_VALID_DAYS = 3560;
22 | private static final String KEYSTORE_ALIAS_KEY = "key";
23 | private static final String KEYSTORE_ALIAS_CERT = "crt";
24 |
25 | private Pkcs12Helper() {
26 | }
27 |
28 | /**
29 | * Stores the given key pair in PKCS#12 format.
30 | *
31 | * @param keyPair The key pair to export
32 | * @param out The output stream to which the result will be written
33 | * @param pw The password to protect the key material
34 | * @param signatureAlg A suited signature algorithm to sign a x509v3 cert holding the public key
35 | * @throws IOException In case of I/O errors
36 | * @throws Pkcs12Exception If any cryptographic operation fails
37 | */
38 | public static void exportTo(KeyPair keyPair, OutputStream out, char[] pw, String signatureAlg) throws IOException, Pkcs12Exception {
39 | try {
40 | KeyStore keyStore = getKeyStore();
41 | keyStore.load(null, pw);
42 | X509Certificate cert = X509CertBuilder.init(keyPair, signatureAlg) //
43 | .withIssuer(X509_ISSUER) //
44 | .withSubject(X509_SUBJECT) //
45 | .withNotBefore(Instant.now()) //
46 | .withNotAfter(Instant.now().plus(Period.ofDays(X509_VALID_DAYS)))
47 | .build();
48 | X509Certificate[] chain = new X509Certificate[]{cert};
49 | keyStore.setKeyEntry(KEYSTORE_ALIAS_KEY, keyPair.getPrivate(), pw, chain);
50 | keyStore.setCertificateEntry(KEYSTORE_ALIAS_CERT, cert);
51 | keyStore.store(out, pw);
52 | } catch (IllegalArgumentException | GeneralSecurityException e) {
53 | throw new Pkcs12Exception("Failed to store PKCS12 file.", e);
54 | }
55 | }
56 |
57 | /**
58 | * Loads a key pair from PKCS#12 format.
59 | *
60 | * @param in Where to load the key pair from
61 | * @param pw The password to protect the key material
62 | * @throws IOException In case of I/O errors
63 | * @throws Pkcs12PasswordException If the supplied password is incorrect
64 | * @throws Pkcs12Exception If any cryptographic operation fails
65 | */
66 | public static KeyPair importFrom(InputStream in, char[] pw) throws IOException, Pkcs12PasswordException, Pkcs12Exception {
67 | try {
68 | KeyStore keyStore = getKeyStore();
69 | keyStore.load(in, pw);
70 | PrivateKey sk = (PrivateKey) keyStore.getKey(KEYSTORE_ALIAS_KEY, pw);
71 | PublicKey pk = keyStore.getCertificate(KEYSTORE_ALIAS_CERT).getPublicKey();
72 | return new KeyPair(pk, sk);
73 | } catch (UnrecoverableKeyException e) {
74 | throw new Pkcs12PasswordException(e);
75 | } catch (IOException e) {
76 | if (e.getCause() instanceof UnrecoverableKeyException) {
77 | throw new Pkcs12PasswordException(e);
78 | } else {
79 | throw e;
80 | }
81 | } catch (GeneralSecurityException e) {
82 | throw new Pkcs12Exception("Failed to load PKCS12 file.", e);
83 | }
84 | }
85 |
86 | private static KeyStore getKeyStore() {
87 | try {
88 | return KeyStore.getInstance("PKCS12");
89 | } catch (KeyStoreException e) {
90 | throw new IllegalStateException("Every implementation of the Java platform is required to support PKCS12.");
91 | }
92 | }
93 |
94 | }
95 |
--------------------------------------------------------------------------------
/src/main/java/org/cryptomator/cryptolib/common/Pkcs12PasswordException.java:
--------------------------------------------------------------------------------
1 | package org.cryptomator.cryptolib.common;
2 |
3 | /**
4 | * Loading from PKCS12 format failed due to wrong password.
5 | */
6 | public class Pkcs12PasswordException extends Pkcs12Exception {
7 |
8 | protected Pkcs12PasswordException(Throwable cause) {
9 | super("Wrong password", cause);
10 | }
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/org/cryptomator/cryptolib/common/ReseedingSecureRandom.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.common;
10 |
11 | import java.security.NoSuchAlgorithmException;
12 | import java.security.Provider;
13 | import java.security.SecureRandom;
14 | import java.security.SecureRandomSpi;
15 |
16 | import org.slf4j.Logger;
17 | import org.slf4j.LoggerFactory;
18 |
19 | /**
20 | * Wraps a fast CSPRNG, which gets reseeded automatically after a certain amount of bytes has been generated.
21 | *
22 | *
23 | * Java 8 Example:
24 | *
25 | *
26 | * SecureRandom csprng = ReseedingSecureRandom.create(SecureRandom.getInstanceStrong());
27 | *
28 | */
29 | public class ReseedingSecureRandom extends SecureRandom {
30 |
31 | private static final Logger LOG = LoggerFactory.getLogger(ReseedingSecureRandom.class);
32 | private static final Provider PROVIDER = new ReseedingSecureRandomProvider();
33 |
34 | /**
35 | * @param seeder RNG for high-quality random numbers. E.g. SecureRandom.getInstanceStrong()
in Java 8+ environments.
36 | * @param csprng A fast csprng implementation, such as SHA1PRNG
, that will be wrapped by this instance.
37 | * @param reseedAfter How many bytes can be read from the csprng
, before a new seed will be generated.
38 | * @param seedLength Number of bytes generated by seeder
in order to seed csprng
.
39 | */
40 | public ReseedingSecureRandom(SecureRandom seeder, SecureRandom csprng, long reseedAfter, int seedLength) {
41 | super(new ReseedingSecureRandomSpi(seeder, csprng, reseedAfter, seedLength), PROVIDER);
42 | }
43 |
44 | /**
45 | * Creates a pre-configured automatically reseeding SHA1PRNG instance, reseeding itself with 440 bits from the given seeder after generating 2^30 bytes,
46 | * thus satisfying recommendations by NIST SP 800-90A Rev 1.
47 | *
48 | * @param seeder RNG for high-quality random numbers. E.g. SecureRandom.getInstanceStrong()
in Java 8+ environments.
49 | * @return An automatically reseeding SHA1PRNG suitable as CSPRNG for most applications.
50 | */
51 | public static ReseedingSecureRandom create(SecureRandom seeder) {
52 | try {
53 | return new ReseedingSecureRandom(seeder, SecureRandom.getInstance("SHA1PRNG"), 1 << 30, 55);
54 | } catch (NoSuchAlgorithmException e) {
55 | throw new IllegalStateException("SHA1PRNG must exist in every Java platform implementation.", e);
56 | }
57 | }
58 |
59 | private static class ReseedingSecureRandomProvider extends Provider {
60 |
61 | protected ReseedingSecureRandomProvider() {
62 | super("ReseedingSecureRandomProvider", 1.0, "Provides ReseedingSecureRandom");
63 | }
64 |
65 | }
66 |
67 | private static class ReseedingSecureRandomSpi extends SecureRandomSpi {
68 |
69 | private final SecureRandom seeder;
70 | private final SecureRandom csprng;
71 | private final long reseedAfter;
72 | private final int seedLength;
73 | private long counter;
74 |
75 | public ReseedingSecureRandomSpi(SecureRandom seeder, SecureRandom csprng, long reseedAfter, int seedLength) {
76 | this.seeder = seeder;
77 | this.csprng = csprng;
78 | this.reseedAfter = reseedAfter;
79 | this.seedLength = seedLength;
80 | this.counter = reseedAfter; // trigger reseed during first "engineNextBytes(...)"
81 | }
82 |
83 | @Override
84 | protected void engineSetSeed(byte[] seed) {
85 | csprng.setSeed(seed);
86 | }
87 |
88 | @Override
89 | protected void engineNextBytes(byte[] bytes) {
90 | if (counter + bytes.length > reseedAfter) {
91 | reseed();
92 | }
93 | counter += bytes.length;
94 | csprng.nextBytes(bytes);
95 | }
96 |
97 | @Override
98 | protected byte[] engineGenerateSeed(int numBytes) {
99 | try {
100 | LOG.debug("Seeding CSPRNG with {} bytes...", numBytes);
101 | return seeder.generateSeed(numBytes);
102 | } finally {
103 | LOG.debug("Seeded CSPRNG.");
104 | }
105 | }
106 |
107 | private void reseed() {
108 | engineSetSeed(engineGenerateSeed(seedLength));
109 | counter = 0;
110 | }
111 |
112 | }
113 |
114 | }
115 |
--------------------------------------------------------------------------------
/src/main/java/org/cryptomator/cryptolib/v1/Constants.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.v1;
10 |
11 | final class Constants {
12 |
13 | private Constants() {
14 | }
15 |
16 | static final String CONTENT_ENC_ALG = "AES";
17 |
18 | static final int NONCE_SIZE = 16;
19 | static final int PAYLOAD_SIZE = 32 * 1024;
20 | static final int MAC_SIZE = 32;
21 | static final int CHUNK_SIZE = NONCE_SIZE + PAYLOAD_SIZE + MAC_SIZE;
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/org/cryptomator/cryptolib/v1/CryptorImpl.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.v1;
10 |
11 | import org.cryptomator.cryptolib.api.Cryptor;
12 | import org.cryptomator.cryptolib.api.Masterkey;
13 |
14 | import java.security.SecureRandom;
15 |
16 | class CryptorImpl implements Cryptor {
17 |
18 | private final Masterkey masterkey;
19 | private final FileContentCryptorImpl fileContentCryptor;
20 | private final FileHeaderCryptorImpl fileHeaderCryptor;
21 | private final FileNameCryptorImpl fileNameCryptor;
22 |
23 | /**
24 | * Package-private constructor.
25 | * Use {@link CryptorProviderImpl#provide(Masterkey, SecureRandom)} to obtain a Cryptor instance.
26 | */
27 | CryptorImpl(Masterkey masterkey, SecureRandom random) {
28 | this.masterkey = masterkey;
29 | this.fileHeaderCryptor = new FileHeaderCryptorImpl(masterkey, random);
30 | this.fileContentCryptor = new FileContentCryptorImpl(masterkey, random);
31 | this.fileNameCryptor = new FileNameCryptorImpl(masterkey);
32 | }
33 |
34 | @Override
35 | public FileContentCryptorImpl fileContentCryptor() {
36 | assertNotDestroyed();
37 | return fileContentCryptor;
38 | }
39 |
40 | @Override
41 | public FileHeaderCryptorImpl fileHeaderCryptor() {
42 | assertNotDestroyed();
43 | return fileHeaderCryptor;
44 | }
45 |
46 | @Override
47 | public FileNameCryptorImpl fileNameCryptor() {
48 | assertNotDestroyed();
49 | return fileNameCryptor;
50 | }
51 |
52 | @Override
53 | public boolean isDestroyed() {
54 | return masterkey.isDestroyed();
55 | }
56 |
57 | @Override
58 | public void close() {
59 | destroy();
60 | }
61 |
62 | @Override
63 | public void destroy() {
64 | masterkey.destroy();
65 | }
66 |
67 | private void assertNotDestroyed() {
68 | if (isDestroyed()) {
69 | throw new IllegalStateException("Cryptor destroyed.");
70 | }
71 | }
72 |
73 | }
74 |
--------------------------------------------------------------------------------
/src/main/java/org/cryptomator/cryptolib/v1/CryptorProviderImpl.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.v1;
10 |
11 | import org.cryptomator.cryptolib.api.CryptorProvider;
12 | import org.cryptomator.cryptolib.api.Masterkey;
13 | import org.cryptomator.cryptolib.common.ReseedingSecureRandom;
14 |
15 | import java.security.SecureRandom;
16 |
17 | public class CryptorProviderImpl implements CryptorProvider {
18 |
19 | @Override
20 | public Scheme scheme() {
21 | return Scheme.SIV_CTRMAC;
22 | }
23 |
24 | @Override
25 | public CryptorImpl provide(Masterkey masterkey, SecureRandom random) {
26 | return new CryptorImpl(masterkey, ReseedingSecureRandom.create(random));
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderImpl.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.v1;
10 |
11 | import com.google.common.base.Preconditions;
12 | import org.cryptomator.cryptolib.api.FileHeader;
13 | import org.cryptomator.cryptolib.common.DestroyableSecretKey;
14 |
15 | import javax.security.auth.Destroyable;
16 | import java.nio.ByteBuffer;
17 |
18 | class FileHeaderImpl implements FileHeader, Destroyable {
19 |
20 | static final int NONCE_POS = 0;
21 | static final int NONCE_LEN = 16;
22 | static final int PAYLOAD_POS = 16;
23 | static final int PAYLOAD_LEN = Payload.SIZE;
24 | static final int MAC_POS = 56;
25 | static final int MAC_LEN = 32;
26 | static final int SIZE = NONCE_LEN + PAYLOAD_LEN + MAC_LEN;
27 |
28 | private final byte[] nonce;
29 | private final Payload payload;
30 |
31 | FileHeaderImpl(byte[] nonce, Payload payload) {
32 | if (nonce.length != NONCE_LEN) {
33 | throw new IllegalArgumentException("Invalid nonce length. (was: " + nonce.length + ", required: " + NONCE_LEN + ")");
34 | }
35 | this.nonce = nonce;
36 | this.payload = payload;
37 | }
38 |
39 | static FileHeaderImpl cast(FileHeader header) {
40 | if (header instanceof FileHeaderImpl) {
41 | return (FileHeaderImpl) header;
42 | } else {
43 | throw new IllegalArgumentException("Unsupported header type " + header.getClass());
44 | }
45 | }
46 |
47 | public byte[] getNonce() {
48 | return nonce;
49 | }
50 |
51 | public Payload getPayload() {
52 | return payload;
53 | }
54 |
55 | @Override
56 | public long getReserved() {
57 | return payload.getReserved();
58 | }
59 |
60 | @Override
61 | public void setReserved(long reserved) {
62 | payload.setReserved(reserved);
63 | }
64 |
65 | @Override
66 | public boolean isDestroyed() {
67 | return payload.isDestroyed();
68 | }
69 |
70 | @Override
71 | public void destroy() {
72 | payload.destroy();
73 | }
74 |
75 | public static class Payload implements Destroyable {
76 |
77 | static final int REVERSED_LEN = Long.BYTES;
78 | static final int CONTENT_KEY_LEN = 32;
79 | static final int SIZE = REVERSED_LEN + CONTENT_KEY_LEN;
80 |
81 | private long reserved;
82 | private final DestroyableSecretKey contentKey;
83 |
84 | Payload(long reversed, byte[] contentKeyBytes) {
85 | Preconditions.checkArgument(contentKeyBytes.length == CONTENT_KEY_LEN, "Invalid key length. (was: " + contentKeyBytes.length + ", required: " + CONTENT_KEY_LEN + ")");
86 | this.reserved = reversed;
87 | this.contentKey = new DestroyableSecretKey(contentKeyBytes, Constants.CONTENT_ENC_ALG);
88 | }
89 |
90 | static Payload decode(ByteBuffer cleartextPayloadBuf) {
91 | Preconditions.checkArgument(cleartextPayloadBuf.remaining() == SIZE, "invalid payload buffer length");
92 | long reserved = cleartextPayloadBuf.getLong();
93 | byte[] contentKeyBytes = new byte[CONTENT_KEY_LEN];
94 | cleartextPayloadBuf.get(contentKeyBytes);
95 | return new Payload(reserved, contentKeyBytes);
96 | }
97 |
98 | ByteBuffer encode() {
99 | ByteBuffer buf = ByteBuffer.allocate(SIZE);
100 | buf.putLong(reserved);
101 | buf.put(contentKey.getEncoded());
102 | buf.flip();
103 | return buf;
104 | }
105 |
106 | private long getReserved() {
107 | return reserved;
108 | }
109 |
110 | private void setReserved(long reserved) {
111 | this.reserved = reserved;
112 | }
113 |
114 | DestroyableSecretKey getContentKey() {
115 | return contentKey;
116 | }
117 |
118 | @Override
119 | public boolean isDestroyed() {
120 | return contentKey.isDestroyed();
121 | }
122 |
123 | @Override
124 | public void destroy() {
125 | contentKey.destroy();
126 | }
127 |
128 | }
129 |
130 | }
131 |
--------------------------------------------------------------------------------
/src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2015, 2016 Sebastian Stenzel and others.
3 | * This file is licensed under the terms of the MIT license.
4 | * See the LICENSE.txt file for more info.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.v1;
10 |
11 | import com.google.common.io.BaseEncoding;
12 | import org.cryptomator.cryptolib.api.AuthenticationFailedException;
13 | import org.cryptomator.cryptolib.api.FileNameCryptor;
14 | import org.cryptomator.cryptolib.api.Masterkey;
15 | import org.cryptomator.cryptolib.common.DestroyableSecretKey;
16 | import org.cryptomator.cryptolib.common.MessageDigestSupplier;
17 | import org.cryptomator.cryptolib.common.ObjectPool;
18 | import org.cryptomator.siv.SivMode;
19 | import org.cryptomator.siv.UnauthenticCiphertextException;
20 |
21 | import javax.crypto.IllegalBlockSizeException;
22 | import java.security.MessageDigest;
23 |
24 | import static java.nio.charset.StandardCharsets.UTF_8;
25 |
26 | class FileNameCryptorImpl implements FileNameCryptor {
27 |
28 | private static final BaseEncoding BASE32 = BaseEncoding.base32();
29 | private static final ObjectPool AES_SIV = new ObjectPool<>(SivMode::new);
30 |
31 | private final Masterkey masterkey;
32 |
33 | FileNameCryptorImpl(Masterkey masterkey) {
34 | this.masterkey = masterkey;
35 | }
36 |
37 | @Override
38 | public String hashDirectoryId(String cleartextDirectoryId) {
39 | try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey();
40 | ObjectPool.Lease sha1 = MessageDigestSupplier.SHA1.instance();
41 | ObjectPool.Lease siv = AES_SIV.get()) {
42 | byte[] cleartextBytes = cleartextDirectoryId.getBytes(UTF_8);
43 | byte[] encryptedBytes = siv.get().encrypt(ek, mk, cleartextBytes);
44 | byte[] hashedBytes = sha1.get().digest(encryptedBytes);
45 | return BASE32.encode(hashedBytes);
46 | }
47 | }
48 |
49 | @Override
50 | public String encryptFilename(BaseEncoding encoding, String cleartextName, byte[]... associatedData) {
51 | try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey();
52 | ObjectPool.Lease siv = AES_SIV.get()) {
53 | byte[] cleartextBytes = cleartextName.getBytes(UTF_8);
54 | byte[] encryptedBytes = siv.get().encrypt(ek, mk, cleartextBytes, associatedData);
55 | return encoding.encode(encryptedBytes);
56 | }
57 | }
58 |
59 | @Override
60 | public String decryptFilename(BaseEncoding encoding, String ciphertextName, byte[]... associatedData) throws AuthenticationFailedException {
61 | try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey();
62 | ObjectPool.Lease siv = AES_SIV.get()) {
63 | byte[] encryptedBytes = encoding.decode(ciphertextName);
64 | byte[] cleartextBytes = siv.get().decrypt(ek, mk, encryptedBytes, associatedData);
65 | return new String(cleartextBytes, UTF_8);
66 | } catch (UnauthenticCiphertextException | IllegalArgumentException | IllegalBlockSizeException e) {
67 | throw new AuthenticationFailedException("Invalid Ciphertext.", e);
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/main/java/org/cryptomator/cryptolib/v2/Constants.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.v2;
10 |
11 | final class Constants {
12 |
13 | private Constants() {
14 | }
15 |
16 | static final String CONTENT_ENC_ALG = "AES";
17 |
18 | static final int GCM_NONCE_SIZE = 12; // 96 bit IVs strongly recommended for GCM
19 | static final int PAYLOAD_SIZE = 32 * 1024;
20 | static final int GCM_TAG_SIZE = 16;
21 | static final int CHUNK_SIZE = GCM_NONCE_SIZE + PAYLOAD_SIZE + GCM_TAG_SIZE;
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.v2;
10 |
11 | import org.cryptomator.cryptolib.api.Cryptor;
12 | import org.cryptomator.cryptolib.api.Masterkey;
13 | import org.cryptomator.cryptolib.v1.CryptorProviderImpl;
14 |
15 | import java.security.SecureRandom;
16 |
17 | class CryptorImpl implements Cryptor {
18 |
19 | private final Masterkey masterkey;
20 | private final FileContentCryptorImpl fileContentCryptor;
21 | private final FileHeaderCryptorImpl fileHeaderCryptor;
22 | private final FileNameCryptorImpl fileNameCryptor;
23 |
24 | /**
25 | * Package-private constructor.
26 | * Use {@link CryptorProviderImpl#provide(Masterkey, SecureRandom)} to obtain a Cryptor instance.
27 | */
28 | CryptorImpl(Masterkey masterkey, SecureRandom random) {
29 | this.masterkey = masterkey;
30 | this.fileHeaderCryptor = new FileHeaderCryptorImpl(masterkey, random);
31 | this.fileContentCryptor = new FileContentCryptorImpl(random);
32 | this.fileNameCryptor = new FileNameCryptorImpl(masterkey);
33 | }
34 |
35 | @Override
36 | public FileContentCryptorImpl fileContentCryptor() {
37 | assertNotDestroyed();
38 | return fileContentCryptor;
39 | }
40 |
41 | @Override
42 | public FileHeaderCryptorImpl fileHeaderCryptor() {
43 | assertNotDestroyed();
44 | return fileHeaderCryptor;
45 | }
46 |
47 | @Override
48 | public FileNameCryptorImpl fileNameCryptor() {
49 | assertNotDestroyed();
50 | return fileNameCryptor;
51 | }
52 |
53 | @Override
54 | public boolean isDestroyed() {
55 | return masterkey.isDestroyed();
56 | }
57 |
58 | @Override
59 | public void close() {
60 | destroy();
61 | }
62 |
63 | @Override
64 | public void destroy() {
65 | masterkey.destroy();
66 | }
67 |
68 | private void assertNotDestroyed() {
69 | if (isDestroyed()) {
70 | throw new IllegalStateException("Cryptor destroyed.");
71 | }
72 | }
73 |
74 | }
75 |
--------------------------------------------------------------------------------
/src/main/java/org/cryptomator/cryptolib/v2/CryptorProviderImpl.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.v2;
10 |
11 | import org.cryptomator.cryptolib.api.CryptorProvider;
12 | import org.cryptomator.cryptolib.api.Masterkey;
13 | import org.cryptomator.cryptolib.common.ReseedingSecureRandom;
14 |
15 | import java.security.SecureRandom;
16 |
17 | public class CryptorProviderImpl implements CryptorProvider {
18 |
19 | @Override
20 | public Scheme scheme() {
21 | return Scheme.SIV_GCM;
22 | }
23 |
24 | @Override
25 | public CryptorImpl provide(Masterkey masterkey, SecureRandom random) {
26 | return new CryptorImpl(masterkey, ReseedingSecureRandom.create(random));
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImpl.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.v2;
10 |
11 | import org.cryptomator.cryptolib.api.AuthenticationFailedException;
12 | import org.cryptomator.cryptolib.api.FileHeader;
13 | import org.cryptomator.cryptolib.api.FileHeaderCryptor;
14 | import org.cryptomator.cryptolib.api.Masterkey;
15 | import org.cryptomator.cryptolib.common.CipherSupplier;
16 | import org.cryptomator.cryptolib.common.DestroyableSecretKey;
17 | import org.cryptomator.cryptolib.common.ObjectPool;
18 |
19 | import javax.crypto.AEADBadTagException;
20 | import javax.crypto.BadPaddingException;
21 | import javax.crypto.Cipher;
22 | import javax.crypto.IllegalBlockSizeException;
23 | import javax.crypto.ShortBufferException;
24 | import javax.crypto.spec.GCMParameterSpec;
25 | import java.nio.ByteBuffer;
26 | import java.security.SecureRandom;
27 | import java.util.Arrays;
28 |
29 | import static org.cryptomator.cryptolib.v2.Constants.GCM_TAG_SIZE;
30 |
31 | class FileHeaderCryptorImpl implements FileHeaderCryptor {
32 |
33 | private final Masterkey masterkey;
34 | private final SecureRandom random;
35 |
36 | FileHeaderCryptorImpl(Masterkey masterkey, SecureRandom random) {
37 | this.masterkey = masterkey;
38 | this.random = random;
39 | }
40 |
41 | @Override
42 | public FileHeader create() {
43 | byte[] nonce = new byte[FileHeaderImpl.NONCE_LEN];
44 | random.nextBytes(nonce);
45 | byte[] contentKey = new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN];
46 | random.nextBytes(contentKey);
47 | FileHeaderImpl.Payload payload = new FileHeaderImpl.Payload(-1, contentKey);
48 | return new FileHeaderImpl(nonce, payload);
49 | }
50 |
51 | @Override
52 | public int headerSize() {
53 | return FileHeaderImpl.SIZE;
54 | }
55 |
56 | @Override
57 | public ByteBuffer encryptHeader(FileHeader header) {
58 | FileHeaderImpl headerImpl = FileHeaderImpl.cast(header);
59 | ByteBuffer payloadCleartextBuf = headerImpl.getPayload().encode();
60 | try (DestroyableSecretKey ek = masterkey.getEncKey()) {
61 | ByteBuffer result = ByteBuffer.allocate(FileHeaderImpl.SIZE);
62 | result.put(headerImpl.getNonce());
63 |
64 | // encrypt payload:
65 | try (ObjectPool.Lease cipher = CipherSupplier.AES_GCM.encryptionCipher(ek, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, headerImpl.getNonce()))) {
66 | int encrypted = cipher.get().doFinal(payloadCleartextBuf, result);
67 | assert encrypted == FileHeaderImpl.PAYLOAD_LEN + FileHeaderImpl.TAG_LEN;
68 | }
69 | result.flip();
70 | return result;
71 | } catch (ShortBufferException e) {
72 | throw new IllegalStateException("Result buffer too small for encrypted header payload.", e);
73 | } catch (IllegalBlockSizeException | BadPaddingException e) {
74 | throw new IllegalStateException("Unexpected exception during GCM encryption.", e);
75 | } finally {
76 | Arrays.fill(payloadCleartextBuf.array(), (byte) 0x00);
77 | }
78 | }
79 |
80 | @Override
81 | public FileHeader decryptHeader(ByteBuffer ciphertextHeaderBuf) throws AuthenticationFailedException {
82 | if (ciphertextHeaderBuf.remaining() < FileHeaderImpl.SIZE) {
83 | throw new IllegalArgumentException("Malformed ciphertext header");
84 | }
85 | ByteBuffer buf = ciphertextHeaderBuf.duplicate();
86 | byte[] nonce = new byte[FileHeaderImpl.NONCE_LEN];
87 | buf.position(FileHeaderImpl.NONCE_POS);
88 | buf.get(nonce);
89 | byte[] ciphertextAndTag = new byte[FileHeaderImpl.PAYLOAD_LEN + FileHeaderImpl.TAG_LEN];
90 | buf.position(FileHeaderImpl.PAYLOAD_POS);
91 | buf.get(ciphertextAndTag);
92 |
93 | // 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 #24
94 | ByteBuffer payloadCleartextBuf = ByteBuffer.allocate(FileHeaderImpl.Payload.SIZE + GCM_TAG_SIZE);
95 | try (DestroyableSecretKey ek = masterkey.getEncKey()) {
96 | // decrypt payload:
97 | try (ObjectPool.Lease cipher = CipherSupplier.AES_GCM.decryptionCipher(ek, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce))) {
98 | int decrypted = cipher.get().doFinal(ByteBuffer.wrap(ciphertextAndTag), payloadCleartextBuf);
99 | assert decrypted == FileHeaderImpl.Payload.SIZE;
100 | }
101 | payloadCleartextBuf.flip();
102 | FileHeaderImpl.Payload payload = FileHeaderImpl.Payload.decode(payloadCleartextBuf);
103 |
104 | return new FileHeaderImpl(nonce, payload);
105 | } catch (AEADBadTagException e) {
106 | throw new AuthenticationFailedException("Header tag mismatch.", e);
107 | } catch (ShortBufferException e) {
108 | throw new IllegalStateException("Result buffer too small for decrypted header payload.", e);
109 | } catch (IllegalBlockSizeException | BadPaddingException e) {
110 | throw new IllegalStateException("Unexpected exception during GCM decryption.", e);
111 | } finally {
112 | Arrays.fill(payloadCleartextBuf.array(), (byte) 0x00);
113 | }
114 | }
115 |
116 | }
117 |
--------------------------------------------------------------------------------
/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderImpl.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.v2;
10 |
11 | import com.google.common.base.Preconditions;
12 | import org.cryptomator.cryptolib.api.FileHeader;
13 | import org.cryptomator.cryptolib.common.DestroyableSecretKey;
14 |
15 | import javax.security.auth.Destroyable;
16 | import java.nio.ByteBuffer;
17 |
18 | class FileHeaderImpl implements FileHeader, Destroyable {
19 |
20 | static final int NONCE_POS = 0;
21 | static final int NONCE_LEN = Constants.GCM_NONCE_SIZE;
22 | static final int PAYLOAD_POS = NONCE_POS + NONCE_LEN; // 12
23 | static final int PAYLOAD_LEN = Payload.SIZE;
24 | static final int TAG_POS = PAYLOAD_POS + PAYLOAD_LEN; // 52
25 | static final int TAG_LEN = Constants.GCM_TAG_SIZE;
26 | static final int SIZE = NONCE_LEN + PAYLOAD_LEN + TAG_LEN;
27 |
28 | private final byte[] nonce;
29 | private final Payload payload;
30 |
31 | FileHeaderImpl(byte[] nonce, Payload payload) {
32 | if (nonce.length != NONCE_LEN) {
33 | throw new IllegalArgumentException("Invalid nonce length. (was: " + nonce.length + ", required: " + NONCE_LEN + ")");
34 | }
35 | this.nonce = nonce;
36 | this.payload = payload;
37 | }
38 |
39 | static FileHeaderImpl cast(FileHeader header) {
40 | if (header instanceof FileHeaderImpl) {
41 | return (FileHeaderImpl) header;
42 | } else {
43 | throw new IllegalArgumentException("Unsupported header type " + header.getClass());
44 | }
45 | }
46 |
47 | public byte[] getNonce() {
48 | return nonce;
49 | }
50 |
51 | public Payload getPayload() {
52 | return payload;
53 | }
54 |
55 | @Override
56 | public long getReserved() {
57 | return payload.getReserved();
58 | }
59 |
60 | @Override
61 | public void setReserved(long reserved) {
62 | payload.setReserved(reserved);
63 | }
64 |
65 | @Override
66 | public boolean isDestroyed() {
67 | return payload.isDestroyed();
68 | }
69 |
70 | @Override
71 | public void destroy() {
72 | payload.destroy();
73 | }
74 |
75 | public static class Payload implements Destroyable {
76 |
77 | static final int REVERSED_LEN = Long.BYTES;
78 | static final int CONTENT_KEY_LEN = 32;
79 | static final int SIZE = REVERSED_LEN + CONTENT_KEY_LEN;
80 |
81 | private long reserved;
82 | private final DestroyableSecretKey contentKey;
83 |
84 | Payload(long reversed, byte[] contentKeyBytes) {
85 | Preconditions.checkArgument(contentKeyBytes.length == CONTENT_KEY_LEN, "Invalid key length. (was: " + contentKeyBytes.length + ", required: " + CONTENT_KEY_LEN + ")");
86 | this.reserved = reversed;
87 | this.contentKey = new DestroyableSecretKey(contentKeyBytes, Constants.CONTENT_ENC_ALG);
88 | }
89 |
90 | static Payload decode(ByteBuffer cleartextPayloadBuf) {
91 | Preconditions.checkArgument(cleartextPayloadBuf.remaining() == SIZE, "invalid payload buffer length");
92 | long reserved = cleartextPayloadBuf.getLong();
93 | byte[] contentKeyBytes = new byte[CONTENT_KEY_LEN];
94 | cleartextPayloadBuf.get(contentKeyBytes);
95 | return new Payload(reserved, contentKeyBytes);
96 | }
97 |
98 | ByteBuffer encode() {
99 | ByteBuffer buf = ByteBuffer.allocate(SIZE);
100 | buf.putLong(reserved);
101 | buf.put(contentKey.getEncoded());
102 | buf.flip();
103 | return buf;
104 | }
105 |
106 | private long getReserved() {
107 | return reserved;
108 | }
109 |
110 | private void setReserved(long reserved) {
111 | this.reserved = reserved;
112 | }
113 |
114 | DestroyableSecretKey getContentKey() {
115 | return contentKey;
116 | }
117 |
118 | @Override
119 | public boolean isDestroyed() {
120 | return contentKey.isDestroyed();
121 | }
122 |
123 | @Override
124 | public void destroy() {
125 | contentKey.destroy();
126 | }
127 |
128 | }
129 |
130 | }
131 |
--------------------------------------------------------------------------------
/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2015, 2016 Sebastian Stenzel and others.
3 | * This file is licensed under the terms of the MIT license.
4 | * See the LICENSE.txt file for more info.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.v2;
10 |
11 | import com.google.common.io.BaseEncoding;
12 | import org.cryptomator.cryptolib.api.AuthenticationFailedException;
13 | import org.cryptomator.cryptolib.api.FileNameCryptor;
14 | import org.cryptomator.cryptolib.api.Masterkey;
15 | import org.cryptomator.cryptolib.common.DestroyableSecretKey;
16 | import org.cryptomator.cryptolib.common.MessageDigestSupplier;
17 | import org.cryptomator.cryptolib.common.ObjectPool;
18 | import org.cryptomator.siv.SivMode;
19 | import org.cryptomator.siv.UnauthenticCiphertextException;
20 |
21 | import javax.crypto.IllegalBlockSizeException;
22 | import java.security.MessageDigest;
23 |
24 | import static java.nio.charset.StandardCharsets.UTF_8;
25 |
26 | class FileNameCryptorImpl implements FileNameCryptor {
27 |
28 | private static final BaseEncoding BASE32 = BaseEncoding.base32();
29 | private static final ObjectPool AES_SIV = new ObjectPool<>(SivMode::new);
30 |
31 | private final Masterkey masterkey;
32 |
33 | FileNameCryptorImpl(Masterkey masterkey) {
34 | this.masterkey = masterkey;
35 | }
36 |
37 | @Override
38 | public String hashDirectoryId(String cleartextDirectoryId) {
39 | try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey();
40 | ObjectPool.Lease sha1 = MessageDigestSupplier.SHA1.instance();
41 | ObjectPool.Lease siv = AES_SIV.get()) {
42 | byte[] cleartextBytes = cleartextDirectoryId.getBytes(UTF_8);
43 | byte[] encryptedBytes = siv.get().encrypt(ek, mk, cleartextBytes);
44 | byte[] hashedBytes = sha1.get().digest(encryptedBytes);
45 | return BASE32.encode(hashedBytes);
46 | }
47 | }
48 |
49 | @Override
50 | public String encryptFilename(BaseEncoding encoding, String cleartextName, byte[]... associatedData) {
51 | try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey();
52 | ObjectPool.Lease siv = AES_SIV.get()) {
53 | byte[] cleartextBytes = cleartextName.getBytes(UTF_8);
54 | byte[] encryptedBytes = siv.get().encrypt(ek, mk, cleartextBytes, associatedData);
55 | return encoding.encode(encryptedBytes);
56 | }
57 | }
58 |
59 | @Override
60 | public String decryptFilename(BaseEncoding encoding, String ciphertextName, byte[]... associatedData) throws AuthenticationFailedException {
61 | try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey();
62 | ObjectPool.Lease siv = AES_SIV.get()) {
63 | byte[] encryptedBytes = encoding.decode(ciphertextName);
64 | byte[] cleartextBytes = siv.get().decrypt(ek, mk, encryptedBytes, associatedData);
65 | return new String(cleartextBytes, UTF_8);
66 | } catch (IllegalArgumentException | UnauthenticCiphertextException | IllegalBlockSizeException e) {
67 | throw new AuthenticationFailedException("Invalid Ciphertext.", e);
68 | }
69 | }
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/src/main/java22/module-info.java:
--------------------------------------------------------------------------------
1 | import org.cryptomator.cryptolib.api.CryptorProvider;
2 |
3 | /**
4 | * This module provides the highlevel cryptographic API used by Cryptomator.
5 | *
6 | * @uses CryptorProvider See {@link CryptorProvider#forScheme(CryptorProvider.Scheme)}
7 | * @provides CryptorProvider Providers for {@link org.cryptomator.cryptolib.api.CryptorProvider.Scheme#SIV_CTRMAC SIV/CTR-then-MAC}
8 | * and {@link org.cryptomator.cryptolib.api.CryptorProvider.Scheme#SIV_GCM SIV/GCM}
9 | */
10 | module org.cryptomator.cryptolib {
11 | requires static org.bouncycastle.provider; // will be shaded
12 | requires static org.bouncycastle.pkix; // will be shaded
13 | requires org.cryptomator.siv;
14 | requires com.google.gson;
15 | requires transitive com.google.common;
16 | requires org.slf4j;
17 |
18 | exports org.cryptomator.cryptolib.api;
19 | exports org.cryptomator.cryptolib.common;
20 |
21 | opens org.cryptomator.cryptolib.common to com.google.gson;
22 |
23 | uses CryptorProvider;
24 |
25 | provides CryptorProvider
26 | with org.cryptomator.cryptolib.v1.CryptorProviderImpl, org.cryptomator.cryptolib.v2.CryptorProviderImpl;
27 | }
--------------------------------------------------------------------------------
/src/main/java9/module-info.java:
--------------------------------------------------------------------------------
1 | import org.cryptomator.cryptolib.api.CryptorProvider;
2 |
3 | /**
4 | * This module provides the highlevel cryptographic API used by Cryptomator.
5 | *
6 | * @uses CryptorProvider See {@link CryptorProvider#forScheme(CryptorProvider.Scheme)}
7 | * @provides CryptorProvider Providers for {@link org.cryptomator.cryptolib.api.CryptorProvider.Scheme#SIV_CTRMAC SIV/CTR-then-MAC}
8 | * and {@link org.cryptomator.cryptolib.api.CryptorProvider.Scheme#SIV_GCM SIV/GCM}
9 | */
10 | module org.cryptomator.cryptolib {
11 | requires static org.bouncycastle.provider; // will be shaded
12 | requires static org.bouncycastle.pkix; // will be shaded
13 | requires jdk.crypto.ec; // required at runtime for ECC
14 | requires org.cryptomator.siv;
15 | requires com.google.gson;
16 | requires transitive com.google.common;
17 | requires org.slf4j;
18 |
19 | exports org.cryptomator.cryptolib.api;
20 | exports org.cryptomator.cryptolib.common;
21 |
22 | opens org.cryptomator.cryptolib.common to com.google.gson;
23 |
24 | uses CryptorProvider;
25 |
26 | provides CryptorProvider
27 | with org.cryptomator.cryptolib.v1.CryptorProviderImpl, org.cryptomator.cryptolib.v2.CryptorProviderImpl;
28 | }
--------------------------------------------------------------------------------
/src/main/resources/META-INF/native-image/org.cryptomator/cryptolib/reflect-config.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name":"byte[]"
4 | },
5 | {
6 | "name":"com.sun.crypto.provider.AESWrapCipher$General",
7 | "methods":[{"name":"","parameterTypes":[] }]
8 | },
9 | {
10 | "name":"com.sun.crypto.provider.HmacCore$HmacSHA256",
11 | "methods":[{"name":"","parameterTypes":[] }]
12 | },
13 | {
14 | "name":"java.lang.reflect.AccessibleObject",
15 | "fields":[{"name":"override"}]
16 | },
17 | {
18 | "name":"java.security.MessageDigestSpi"
19 | },
20 | {
21 | "name":"java.security.SecureRandomParameters"
22 | },
23 | {
24 | "name":"org.cryptomator.cryptolib.common.MasterkeyFile",
25 | "allDeclaredFields":true,
26 | "methods":[{"name":"","parameterTypes":[] }]
27 | },
28 | {
29 | "name":"sun.misc.Unsafe",
30 | "fields":[{"name":"theUnsafe"}],
31 | "methods":[
32 | {"name":"objectFieldOffset","parameterTypes":["java.lang.reflect.Field"] },
33 | {"name":"putBoolean","parameterTypes":["java.lang.Object","long","boolean"] }
34 | ]
35 | },
36 | {
37 | "name":"sun.security.provider.NativePRNG",
38 | "methods":[{"name":"","parameterTypes":[] }]
39 | },
40 | {
41 | "name":"sun.security.provider.SHA2$SHA256",
42 | "methods":[
43 | {"name":"","parameterTypes":[] }
44 | ]
45 | }
46 | ]
--------------------------------------------------------------------------------
/src/main/resources/META-INF/services/org.cryptomator.cryptolib.api.CryptorProvider:
--------------------------------------------------------------------------------
1 | org.cryptomator.cryptolib.v1.CryptorProviderImpl
2 | org.cryptomator.cryptolib.v2.CryptorProviderImpl
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/api/CryptoLibIntegrationTest.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.api;
10 |
11 | import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel;
12 | import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel;
13 | import org.cryptomator.cryptolib.common.SecureRandomMock;
14 | import org.cryptomator.cryptolib.common.SeekableByteChannelMock;
15 | import org.hamcrest.CoreMatchers;
16 | import org.hamcrest.MatcherAssert;
17 | import org.junit.jupiter.api.Assertions;
18 | import org.junit.jupiter.api.Assumptions;
19 | import org.junit.jupiter.params.ParameterizedTest;
20 | import org.junit.jupiter.params.provider.MethodSource;
21 |
22 | import java.io.IOException;
23 | import java.nio.ByteBuffer;
24 | import java.nio.channels.ReadableByteChannel;
25 | import java.nio.channels.WritableByteChannel;
26 | import java.security.SecureRandom;
27 | import java.util.Arrays;
28 | import java.util.stream.Stream;
29 |
30 | public class CryptoLibIntegrationTest {
31 |
32 | private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM;
33 |
34 | private static Stream getCryptors() {
35 | return Stream.of(
36 | CryptorProvider.forScheme(CryptorProvider.Scheme.SIV_CTRMAC).provide(Masterkey.generate(RANDOM_MOCK), RANDOM_MOCK),
37 | CryptorProvider.forScheme(CryptorProvider.Scheme.SIV_GCM).provide(Masterkey.generate(RANDOM_MOCK), RANDOM_MOCK)
38 | );
39 | }
40 |
41 | @ParameterizedTest
42 | @MethodSource("getCryptors")
43 | public void testDecryptEncrypted(Cryptor cryptor) throws IOException {
44 | int size = 1 * 1024 * 1024;
45 | ByteBuffer ciphertextBuffer = ByteBuffer.allocate(2 * size);
46 |
47 | ByteBuffer cleartext = ByteBuffer.allocate(size);
48 | try (WritableByteChannel ch = new EncryptingWritableByteChannel(new SeekableByteChannelMock(ciphertextBuffer), cryptor)) {
49 | int written = ch.write(cleartext);
50 | Assertions.assertEquals(size, written);
51 | }
52 |
53 | ciphertextBuffer.flip();
54 |
55 | ByteBuffer result = ByteBuffer.allocate(size + 1);
56 | try (ReadableByteChannel ch = new DecryptingReadableByteChannel(new SeekableByteChannelMock(ciphertextBuffer), cryptor, true)) {
57 | int read = ch.read(result);
58 | Assertions.assertEquals(size, read);
59 | }
60 |
61 | Assertions.assertArrayEquals(cleartext.array(), Arrays.copyOfRange(result.array(), 0, size));
62 | }
63 |
64 | @ParameterizedTest
65 | @MethodSource("getCryptors")
66 | public void testDecryptManipulatedEncrypted(Cryptor cryptor) throws IOException {
67 | int size = 1 * 1024 * 1024;
68 | ByteBuffer ciphertextBuffer = ByteBuffer.allocate(2 * size);
69 |
70 | ByteBuffer cleartext = ByteBuffer.allocate(size);
71 | try (WritableByteChannel ch = new EncryptingWritableByteChannel(new SeekableByteChannelMock(ciphertextBuffer), cryptor)) {
72 | int written = ch.write(cleartext);
73 | Assertions.assertEquals(size, written);
74 | }
75 |
76 | ciphertextBuffer.position(0);
77 | int firstByteOfFirstChunk = cryptor.fileHeaderCryptor().headerSize() + 1; // not inside chunk MAC
78 | ciphertextBuffer.put(firstByteOfFirstChunk, (byte) ~ciphertextBuffer.get(firstByteOfFirstChunk));
79 |
80 | ByteBuffer result = ByteBuffer.allocate(size + 1);
81 | try (ReadableByteChannel ch = new DecryptingReadableByteChannel(new SeekableByteChannelMock(ciphertextBuffer), cryptor, true)) {
82 | IOException thrown = Assertions.assertThrows(IOException.class, () -> {
83 | ch.read(result);
84 | });
85 | MatcherAssert.assertThat(thrown.getCause(), CoreMatchers.instanceOf(AuthenticationFailedException.class));
86 | }
87 | }
88 |
89 | @ParameterizedTest
90 | @MethodSource("getCryptors")
91 | public void testDecryptManipulatedEncryptedSkipAuth(Cryptor cryptor) throws IOException {
92 | Assumptions.assumeTrue(cryptor.fileContentCryptor().canSkipAuthentication(), "cryptor doesn't support decryption of unauthentic ciphertext");
93 | int size = 1 * 1024 * 1024;
94 | ByteBuffer ciphertextBuffer = ByteBuffer.allocate(2 * size);
95 |
96 | ByteBuffer cleartext = ByteBuffer.allocate(size);
97 | try (WritableByteChannel ch = new EncryptingWritableByteChannel(new SeekableByteChannelMock(ciphertextBuffer), cryptor)) {
98 | int written = ch.write(cleartext);
99 | Assertions.assertEquals(size, written);
100 | }
101 |
102 | ciphertextBuffer.flip();
103 | int lastByteOfFirstChunk = cryptor.fileHeaderCryptor().headerSize() + cryptor.fileContentCryptor().ciphertextChunkSize() - 1; // inside chunk MAC
104 | ciphertextBuffer.put(lastByteOfFirstChunk, (byte) ~ciphertextBuffer.get(lastByteOfFirstChunk));
105 |
106 | ByteBuffer result = ByteBuffer.allocate(size + 1);
107 | try (ReadableByteChannel ch = new DecryptingReadableByteChannel(new SeekableByteChannelMock(ciphertextBuffer), cryptor, false)) {
108 | int read = ch.read(result);
109 | Assertions.assertEquals(size, read);
110 | }
111 |
112 | Assertions.assertArrayEquals(cleartext.array(), Arrays.copyOfRange(result.array(), 0, size));
113 | }
114 |
115 | }
116 |
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/api/CryptorProviderTest.java:
--------------------------------------------------------------------------------
1 | package org.cryptomator.cryptolib.api;
2 |
3 | import org.junit.jupiter.api.Assertions;
4 | import org.junit.jupiter.api.DisplayName;
5 | import org.junit.jupiter.params.ParameterizedTest;
6 | import org.junit.jupiter.params.provider.EnumSource;
7 |
8 | public class CryptorProviderTest {
9 |
10 | @DisplayName("CryptorProvider.forScheme(...)")
11 | @ParameterizedTest
12 | @EnumSource(CryptorProvider.Scheme.class)
13 | public void testForScheme(CryptorProvider.Scheme scheme) {
14 | CryptorProvider provider = CryptorProvider.forScheme(scheme);
15 | Assertions.assertNotNull(provider);
16 | }
17 |
18 | }
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/api/FileContentCryptorTest.java:
--------------------------------------------------------------------------------
1 | package org.cryptomator.cryptolib.api;
2 |
3 | import org.junit.jupiter.api.Assertions;
4 | import org.junit.jupiter.api.BeforeEach;
5 | import org.junit.jupiter.params.ParameterizedTest;
6 | import org.junit.jupiter.params.provider.CsvSource;
7 | import org.junit.jupiter.params.provider.ValueSource;
8 | import org.mockito.Mockito;
9 |
10 | public class FileContentCryptorTest {
11 |
12 | private final FileContentCryptor contentCryptor = Mockito.mock(FileContentCryptor.class);
13 |
14 | @BeforeEach
15 | public void setup() {
16 | Mockito.when(contentCryptor.cleartextChunkSize()).thenReturn(32);
17 | Mockito.when(contentCryptor.ciphertextChunkSize()).thenReturn(40);
18 | Mockito.doCallRealMethod().when(contentCryptor).cleartextSize(Mockito.anyLong());
19 | Mockito.doCallRealMethod().when(contentCryptor).ciphertextSize(Mockito.anyLong());
20 | }
21 |
22 | @ParameterizedTest(name = "cleartextSize({1}) == {0}")
23 | @CsvSource(value = {
24 | "0,0",
25 | "1,9",
26 | "31,39",
27 | "32,40",
28 | "33,49",
29 | "34,50",
30 | "63,79",
31 | "64,80",
32 | "65,89"
33 | })
34 | public void testCleartextSize(int cleartextSize, int ciphertextSize) {
35 | Assertions.assertEquals(cleartextSize, contentCryptor.cleartextSize(ciphertextSize));
36 | }
37 |
38 | @ParameterizedTest(name = "cleartextSize({0}) == undefined")
39 | @ValueSource(ints = {-1, 1, 8, 41, 48, 81, 88})
40 | public void testCleartextSizeWithInvalidCiphertextSize(int invalidCiphertextSize) {
41 | Assertions.assertThrows(IllegalArgumentException.class, () -> {
42 | contentCryptor.cleartextSize(invalidCiphertextSize);
43 | });
44 | }
45 |
46 | @ParameterizedTest(name = "ciphertextSize({0}) == {1}")
47 | @CsvSource(value = {
48 | "0,0",
49 | "1,9",
50 | "31,39",
51 | "32,40",
52 | "33,49",
53 | "34,50",
54 | "63,79",
55 | "64,80",
56 | "65,89"
57 | })
58 | public void testCiphertextSize(int cleartextSize, int ciphertextSize) {
59 | Assertions.assertEquals(ciphertextSize, contentCryptor.ciphertextSize(cleartextSize));
60 | }
61 |
62 | @ParameterizedTest(name = "ciphertextSize({0}) == undefined")
63 | @ValueSource(ints = {-1})
64 | public void testCiphertextSizewithInvalidCleartextSize(int invalidCleartextSize) {
65 | Assertions.assertThrows(IllegalArgumentException.class, () -> {
66 | contentCryptor.ciphertextSize(invalidCleartextSize);
67 | });
68 | }
69 |
70 | }
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/common/AesKeyWrapTest.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.common;
10 |
11 | import org.junit.jupiter.api.Assertions;
12 | import org.junit.jupiter.api.Test;
13 |
14 | import javax.crypto.Cipher;
15 | import javax.crypto.SecretKey;
16 | import javax.crypto.spec.SecretKeySpec;
17 | import java.security.InvalidKeyException;
18 |
19 | public class AesKeyWrapTest {
20 |
21 | @Test
22 | public void wrapAndUnwrap() throws InvalidKeyException {
23 | DestroyableSecretKey kek = new DestroyableSecretKey(new byte[32], "AES");
24 | SecretKey key = new SecretKeySpec(new byte[32], "AES");
25 | byte[] wrapped = AesKeyWrap.wrap(kek, key);
26 | SecretKey unwrapped = AesKeyWrap.unwrap(kek, wrapped, "AES");
27 | Assertions.assertEquals(key, unwrapped);
28 | }
29 |
30 | @Test
31 | public void wrapWithInvalidKey() {
32 | DestroyableSecretKey kek = new DestroyableSecretKey(new byte[32], "AES");
33 | SecretKey key = new SecretKeySpec(new byte[17], "AES");
34 | Assertions.assertThrows(IllegalArgumentException.class, () -> {
35 | AesKeyWrap.wrap(kek, key);
36 | });
37 | }
38 |
39 | @Test
40 | public void unwrapWithInvalidKey() {
41 | DestroyableSecretKey kek = new DestroyableSecretKey(new byte[32], "AES");
42 | SecretKey key = new SecretKeySpec(new byte[32], "AES");
43 | byte[] wrapped = AesKeyWrap.wrap(kek, key);
44 | Assertions.assertThrows(IllegalArgumentException.class, () -> {
45 | AesKeyWrap.unwrap(kek, wrapped, "FOO", Cipher.PRIVATE_KEY);
46 | });
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/common/CipherSupplierTest.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.common;
10 |
11 | import org.hamcrest.CoreMatchers;
12 | import org.hamcrest.MatcherAssert;
13 | import org.junit.jupiter.api.Assertions;
14 | import org.junit.jupiter.api.Test;
15 |
16 | import javax.crypto.SecretKey;
17 | import javax.crypto.spec.IvParameterSpec;
18 | import javax.crypto.spec.RC5ParameterSpec;
19 | import java.security.spec.AlgorithmParameterSpec;
20 |
21 | public class CipherSupplierTest {
22 |
23 | @Test
24 | public void testGetUnknownCipher() {
25 | Assertions.assertThrows(IllegalArgumentException.class, () -> {
26 | new CipherSupplier("doesNotExist");
27 | });
28 | }
29 |
30 | @Test
31 | public void testGetCipherWithInvalidKey() {
32 | CipherSupplier supplier = new CipherSupplier("AES/CBC/PKCS5Padding");
33 | SecretKey key = new DestroyableSecretKey(new byte[13], "AES");
34 | AlgorithmParameterSpec params = new IvParameterSpec(new byte[16]);
35 | IllegalArgumentException exception = Assertions.assertThrows(IllegalArgumentException.class, () -> {
36 | supplier.encryptionCipher(key, params);
37 | });
38 | MatcherAssert.assertThat(exception.getMessage(), CoreMatchers.containsString("Invalid key"));
39 | }
40 |
41 | @Test
42 | public void testGetCipherWithInvalidAlgorithmParam() {
43 | CipherSupplier supplier = new CipherSupplier("AES/CBC/PKCS5Padding");
44 | SecretKey key = new DestroyableSecretKey(new byte[16], "AES");
45 | AlgorithmParameterSpec params = new RC5ParameterSpec(1, 1, 8);
46 | IllegalArgumentException exception = Assertions.assertThrows(IllegalArgumentException.class, () -> {
47 | supplier.encryptionCipher(key, params);
48 | });
49 | MatcherAssert.assertThat(exception.getMessage(), CoreMatchers.containsString("Algorithm parameter not appropriate for"));
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/common/DecryptingReadableByteChannelTest.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.common;
10 |
11 | import org.cryptomator.cryptolib.api.AuthenticationFailedException;
12 | import org.cryptomator.cryptolib.api.Cryptor;
13 | import org.cryptomator.cryptolib.api.FileContentCryptor;
14 | import org.cryptomator.cryptolib.api.FileHeader;
15 | import org.cryptomator.cryptolib.api.FileHeaderCryptor;
16 | import org.junit.jupiter.api.Assertions;
17 | import org.junit.jupiter.api.BeforeEach;
18 | import org.junit.jupiter.api.Test;
19 | import org.mockito.Mockito;
20 |
21 | import java.io.ByteArrayInputStream;
22 | import java.io.IOException;
23 | import java.nio.ByteBuffer;
24 | import java.nio.channels.Channels;
25 | import java.nio.channels.ReadableByteChannel;
26 | import java.util.Arrays;
27 |
28 | import static java.nio.charset.StandardCharsets.UTF_8;
29 |
30 | public class DecryptingReadableByteChannelTest {
31 |
32 | private Cryptor cryptor;
33 | private FileContentCryptor contentCryptor;
34 | private FileHeaderCryptor headerCryptor;
35 | private FileHeader header;
36 |
37 | @BeforeEach
38 | public void setup() throws AuthenticationFailedException {
39 | cryptor = Mockito.mock(Cryptor.class);
40 | contentCryptor = Mockito.mock(FileContentCryptor.class);
41 | headerCryptor = Mockito.mock(FileHeaderCryptor.class);
42 | header = Mockito.mock(FileHeader.class);
43 | Mockito.when(cryptor.fileContentCryptor()).thenReturn(contentCryptor);
44 | Mockito.when(cryptor.fileHeaderCryptor()).thenReturn(headerCryptor);
45 | Mockito.when(contentCryptor.ciphertextChunkSize()).thenReturn(10);
46 | Mockito.when(contentCryptor.cleartextChunkSize()).thenReturn(10);
47 | Mockito.when(headerCryptor.headerSize()).thenReturn(5);
48 | Mockito.when(headerCryptor.decryptHeader(Mockito.any(ByteBuffer.class))).thenReturn(header);
49 | Mockito.when(contentCryptor.decryptChunk(Mockito.any(ByteBuffer.class), Mockito.anyLong(), Mockito.any(FileHeader.class), Mockito.anyBoolean())).thenAnswer(invocation -> {
50 | ByteBuffer input = invocation.getArgument(0);
51 | String inStr = UTF_8.decode(input).toString();
52 | return ByteBuffer.wrap(inStr.toLowerCase().getBytes(UTF_8));
53 | });
54 | }
55 |
56 | @Test
57 | public void testDecryption() throws IOException, AuthenticationFailedException {
58 | ReadableByteChannel src = Channels.newChannel(new ByteArrayInputStream("hhhhhTOPSECRET!TOPSECRET!".getBytes()));
59 | ByteBuffer result = ByteBuffer.allocate(30);
60 | try (DecryptingReadableByteChannel ch = new DecryptingReadableByteChannel(src, cryptor, true)) {
61 | int read1 = ch.read(result);
62 | Assertions.assertEquals(20, read1);
63 | int read2 = ch.read(result);
64 | Assertions.assertEquals(-1, read2);
65 | Assertions.assertArrayEquals("topsecret!topsecret!".getBytes(), Arrays.copyOfRange(result.array(), 0, read1));
66 | }
67 | Mockito.verify(contentCryptor).decryptChunk(Mockito.any(), Mockito.eq(0l), Mockito.eq(header), Mockito.eq(true));
68 | Mockito.verify(contentCryptor).decryptChunk(Mockito.any(), Mockito.eq(1l), Mockito.eq(header), Mockito.eq(true));
69 | }
70 |
71 | @Test
72 | public void testRandomAccessDecryption() throws IOException, AuthenticationFailedException {
73 | ReadableByteChannel src = Channels.newChannel(new ByteArrayInputStream("TOPSECRET!".getBytes()));
74 | ByteBuffer result = ByteBuffer.allocate(30);
75 | try (DecryptingReadableByteChannel ch = new DecryptingReadableByteChannel(src, cryptor, true, header, 1)) {
76 | int read1 = ch.read(result);
77 | Assertions.assertEquals(10, read1);
78 | int read2 = ch.read(result);
79 | Assertions.assertEquals(-1, read2);
80 | Assertions.assertArrayEquals("topsecret!".getBytes(), Arrays.copyOfRange(result.array(), 0, read1));
81 | }
82 | Mockito.verify(contentCryptor).decryptChunk(Mockito.any(), Mockito.eq(1l), Mockito.eq(header), Mockito.eq(true));
83 | }
84 |
85 | }
86 |
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/common/DestroyablesTest.java:
--------------------------------------------------------------------------------
1 | package org.cryptomator.cryptolib.common;
2 |
3 | import org.junit.jupiter.api.Assertions;
4 | import org.junit.jupiter.api.Test;
5 | import org.mockito.Mockito;
6 |
7 | import javax.security.auth.DestroyFailedException;
8 | import javax.security.auth.Destroyable;
9 |
10 | public class DestroyablesTest {
11 |
12 | @Test
13 | public void testDestroySilently() throws DestroyFailedException {
14 | Destroyable destroyable = Mockito.mock(Destroyable.class);
15 |
16 | Assertions.assertDoesNotThrow(() -> {
17 | Destroyables.destroySilently(destroyable);
18 | });
19 |
20 | Mockito.verify(destroyable).destroy();
21 | }
22 |
23 | @Test
24 | public void testDestroySilentlyIgnoresNull() {
25 | Assertions.assertDoesNotThrow(() -> {
26 | Destroyables.destroySilently(null);
27 | });
28 | }
29 |
30 | @Test
31 | public void testDestroySilentlySuppressesException() throws DestroyFailedException {
32 | Destroyable destroyable = Mockito.mock(Destroyable.class);
33 | Mockito.doThrow(new DestroyFailedException()).when(destroyable).destroy();
34 |
35 | Assertions.assertDoesNotThrow(() -> {
36 | Destroyables.destroySilently(destroyable);
37 | });
38 |
39 | Mockito.verify(destroyable).destroy();
40 | }
41 |
42 | }
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/common/EncryptingReadableByteChannelTest.java:
--------------------------------------------------------------------------------
1 | package org.cryptomator.cryptolib.common;
2 |
3 | import org.cryptomator.cryptolib.api.Cryptor;
4 | import org.cryptomator.cryptolib.api.FileContentCryptor;
5 | import org.cryptomator.cryptolib.api.FileHeader;
6 | import org.cryptomator.cryptolib.api.FileHeaderCryptor;
7 | import org.junit.jupiter.api.Assertions;
8 | import org.junit.jupiter.api.BeforeEach;
9 | import org.junit.jupiter.api.Test;
10 | import org.mockito.Mockito;
11 |
12 | import java.io.ByteArrayInputStream;
13 | import java.io.IOException;
14 | import java.nio.ByteBuffer;
15 | import java.nio.channels.Channels;
16 | import java.nio.channels.ReadableByteChannel;
17 | import java.util.Arrays;
18 |
19 | import static java.nio.charset.StandardCharsets.UTF_8;
20 |
21 | class EncryptingReadableByteChannelTest {
22 |
23 | private ByteBuffer dstFile;
24 | private ReadableByteChannel srcFileChannel;
25 | private Cryptor cryptor;
26 | private FileContentCryptor contentCryptor;
27 | private FileHeaderCryptor headerCryptor;
28 | private FileHeader header;
29 |
30 | @BeforeEach
31 | public void setup() {
32 | dstFile = ByteBuffer.allocate(100);
33 | srcFileChannel = new SeekableByteChannelMock(dstFile);
34 | cryptor = Mockito.mock(Cryptor.class);
35 | contentCryptor = Mockito.mock(FileContentCryptor.class);
36 | headerCryptor = Mockito.mock(FileHeaderCryptor.class);
37 | header = Mockito.mock(FileHeader.class);
38 | Mockito.when(cryptor.fileContentCryptor()).thenReturn(contentCryptor);
39 | Mockito.when(cryptor.fileHeaderCryptor()).thenReturn(headerCryptor);
40 | Mockito.when(contentCryptor.cleartextChunkSize()).thenReturn(10);
41 | Mockito.when(headerCryptor.create()).thenReturn(header);
42 | Mockito.when(headerCryptor.encryptHeader(header)).thenReturn(ByteBuffer.wrap("hhhhh".getBytes()));
43 | Mockito.when(contentCryptor.encryptChunk(Mockito.any(ByteBuffer.class), Mockito.anyLong(), Mockito.any(FileHeader.class))).thenAnswer(invocation -> {
44 | ByteBuffer input = invocation.getArgument(0);
45 | String inStr = UTF_8.decode(input).toString();
46 | return ByteBuffer.wrap(inStr.toUpperCase().getBytes(UTF_8));
47 | });
48 | }
49 |
50 | @Test
51 | public void testEncryptionOfEmptyCleartext() throws IOException {
52 | ReadableByteChannel src = Channels.newChannel(new ByteArrayInputStream(new byte[0]));
53 | ByteBuffer result = ByteBuffer.allocate(10);
54 | try (EncryptingReadableByteChannel ch = new EncryptingReadableByteChannel(src, cryptor)) {
55 | int read1 = ch.read(result);
56 | Assertions.assertEquals(5, read1);
57 | int read2 = ch.read(result);
58 | Assertions.assertEquals(-1, read2);
59 | Assertions.assertArrayEquals("hhhhh".getBytes(), Arrays.copyOfRange(result.array(), 0, read1));
60 | }
61 | Mockito.verify(contentCryptor, Mockito.never()).encryptChunk(Mockito.any(), Mockito.anyLong(), Mockito.any());
62 | }
63 |
64 | @Test
65 | public void testEncryptionOfCleartext() throws IOException {
66 | ReadableByteChannel src = Channels.newChannel(new ByteArrayInputStream("hello world 1 hello world 2".getBytes()));
67 | ByteBuffer result = ByteBuffer.allocate(50);
68 | try (EncryptingReadableByteChannel ch = new EncryptingReadableByteChannel(src, cryptor)) {
69 | int read1 = ch.read(result);
70 | Assertions.assertEquals(32, read1);
71 | int read2 = ch.read(result);
72 | Assertions.assertEquals(-1, read2);
73 | Assertions.assertArrayEquals("hhhhhHELLO WORLD 1 HELLO WORLD 2".getBytes(), Arrays.copyOfRange(result.array(), 0, read1));
74 | }
75 | Mockito.verify(contentCryptor).encryptChunk(Mockito.any(), Mockito.eq(0l), Mockito.eq(header));
76 | Mockito.verify(contentCryptor).encryptChunk(Mockito.any(), Mockito.eq(1l), Mockito.eq(header));
77 | }
78 |
79 | }
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/common/EncryptingWritableByteChannelTest.java:
--------------------------------------------------------------------------------
1 | package org.cryptomator.cryptolib.common;
2 |
3 | import org.cryptomator.cryptolib.api.Cryptor;
4 | import org.cryptomator.cryptolib.api.FileContentCryptor;
5 | import org.cryptomator.cryptolib.api.FileHeader;
6 | import org.cryptomator.cryptolib.api.FileHeaderCryptor;
7 | import org.junit.jupiter.api.Assertions;
8 | import org.junit.jupiter.api.BeforeEach;
9 | import org.junit.jupiter.api.Test;
10 | import org.mockito.Mockito;
11 |
12 | import java.io.IOException;
13 | import java.nio.ByteBuffer;
14 | import java.nio.channels.WritableByteChannel;
15 | import java.util.Arrays;
16 |
17 | import static java.nio.charset.StandardCharsets.UTF_8;
18 |
19 | public class EncryptingWritableByteChannelTest {
20 |
21 | private ByteBuffer dstFile;
22 | private WritableByteChannel dstFileChannel;
23 | private Cryptor cryptor;
24 | private FileContentCryptor contentCryptor;
25 | private FileHeaderCryptor headerCryptor;
26 | private FileHeader header;
27 |
28 | @BeforeEach
29 | public void setup() {
30 | dstFile = ByteBuffer.allocate(100);
31 | dstFileChannel = new SeekableByteChannelMock(dstFile);
32 | cryptor = Mockito.mock(Cryptor.class);
33 | contentCryptor = Mockito.mock(FileContentCryptor.class);
34 | headerCryptor = Mockito.mock(FileHeaderCryptor.class);
35 | header = Mockito.mock(FileHeader.class);
36 | Mockito.when(cryptor.fileContentCryptor()).thenReturn(contentCryptor);
37 | Mockito.when(cryptor.fileHeaderCryptor()).thenReturn(headerCryptor);
38 | Mockito.when(contentCryptor.cleartextChunkSize()).thenReturn(10);
39 | Mockito.when(headerCryptor.create()).thenReturn(header);
40 | Mockito.when(headerCryptor.encryptHeader(header)).thenReturn(ByteBuffer.wrap("hhhhh".getBytes()));
41 | Mockito.when(contentCryptor.encryptChunk(Mockito.any(ByteBuffer.class), Mockito.anyLong(), Mockito.any(FileHeader.class))).thenAnswer(invocation -> {
42 | ByteBuffer input = invocation.getArgument(0);
43 | String inStr = UTF_8.decode(input).toString();
44 | String outStr = "<" + inStr.toUpperCase() + ">";
45 | return UTF_8.encode(outStr);
46 | });
47 | }
48 |
49 | @Test
50 | public void testEncryption() throws IOException {
51 | try (EncryptingWritableByteChannel ch = new EncryptingWritableByteChannel(dstFileChannel, cryptor)) {
52 | ch.write(UTF_8.encode("hello world 1"));
53 | ch.write(UTF_8.encode("hello world 2"));
54 | }
55 | dstFile.flip();
56 | Assertions.assertEquals("hhhhh", UTF_8.decode(dstFile).toString());
57 | }
58 |
59 | @Test
60 | public void testEncryptionOfEmptyFile() throws IOException {
61 | try (EncryptingWritableByteChannel ch = new EncryptingWritableByteChannel(dstFileChannel, cryptor)) {
62 | // empty, so write nothing
63 | }
64 | dstFile.flip();
65 | Assertions.assertEquals("hhhhh<>", UTF_8.decode(dstFile).toString());
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/common/GcmTestHelper.java:
--------------------------------------------------------------------------------
1 | package org.cryptomator.cryptolib.common;
2 |
3 | import javax.crypto.Cipher;
4 | import javax.crypto.SecretKey;
5 | import javax.crypto.spec.GCMParameterSpec;
6 | import javax.crypto.spec.SecretKeySpec;
7 | import java.security.InvalidAlgorithmParameterException;
8 | import java.security.InvalidKeyException;
9 | import java.security.Key;
10 | import java.security.spec.AlgorithmParameterSpec;
11 | import java.util.Random;
12 |
13 | public class GcmTestHelper {
14 |
15 | private static final Random RNG = new Random(42L);
16 |
17 | /**
18 | * Java's default GCM implementation has built-in IV-reuse protection. In order to run certain unit tests,
19 | * this needs to be bypassed. The easiest way would be to re-initialize the cipher before running a test.
20 | *
21 | * This method can be used to init a cipher using randomized key-iv-pairs during test setup and avoid
22 | * InvalidAlgorithmParameterExceptions.
23 | *
24 | * @param cipherInitializer The {@link Cipher#init(int, Key, AlgorithmParameterSpec) cipher.init()} or equivalent method
25 | */
26 | public static void reset(CipherInitializer cipherInitializer) {
27 | byte[] keyBytes = new byte[16];
28 | byte[] nonceBytes = new byte[12];
29 | RNG.nextBytes(keyBytes);
30 | RNG.nextBytes(nonceBytes);
31 | SecretKey key = new SecretKeySpec(keyBytes, "AES");
32 | AlgorithmParameterSpec params = new GCMParameterSpec(96, nonceBytes);
33 | try {
34 | cipherInitializer.init(Cipher.ENCRYPT_MODE, key, params);
35 | } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
36 | throw new IllegalArgumentException("Failed to reset cipher");
37 | }
38 | }
39 |
40 | @FunctionalInterface
41 | public interface CipherInitializer {
42 |
43 | void init(int opmode, SecretKey key, AlgorithmParameterSpec params)
44 | throws InvalidKeyException, InvalidAlgorithmParameterException;
45 |
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/common/MacSupplierTest.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.common;
10 |
11 | import org.junit.jupiter.api.Assertions;
12 | import org.junit.jupiter.api.Test;
13 |
14 | import javax.crypto.Mac;
15 | import javax.crypto.SecretKey;
16 | import javax.crypto.spec.SecretKeySpec;
17 | import java.lang.reflect.Method;
18 | import java.security.Key;
19 | import java.security.KeyPairGenerator;
20 | import java.security.NoSuchAlgorithmException;
21 |
22 | public class MacSupplierTest {
23 |
24 | @Test
25 | public void testConstructorWithInvalidDigest() {
26 | Assertions.assertThrows(IllegalArgumentException.class, () -> {
27 | new MacSupplier("FOO3000");
28 | });
29 | }
30 |
31 | @Test
32 | public void testGetMac() {
33 | SecretKey key = new SecretKeySpec(new byte[16], "HmacSHA256");
34 | try (ObjectPool.Lease mac1 = MacSupplier.HMAC_SHA256.keyed(key)) {
35 | Assertions.assertNotNull(mac1);
36 | }
37 |
38 | try (ObjectPool.Lease mac2 = MacSupplier.HMAC_SHA256.keyed(key)) {
39 | Assertions.assertNotNull(mac2);
40 | }
41 | }
42 |
43 | @Test
44 | public void testGetMacWithInvalidKey() throws NoSuchAlgorithmException, ReflectiveOperationException {
45 | Key key = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPrivate();
46 | // invoked via reflection, as we can not cast Key to SecretKey.
47 | Method m = MacSupplier.class.getMethod("keyed", SecretKey.class);
48 | Assertions.assertThrows(IllegalArgumentException.class, () -> {
49 | m.invoke(MacSupplier.HMAC_SHA256, key);
50 | });
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileTest.java:
--------------------------------------------------------------------------------
1 | package org.cryptomator.cryptolib.common;
2 |
3 | import org.hamcrest.CoreMatchers;
4 | import org.hamcrest.MatcherAssert;
5 | import org.junit.jupiter.api.Assertions;
6 | import org.junit.jupiter.api.Test;
7 |
8 | import java.io.IOException;
9 | import java.io.StringReader;
10 | import java.io.StringWriter;
11 |
12 | public class MasterkeyFileTest {
13 |
14 | @Test
15 | public void testRead() throws IOException {
16 | MasterkeyFile masterkeyFile = MasterkeyFile.read(new StringReader("{\"scryptSalt\": \"Zm9v\"}"));
17 |
18 | Assertions.assertArrayEquals("foo".getBytes(), masterkeyFile.scryptSalt);
19 | }
20 |
21 | @Test
22 | public void testWrite() throws IOException {
23 | MasterkeyFile masterkeyFile = new MasterkeyFile();
24 | masterkeyFile.scryptSalt = "foo".getBytes();
25 |
26 | StringWriter jsonWriter = new StringWriter();
27 | masterkeyFile.write(jsonWriter);
28 | String json = jsonWriter.toString();
29 |
30 | MatcherAssert.assertThat(json, CoreMatchers.containsString("\"scryptSalt\": \"Zm9v\""));
31 | }
32 |
33 | }
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java:
--------------------------------------------------------------------------------
1 | package org.cryptomator.cryptolib.common;
2 |
3 | import org.cryptomator.cryptolib.api.Masterkey;
4 | import org.junit.jupiter.api.Assertions;
5 | import org.junit.jupiter.api.BeforeEach;
6 | import org.junit.jupiter.api.Test;
7 | import org.mockito.Mockito;
8 |
9 | import javax.crypto.SecretKey;
10 | import java.security.SecureRandom;
11 | import java.util.Arrays;
12 |
13 | public class MasterkeyTest {
14 |
15 | private byte[] raw;
16 | private Masterkey masterkey;
17 |
18 | @BeforeEach
19 | public void setup() {
20 | raw = new byte[64];
21 | for (byte b=0; b {
21 | new MessageDigestSupplier("FOO3000");
22 | });
23 | }
24 |
25 | @Test
26 | public void testGetSha1() {
27 | try (ObjectPool.Lease digest = MessageDigestSupplier.SHA1.instance()) {
28 | Assertions.assertNotNull(digest);
29 | }
30 |
31 | try (ObjectPool.Lease digest = MessageDigestSupplier.SHA1.instance()) {
32 | Assertions.assertNotNull(digest);
33 | }
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/common/ObjectPoolTest.java:
--------------------------------------------------------------------------------
1 | package org.cryptomator.cryptolib.common;
2 |
3 | import org.junit.jupiter.api.Assertions;
4 | import org.junit.jupiter.api.BeforeEach;
5 | import org.junit.jupiter.api.DisplayName;
6 | import org.junit.jupiter.api.Test;
7 | import org.mockito.Mockito;
8 |
9 | import java.util.function.Supplier;
10 |
11 | public class ObjectPoolTest {
12 |
13 | private Supplier factory = Mockito.mock(Supplier.class);
14 | private ObjectPool pool = new ObjectPool<>(factory);
15 |
16 | @BeforeEach
17 | public void setup() {
18 | Mockito.doAnswer(invocation -> new Foo()).when(factory).get();
19 | }
20 |
21 | @Test
22 | @DisplayName("new instance is created if pool is empty")
23 | public void testCreateNewObjWhenPoolIsEmpty() {
24 | try (ObjectPool.Lease lease1 = pool.get()) {
25 | try (ObjectPool.Lease lease2 = pool.get()) {
26 | Assertions.assertNotSame(lease1.get(), lease2.get());
27 | }
28 | }
29 | Mockito.verify(factory, Mockito.times(2)).get();
30 | }
31 |
32 | @Test
33 | @DisplayName("recycle existing instance")
34 | public void testRecycleExistingObj() {
35 | Foo foo1;
36 | try (ObjectPool.Lease lease = pool.get()) {
37 | foo1 = lease.get();
38 | }
39 | try (ObjectPool.Lease lease = pool.get()) {
40 | Assertions.assertSame(foo1, lease.get());
41 | }
42 | Mockito.verify(factory, Mockito.times(1)).get();
43 | }
44 |
45 | @Test
46 | @DisplayName("create new instance when pool is GC'ed")
47 | public void testGc() {
48 | try (ObjectPool.Lease lease = pool.get()) {
49 | Assertions.assertNotNull(lease.get());
50 | }
51 | System.gc(); // seems to be reliable on Temurin 17 with @RepeatedTest(1000)
52 | try (ObjectPool.Lease lease = pool.get()) {
53 | Assertions.assertNotNull(lease.get());
54 | }
55 | Mockito.verify(factory, Mockito.times(2)).get();
56 | }
57 |
58 | private static class Foo {
59 | }
60 |
61 | }
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/common/P384KeyPairTest.java:
--------------------------------------------------------------------------------
1 | package org.cryptomator.cryptolib.common;
2 |
3 | import org.junit.jupiter.api.Assertions;
4 | import org.junit.jupiter.api.BeforeEach;
5 | import org.junit.jupiter.api.DisplayName;
6 | import org.junit.jupiter.api.Nested;
7 | import org.junit.jupiter.api.Test;
8 | import org.junit.jupiter.api.io.TempDir;
9 |
10 | import java.io.IOException;
11 | import java.nio.file.Path;
12 | import java.security.spec.InvalidKeySpecException;
13 | import java.security.spec.PKCS8EncodedKeySpec;
14 | import java.security.spec.X509EncodedKeySpec;
15 | import java.util.Base64;
16 |
17 | public class P384KeyPairTest {
18 |
19 | @Test
20 | @DisplayName("generate()")
21 | public void testGenerate() {
22 | P384KeyPair keyPair1 = P384KeyPair.generate();
23 | P384KeyPair keyPair2 = P384KeyPair.generate();
24 | Assertions.assertNotNull(keyPair1);
25 | Assertions.assertNotNull(keyPair2);
26 | Assertions.assertNotEquals(keyPair1, keyPair2);
27 | }
28 |
29 | @Test
30 | @DisplayName("create()")
31 | public void testCreate() throws InvalidKeySpecException {
32 | X509EncodedKeySpec publicKey = new X509EncodedKeySpec(Base64.getDecoder().decode("MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAERxQR+NRN6Wga01370uBBzr2NHDbKIC56tPUEq2HX64RhITGhii8Zzbkb1HnRmdF0aq6uqmUy4jUhuxnKxsv59A6JeK7Unn+mpmm3pQAygjoGc9wrvoH4HWJSQYUlsXDu"));
33 | PKCS8EncodedKeySpec privateKey = new PKCS8EncodedKeySpec(Base64.getDecoder().decode("ME8CAQAwEAYHKoZIzj0CAQYFK4EEACIEODA2AgEBBDEA6QybmBitf94veD5aCLr7nlkF5EZpaXHCfq1AXm57AKQyGOjTDAF9EQB28fMywTDQ"));
34 |
35 | P384KeyPair keyPair = P384KeyPair.create(publicKey, privateKey);
36 | Assertions.assertNotNull(keyPair);
37 | }
38 |
39 | @Test
40 | @DisplayName("store(...)")
41 | public void testStore(@TempDir Path tmpDir) {
42 | Path p12File = tmpDir.resolve("test.p12");
43 | P384KeyPair keyPair = P384KeyPair.generate();
44 |
45 | Assertions.assertDoesNotThrow(() -> {
46 | keyPair.store(p12File, "topsecret".toCharArray());
47 | });
48 | }
49 |
50 | @Nested
51 | @DisplayName("With stored PKCS12 file...")
52 | public class WithStored {
53 |
54 | private P384KeyPair origKeyPair;
55 | private Path p12File;
56 |
57 | @BeforeEach
58 | public void setup(@TempDir Path tmpDir) throws IOException {
59 | this.origKeyPair = P384KeyPair.generate();
60 | this.p12File = tmpDir.resolve("test.p12");
61 | origKeyPair.store(p12File, "topsecret".toCharArray());
62 | }
63 |
64 | @Test
65 | @DisplayName("load(...) with invalid passphrase")
66 | public void testLoadWithInvalidPassphrase() {
67 | char[] wrongPassphrase = "bottompublic".toCharArray();
68 | Assertions.assertThrows(Pkcs12PasswordException.class, () -> {
69 | P384KeyPair.load(p12File, wrongPassphrase);
70 | });
71 | }
72 |
73 | @Test
74 | @DisplayName("load(...) with valid passphrase")
75 | public void testLoadWithValidPassphrase() throws IOException {
76 | P384KeyPair keyPair = P384KeyPair.load(p12File, "topsecret".toCharArray());
77 | Assertions.assertEquals(origKeyPair, keyPair);
78 | }
79 |
80 | }
81 |
82 | }
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/common/Pkcs12HelperTest.java:
--------------------------------------------------------------------------------
1 | package org.cryptomator.cryptolib.common;
2 |
3 | import org.junit.jupiter.api.Assertions;
4 | import org.junit.jupiter.api.BeforeEach;
5 | import org.junit.jupiter.api.DisplayName;
6 | import org.junit.jupiter.api.Nested;
7 | import org.junit.jupiter.api.Test;
8 | import org.junit.jupiter.api.io.TempDir;
9 |
10 | import java.io.IOException;
11 | import java.io.InputStream;
12 | import java.io.OutputStream;
13 | import java.nio.file.Files;
14 | import java.nio.file.Path;
15 | import java.nio.file.StandardOpenOption;
16 | import java.security.InvalidAlgorithmParameterException;
17 | import java.security.KeyPair;
18 | import java.security.KeyPairGenerator;
19 | import java.security.NoSuchAlgorithmException;
20 | import java.security.spec.ECGenParameterSpec;
21 |
22 | public class Pkcs12HelperTest {
23 |
24 | private Path p12File;
25 |
26 | @BeforeEach
27 | public void setup(@TempDir Path tmpDir) throws NoSuchAlgorithmException {
28 | this.p12File = tmpDir.resolve("test.p12");
29 | }
30 |
31 | @Test
32 | @DisplayName("attempt export RSA key pair with EC signature alg")
33 | public void testExportWithInappropriateSignatureAlg() throws NoSuchAlgorithmException, IOException {
34 | KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
35 | try (OutputStream out = Files.newOutputStream(p12File, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)) {
36 | char[] passphrase = "topsecret".toCharArray();
37 | Assertions.assertThrows(Pkcs12Exception.class, () -> {
38 | Pkcs12Helper.exportTo(keyPair, out, passphrase, "SHA256withECDSA");
39 | });
40 | }
41 | }
42 |
43 | @Test
44 | @DisplayName("attempt export EC key pair with EC signature alg")
45 | public void testExport() throws NoSuchAlgorithmException, IOException {
46 | KeyPair keyPair = KeyPairGenerator.getInstance("EC").generateKeyPair();
47 | try (OutputStream out = Files.newOutputStream(p12File, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)) {
48 | char[] passphrase = "topsecret".toCharArray();
49 | Assertions.assertDoesNotThrow(() -> {
50 | Pkcs12Helper.exportTo(keyPair, out, passphrase, "SHA256withECDSA");
51 | });
52 | }
53 | }
54 |
55 | @Nested
56 | @DisplayName("With exported PKCS12 file...")
57 | public class WithExported {
58 |
59 | private KeyPair keyPair;
60 | private char[] passphrase = "topsecret".toCharArray();
61 |
62 | @BeforeEach
63 | public void setup() throws NoSuchAlgorithmException, IOException, InvalidAlgorithmParameterException {
64 | KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("EC");
65 | keyPairGen.initialize(new ECGenParameterSpec("secp384r1"));
66 | this.keyPair = keyPairGen.generateKeyPair();
67 | try (OutputStream out = Files.newOutputStream(p12File, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)) {
68 | Pkcs12Helper.exportTo(keyPair, out, passphrase, "SHA384withECDSA");
69 | }
70 | }
71 |
72 | @Test
73 | @DisplayName("attempt import with invalid passphrase")
74 | public void testImportWithInvalidPassphrase() throws IOException {
75 | try (InputStream in = Files.newInputStream(p12File, StandardOpenOption.READ)) {
76 | char[] wrongPassphrase = "bottompublic".toCharArray();
77 | Assertions.assertThrows(Pkcs12PasswordException.class, () -> {
78 | Pkcs12Helper.importFrom(in, wrongPassphrase);
79 | });
80 | }
81 | }
82 |
83 | @Test
84 | @DisplayName("attempt import with valid passphrase")
85 | public void testImportWithValidPassphrase() throws IOException {
86 | try (InputStream in = Files.newInputStream(p12File, StandardOpenOption.READ)) {
87 | KeyPair imported = Pkcs12Helper.importFrom(in, passphrase);
88 | Assertions.assertEquals(keyPair.getPublic().getAlgorithm(), imported.getPublic().getAlgorithm());
89 | Assertions.assertArrayEquals(keyPair.getPublic().getEncoded(), imported.getPublic().getEncoded());
90 | }
91 | }
92 |
93 | }
94 |
95 | }
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/common/PooledSuppliersBenchmarkTest.java:
--------------------------------------------------------------------------------
1 | package org.cryptomator.cryptolib.common;
2 |
3 | import org.junit.jupiter.api.Disabled;
4 | import org.junit.jupiter.api.Test;
5 | import org.openjdk.jmh.annotations.Benchmark;
6 | import org.openjdk.jmh.annotations.BenchmarkMode;
7 | import org.openjdk.jmh.annotations.Level;
8 | import org.openjdk.jmh.annotations.Measurement;
9 | import org.openjdk.jmh.annotations.Mode;
10 | import org.openjdk.jmh.annotations.OutputTimeUnit;
11 | import org.openjdk.jmh.annotations.Scope;
12 | import org.openjdk.jmh.annotations.Setup;
13 | import org.openjdk.jmh.annotations.State;
14 | import org.openjdk.jmh.annotations.Warmup;
15 | import org.openjdk.jmh.infra.Blackhole;
16 | import org.openjdk.jmh.runner.Runner;
17 | import org.openjdk.jmh.runner.RunnerException;
18 | import org.openjdk.jmh.runner.options.Options;
19 | import org.openjdk.jmh.runner.options.OptionsBuilder;
20 |
21 | import javax.crypto.Cipher;
22 | import javax.crypto.Mac;
23 | import javax.crypto.SecretKey;
24 | import javax.crypto.spec.GCMParameterSpec;
25 | import javax.crypto.spec.SecretKeySpec;
26 | import java.security.MessageDigest;
27 | import java.util.Random;
28 | import java.util.concurrent.TimeUnit;
29 |
30 | @State(Scope.Thread)
31 | @Warmup(iterations = 2)
32 | @Measurement(iterations = 2)
33 | @BenchmarkMode(value = {Mode.AverageTime})
34 | @OutputTimeUnit(TimeUnit.MICROSECONDS)
35 | public class PooledSuppliersBenchmarkTest {
36 |
37 | private static final Random RNG = new Random(42);
38 | private static final CipherSupplier CIPHER_SUPPLIER = CipherSupplier.AES_GCM;
39 | private static final MessageDigestSupplier MD_SUPPLIER = MessageDigestSupplier.SHA256;
40 | private static final MacSupplier MAC_SUPPLIER = MacSupplier.HMAC_SHA256;
41 | private SecretKey key;
42 | private GCMParameterSpec gcmParams;
43 |
44 | @Disabled("only on demand")
45 | @Test
46 | public void runBenchmarks() throws RunnerException {
47 | Options opt = new OptionsBuilder() //
48 | .include(getClass().getName()) //
49 | .threads(2).forks(1) //
50 | .shouldFailOnError(true).shouldDoGC(true) //
51 | .build();
52 | new Runner(opt).run();
53 | }
54 |
55 | @Setup(Level.Invocation)
56 | public void shuffleData() {
57 | byte[] bytes = new byte[28];
58 | RNG.nextBytes(bytes);
59 | this.key = new SecretKeySpec(bytes, 0, 16, "AES");
60 | this.gcmParams = new GCMParameterSpec(128, bytes, 16, 12);
61 | }
62 |
63 | @Benchmark
64 | public void createCipher(Blackhole blackHole) {
65 | blackHole.consume(CIPHER_SUPPLIER.forEncryption(key, gcmParams));
66 | }
67 |
68 | @Benchmark
69 | public void recycleCipher(Blackhole blackHole) {
70 | try (ObjectPool.Lease lease = CIPHER_SUPPLIER.encryptionCipher(key, gcmParams)) {
71 | blackHole.consume(lease.get());
72 | }
73 | }
74 |
75 | @Benchmark
76 | public void createMac(Blackhole blackHole) {
77 | blackHole.consume(MAC_SUPPLIER.withKey(key));
78 | }
79 |
80 | @Benchmark
81 | public void recycleMac(Blackhole blackHole) {
82 | try (ObjectPool.Lease lease = MAC_SUPPLIER.keyed(key)) {
83 | blackHole.consume(lease.get());
84 | }
85 | }
86 |
87 | @Benchmark
88 | public void createMd(Blackhole blackHole) {
89 | blackHole.consume(MD_SUPPLIER.get());
90 | }
91 |
92 | @Benchmark
93 | public void recycleMd(Blackhole blackHole) {
94 | try (ObjectPool.Lease lease = MD_SUPPLIER.instance()) {
95 | blackHole.consume(lease.get());
96 | }
97 | }
98 |
99 | }
100 |
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/common/ReseedingSecureRandomTest.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.common;
10 |
11 | import org.junit.jupiter.api.BeforeEach;
12 | import org.junit.jupiter.api.Test;
13 | import org.mockito.Mockito;
14 |
15 | import java.security.SecureRandom;
16 |
17 | public class ReseedingSecureRandomTest {
18 |
19 | private SecureRandom seeder, csprng;
20 |
21 | @BeforeEach
22 | public void setup() {
23 | seeder = Mockito.mock(SecureRandom.class);
24 | csprng = Mockito.mock(SecureRandom.class);
25 | Mockito.when(seeder.generateSeed(Mockito.anyInt())).then(invocation -> {
26 | int num = invocation.getArgument(0);
27 | return new byte[num];
28 | });
29 |
30 | }
31 |
32 | @Test
33 | public void testReseedAfterLimitReached() {
34 | SecureRandom rand = new ReseedingSecureRandom(seeder, csprng, 10, 3);
35 | Mockito.verify(seeder, Mockito.never()).generateSeed(3);
36 | rand.nextBytes(new byte[4]);
37 | Mockito.verify(seeder, Mockito.times(1)).generateSeed(3);
38 | rand.nextBytes(new byte[4]);
39 | Mockito.verify(seeder, Mockito.times(1)).generateSeed(3);
40 | rand.nextBytes(new byte[4]);
41 | Mockito.verify(seeder, Mockito.times(2)).generateSeed(3);
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/common/ScryptTest.java:
--------------------------------------------------------------------------------
1 | package org.cryptomator.cryptolib.common;
2 |
3 | import org.junit.jupiter.api.Assertions;
4 | import org.junit.jupiter.api.Test;
5 |
6 | import static java.nio.charset.StandardCharsets.US_ASCII;
7 |
8 | /**
9 | * Tests from https://tools.ietf.org/html/rfc7914#section-12
10 | */
11 | public class ScryptTest {
12 |
13 | @Test
14 | public void testEmptyString() {
15 | byte[] key = Scrypt.scrypt("", "".getBytes(US_ASCII), 16, 1, 64);
16 | byte[] expected = new byte[] { //
17 | (byte) 0x77, (byte) 0xd6, (byte) 0x57, (byte) 0x62, (byte) 0x38, (byte) 0x65, (byte) 0x7b, (byte) 0x20, //
18 | (byte) 0x3b, (byte) 0x19, (byte) 0xca, (byte) 0x42, (byte) 0xc1, (byte) 0x8a, (byte) 0x04, (byte) 0x97, //
19 | (byte) 0xf1, (byte) 0x6b, (byte) 0x48, (byte) 0x44, (byte) 0xe3, (byte) 0x07, (byte) 0x4a, (byte) 0xe8, //
20 | (byte) 0xdf, (byte) 0xdf, (byte) 0xfa, (byte) 0x3f, (byte) 0xed, (byte) 0xe2, (byte) 0x14, (byte) 0x42, //
21 | (byte) 0xfc, (byte) 0xd0, (byte) 0x06, (byte) 0x9d, (byte) 0xed, (byte) 0x09, (byte) 0x48, (byte) 0xf8, //
22 | (byte) 0x32, (byte) 0x6a, (byte) 0x75, (byte) 0x3a, (byte) 0x0f, (byte) 0xc8, (byte) 0x1f, (byte) 0x17, //
23 | (byte) 0xe8, (byte) 0xd3, (byte) 0xe0, (byte) 0xfb, (byte) 0x2e, (byte) 0x0d, (byte) 0x36, (byte) 0x28, //
24 | (byte) 0xcf, (byte) 0x35, (byte) 0xe2, (byte) 0x0c, (byte) 0x38, (byte) 0xd1, (byte) 0x89, (byte) 0x06 //
25 | };
26 | Assertions.assertArrayEquals(expected, key);
27 | }
28 |
29 | @Test
30 | public void testPleaseLetMeInString() {
31 | byte[] key = Scrypt.scrypt("pleaseletmein", "SodiumChloride".getBytes(US_ASCII), 16384, 8, 64);
32 | byte[] expected = new byte[] { //
33 | (byte) 0x70, (byte) 0x23, (byte) 0xbd, (byte) 0xcb, (byte) 0x3a, (byte) 0xfd, (byte) 0x73, (byte) 0x48, //
34 | (byte) 0x46, (byte) 0x1c, (byte) 0x06, (byte) 0xcd, (byte) 0x81, (byte) 0xfd, (byte) 0x38, (byte) 0xeb, //
35 | (byte) 0xfd, (byte) 0xa8, (byte) 0xfb, (byte) 0xba, (byte) 0x90, (byte) 0x4f, (byte) 0x8e, (byte) 0x3e, //
36 | (byte) 0xa9, (byte) 0xb5, (byte) 0x43, (byte) 0xf6, (byte) 0x54, (byte) 0x5d, (byte) 0xa1, (byte) 0xf2, //
37 | (byte) 0xd5, (byte) 0x43, (byte) 0x29, (byte) 0x55, (byte) 0x61, (byte) 0x3f, (byte) 0x0f, (byte) 0xcf, //
38 | (byte) 0x62, (byte) 0xd4, (byte) 0x97, (byte) 0x05, (byte) 0x24, (byte) 0x2a, (byte) 0x9a, (byte) 0xf9, //
39 | (byte) 0xe6, (byte) 0x1e, (byte) 0x85, (byte) 0xdc, (byte) 0x0d, (byte) 0x65, (byte) 0x1e, (byte) 0x40, //
40 | (byte) 0xdf, (byte) 0xcf, (byte) 0x01, (byte) 0x7b, (byte) 0x45, (byte) 0x57, (byte) 0x58, (byte) 0x87 //
41 | };
42 | Assertions.assertArrayEquals(expected, key);
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/common/SecureRandomMock.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.common;
10 |
11 | import com.google.common.collect.Iterators;
12 | import com.google.common.primitives.Bytes;
13 |
14 | import java.security.SecureRandom;
15 | import java.util.Arrays;
16 | import java.util.Iterator;
17 | import java.util.Random;
18 |
19 | public class SecureRandomMock extends SecureRandom {
20 |
21 | private static final ByteFiller NULL_FILLER = bytes -> Arrays.fill(bytes, (byte) 0x00);
22 | public static final SecureRandomMock NULL_RANDOM = new SecureRandomMock(NULL_FILLER);
23 | private static final ByteFiller PRNG_FILLER = new ByteFiller() {
24 |
25 | private final Random random = new Random();
26 |
27 | @Override
28 | public void fill(byte[] bytes) {
29 | random.nextBytes(bytes);
30 | }
31 |
32 | };
33 | public static final SecureRandomMock PRNG_RANDOM = new SecureRandomMock(PRNG_FILLER);
34 |
35 | private final ByteFiller byteFiller;
36 |
37 | public SecureRandomMock(ByteFiller byteFiller) {
38 | this.byteFiller = byteFiller;
39 | }
40 |
41 | @Override
42 | public void nextBytes(byte[] bytes) {
43 | byteFiller.fill(bytes);
44 | }
45 |
46 | public static SecureRandomMock cycle(byte... bytes) {
47 | return new SecureRandomMock(new CyclicByteFiller(bytes));
48 | }
49 |
50 | public interface ByteFiller {
51 | void fill(byte[] bytes);
52 | }
53 |
54 | private static class CyclicByteFiller implements ByteFiller {
55 |
56 | private final Iterator source;
57 |
58 | CyclicByteFiller(byte... bytes) {
59 | source = Iterators.cycle(Bytes.asList(bytes));
60 | }
61 |
62 | @Override
63 | public void fill(byte[] bytes) {
64 | Arrays.fill(bytes, source.next());
65 | }
66 |
67 | }
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/common/SeekableByteChannelMock.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.common;
10 |
11 | import java.io.IOException;
12 | import java.nio.ByteBuffer;
13 | import java.nio.channels.SeekableByteChannel;
14 |
15 | public class SeekableByteChannelMock implements SeekableByteChannel {
16 |
17 | boolean open = true;
18 | private final ByteBuffer buf;
19 |
20 | public SeekableByteChannelMock(ByteBuffer buf) {
21 | this.buf = buf;
22 | }
23 |
24 | @Override
25 | public boolean isOpen() {
26 | return open;
27 | }
28 |
29 | @Override
30 | public void close() {
31 | open = false;
32 | }
33 |
34 | @Override
35 | public int read(ByteBuffer dst) throws IOException {
36 | if (!buf.hasRemaining()) {
37 | return -1;
38 | } else {
39 | int num = Math.min(buf.remaining(), dst.remaining());
40 | ByteBuffer limitedSrc = buf.asReadOnlyBuffer();
41 | limitedSrc.limit(limitedSrc.position() + num);
42 | dst.put(limitedSrc);
43 | buf.position(limitedSrc.position());
44 | return num;
45 | }
46 | }
47 |
48 | @Override
49 | public int write(ByteBuffer src) {
50 | int num = Math.min(buf.remaining(), src.remaining());
51 | ByteBuffer limitedSrc = src.asReadOnlyBuffer();
52 | limitedSrc.limit(limitedSrc.position() + num);
53 | buf.put(limitedSrc);
54 | return num;
55 | }
56 |
57 | @Override
58 | public long position() {
59 | return buf.position();
60 | }
61 |
62 | @Override
63 | public SeekableByteChannel position(long newPosition) {
64 | assert newPosition < Integer.MAX_VALUE;
65 | buf.position((int) newPosition);
66 | return this;
67 | }
68 |
69 | @Override
70 | public long size() throws IOException {
71 | return buf.limit();
72 | }
73 |
74 | @Override
75 | public SeekableByteChannel truncate(long size) {
76 | assert size < Integer.MAX_VALUE;
77 | if (size < buf.position()) {
78 | buf.position((int) size);
79 | }
80 | buf.limit((int) size);
81 | return this;
82 | }
83 |
84 | }
85 |
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/common/X509CertBuilderTest.java:
--------------------------------------------------------------------------------
1 | package org.cryptomator.cryptolib.common;
2 |
3 | import org.junit.jupiter.api.Assertions;
4 | import org.junit.jupiter.api.BeforeEach;
5 | import org.junit.jupiter.api.DisplayName;
6 | import org.junit.jupiter.api.Nested;
7 | import org.junit.jupiter.api.Test;
8 |
9 | import java.security.KeyPair;
10 | import java.security.KeyPairGenerator;
11 | import java.security.NoSuchAlgorithmException;
12 | import java.security.cert.CertificateException;
13 | import java.security.cert.X509Certificate;
14 | import java.time.Instant;
15 |
16 | public class X509CertBuilderTest {
17 |
18 | @Test
19 | @DisplayName("init() with RSA key and EC signature")
20 | public void testInitWithInvalidKeyPair() throws NoSuchAlgorithmException {
21 | KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
22 | String signingAlg = "SHA256withECDSA";
23 |
24 | Assertions.assertThrows(IllegalArgumentException.class, () -> {
25 | X509CertBuilder.init(keyPair, signingAlg);
26 | });
27 | }
28 |
29 | @Test
30 | @DisplayName("init() with RSA key and RSA signature")
31 | public void testInitWithRSAKeyPair() throws NoSuchAlgorithmException {
32 | KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
33 | String signingAlg = "SHA256withRSA";
34 |
35 | Assertions.assertDoesNotThrow(() -> {
36 | X509CertBuilder.init(keyPair, signingAlg);
37 | });
38 | }
39 |
40 | @Test
41 | @DisplayName("init() with EC key and EC signature")
42 | public void testInitWithECKeyPair() throws NoSuchAlgorithmException {
43 | KeyPair keyPair = KeyPairGenerator.getInstance("EC").generateKeyPair();
44 | String signingAlg = "SHA256withECDSA";
45 |
46 | Assertions.assertDoesNotThrow(() -> {
47 | X509CertBuilder.init(keyPair, signingAlg);
48 | });
49 | }
50 |
51 | @Nested
52 | @DisplayName("With initialized builder...")
53 | public class WithInitialized {
54 |
55 | private KeyPair keyPair;
56 | private X509CertBuilder builder;
57 |
58 | @BeforeEach
59 | public void setup() throws NoSuchAlgorithmException {
60 | this.keyPair = KeyPairGenerator.getInstance("EC").generateKeyPair();
61 | this.builder = X509CertBuilder.init(keyPair, "SHA256withECDSA");
62 | }
63 |
64 | @Test
65 | @DisplayName("set invalid issuer")
66 | public void testWithInvalidIssuer() {
67 | Assertions.assertThrows(IllegalArgumentException.class, () -> {
68 | builder.withIssuer("Test");
69 | });
70 | }
71 |
72 | @Test
73 | @DisplayName("set invalid subject")
74 | public void testWithInvalidSubject() {
75 | Assertions.assertThrows(IllegalArgumentException.class, () -> {
76 | builder.withSubject("Test");
77 | });
78 | }
79 |
80 | @Test
81 | @DisplayName("build() with missing issuer")
82 | public void testBuildWithMissingIssuer() {
83 | builder.withSubject("CN=Test") //
84 | .withNotBefore(Instant.now()) //
85 | .withNotAfter(Instant.now().minusSeconds(3600));
86 | Assertions.assertThrows(IllegalStateException.class, () -> {
87 | builder.build();
88 | });
89 | }
90 |
91 | @Test
92 | @DisplayName("build() with missing subject")
93 | public void testBuildWithMissingSubject() {
94 | builder.withIssuer("CN=Test") //
95 | .withNotBefore(Instant.now()) //
96 | .withNotAfter(Instant.now().minusSeconds(3600));
97 | Assertions.assertThrows(IllegalStateException.class, () -> {
98 | builder.build();
99 | });
100 | }
101 |
102 | @Test
103 | @DisplayName("build() with missing notBefore")
104 | public void testBuildWithMissingNotBefore() {
105 | builder.withIssuer("CN=Test") //
106 | .withSubject("CN=Test") //
107 | .withNotAfter(Instant.now().minusSeconds(3600));
108 | Assertions.assertThrows(IllegalStateException.class, () -> {
109 | builder.build();
110 | });
111 | }
112 |
113 | @Test
114 | @DisplayName("build() with missing notAfter")
115 | public void testBuildWithMissingNotAfter() {
116 | builder.withIssuer("CN=Test") //
117 | .withSubject("CN=Test") //
118 | .withNotBefore(Instant.now());
119 | Assertions.assertThrows(IllegalStateException.class, () -> {
120 | builder.build();
121 | });
122 | }
123 |
124 | @Test
125 | @DisplayName("build() with invalid notAfter")
126 | public void testBuildWithInvalidNotAfter() {
127 | builder.withIssuer("CN=Test") //
128 | .withSubject("CN=Test") //
129 | .withNotBefore(Instant.now()) //
130 | .withNotAfter(Instant.now().minusSeconds(1));
131 | Assertions.assertThrows(IllegalStateException.class, () -> {
132 | builder.build();
133 | });
134 | }
135 |
136 | @Test
137 | @DisplayName("build() with all params set")
138 | public void testBuild() throws CertificateException {
139 | X509Certificate cert = builder //
140 | .withIssuer("CN=Test") //
141 | .withSubject("CN=Test") //
142 | .withNotBefore(Instant.now()) //
143 | .withNotAfter(Instant.now().plusSeconds(3600)) //
144 | .build();
145 |
146 | Assertions.assertNotNull(cert);
147 | Assertions.assertDoesNotThrow(() -> {
148 | cert.verify(keyPair.getPublic());
149 | });
150 | Assertions.assertDoesNotThrow(() -> {
151 | cert.checkValidity();
152 | });
153 | }
154 |
155 | }
156 |
157 | }
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/v1/BenchmarkTest.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.v1;
10 |
11 | import org.junit.jupiter.api.Disabled;
12 | import org.junit.jupiter.api.Test;
13 | import org.openjdk.jmh.runner.Runner;
14 | import org.openjdk.jmh.runner.RunnerException;
15 | import org.openjdk.jmh.runner.options.Options;
16 | import org.openjdk.jmh.runner.options.OptionsBuilder;
17 |
18 | public class BenchmarkTest {
19 |
20 | @Disabled("only on demand")
21 | @Test
22 | public void runBenchmarks() throws RunnerException {
23 | // Taken from http://stackoverflow.com/a/30486197/4014509:
24 | Options opt = new OptionsBuilder()
25 | // Specify which benchmarks to run
26 | .include(getClass().getPackage().getName() + ".*Benchmark.*")
27 | // Set the following options as needed
28 | .threads(2).forks(1) //
29 | .shouldFailOnError(true).shouldDoGC(true)
30 | // .jvmArgs("-XX:+UnlockDiagnosticVMOptions", "-XX:+PrintInlining")
31 | // .addProfiler(WinPerfAsmProfiler.class)
32 | .build();
33 |
34 | new Runner(opt).run();
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/v1/CryptorImplTest.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.v1;
10 |
11 | import org.cryptomator.cryptolib.api.Masterkey;
12 | import org.cryptomator.cryptolib.common.SecureRandomMock;
13 | import org.hamcrest.CoreMatchers;
14 | import org.hamcrest.MatcherAssert;
15 | import org.junit.jupiter.api.Assertions;
16 | import org.junit.jupiter.api.BeforeEach;
17 | import org.junit.jupiter.api.Test;
18 | import org.mockito.Mockito;
19 |
20 | import java.security.SecureRandom;
21 |
22 | public class CryptorImplTest {
23 |
24 | private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM;
25 |
26 | private Masterkey masterkey;
27 |
28 | @BeforeEach
29 | public void setup() {
30 | this.masterkey = new Masterkey(new byte[64]);
31 | }
32 |
33 | @Test
34 | public void testGetFileContentCryptor() {
35 | try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) {
36 | MatcherAssert.assertThat(cryptor.fileContentCryptor(), CoreMatchers.instanceOf(FileContentCryptorImpl.class));
37 | }
38 | }
39 |
40 | @Test
41 | public void testGetFileHeaderCryptor() {
42 | try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) {
43 | MatcherAssert.assertThat(cryptor.fileHeaderCryptor(), CoreMatchers.instanceOf(FileHeaderCryptorImpl.class));
44 | }
45 | }
46 |
47 | @Test
48 | public void testGetFileNameCryptor() {
49 | try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) {
50 | MatcherAssert.assertThat(cryptor.fileNameCryptor(), CoreMatchers.instanceOf(FileNameCryptorImpl.class));
51 | }
52 | }
53 |
54 | @Test
55 | public void testExplicitDestruction() {
56 | Masterkey masterkey = Mockito.mock(Masterkey.class);
57 | try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) {
58 | cryptor.destroy();
59 | Mockito.verify(masterkey).destroy();
60 | Mockito.when(masterkey.isDestroyed()).thenReturn(true);
61 | Assertions.assertTrue(cryptor.isDestroyed());
62 | }
63 | }
64 |
65 | @Test
66 | public void testImplicitDestruction() {
67 | Masterkey masterkey = Mockito.mock(Masterkey.class);
68 | try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) {
69 | Assertions.assertFalse(cryptor.isDestroyed());
70 | }
71 | Mockito.verify(masterkey).destroy();
72 | }
73 |
74 | }
75 |
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/v1/CryptorProviderImplTest.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.v1;
10 |
11 | import org.cryptomator.cryptolib.api.Masterkey;
12 | import org.cryptomator.cryptolib.common.SecureRandomMock;
13 | import org.junit.jupiter.api.Assertions;
14 | import org.junit.jupiter.api.Test;
15 | import org.mockito.Mockito;
16 |
17 | import java.security.SecureRandom;
18 |
19 | public class CryptorProviderImplTest {
20 |
21 | private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM;
22 |
23 | @Test
24 | public void testProvide() {
25 | Masterkey masterkey = Mockito.mock(Masterkey.class);
26 | CryptorImpl cryptor = new CryptorProviderImpl().provide(masterkey, RANDOM_MOCK);
27 | Assertions.assertNotNull(cryptor);
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplBenchmark.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.v1;
10 |
11 | import java.nio.ByteBuffer;
12 | import java.security.SecureRandom;
13 | import java.util.concurrent.TimeUnit;
14 |
15 | import javax.crypto.SecretKey;
16 | import javax.crypto.spec.SecretKeySpec;
17 |
18 | import org.cryptomator.cryptolib.api.Masterkey;
19 | import org.cryptomator.cryptolib.common.DestroyableSecretKey;
20 | import org.cryptomator.cryptolib.common.SecureRandomMock;
21 | import org.openjdk.jmh.annotations.Benchmark;
22 | import org.openjdk.jmh.annotations.BenchmarkMode;
23 | import org.openjdk.jmh.annotations.Level;
24 | import org.openjdk.jmh.annotations.Measurement;
25 | import org.openjdk.jmh.annotations.Mode;
26 | import org.openjdk.jmh.annotations.OutputTimeUnit;
27 | import org.openjdk.jmh.annotations.Scope;
28 | import org.openjdk.jmh.annotations.Setup;
29 | import org.openjdk.jmh.annotations.State;
30 | import org.openjdk.jmh.annotations.Warmup;
31 |
32 | /**
33 | * Needs to be compiled via maven as the JMH annotation processor needs to do stuff...
34 | */
35 | @State(Scope.Thread)
36 | @Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
37 | @Measurement(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS)
38 | @BenchmarkMode(value = {Mode.AverageTime})
39 | @OutputTimeUnit(TimeUnit.MICROSECONDS)
40 | public class FileContentCryptorImplBenchmark {
41 |
42 | private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM;
43 | private static final Masterkey MASTERKEY = new Masterkey(new byte[64]);;
44 | private final byte[] headerNonce = new byte[Constants.NONCE_SIZE];
45 | private final ByteBuffer cleartextChunk = ByteBuffer.allocate(Constants.PAYLOAD_SIZE);
46 | private final ByteBuffer ciphertextChunk = ByteBuffer.allocate(Constants.CHUNK_SIZE);
47 | private final FileContentCryptorImpl fileContentCryptor = new FileContentCryptorImpl(MASTERKEY, RANDOM_MOCK);
48 | private long chunkNumber;
49 |
50 | @Setup(Level.Invocation)
51 | public void shuffleData() {
52 | chunkNumber = RANDOM_MOCK.nextLong();
53 | cleartextChunk.rewind();
54 | ciphertextChunk.rewind();
55 | RANDOM_MOCK.nextBytes(headerNonce);
56 | RANDOM_MOCK.nextBytes(cleartextChunk.array());
57 | RANDOM_MOCK.nextBytes(ciphertextChunk.array());
58 | }
59 |
60 | @Benchmark
61 | public void benchmarkEncryption() {
62 | fileContentCryptor.encryptChunk(cleartextChunk, ciphertextChunk, chunkNumber, headerNonce, MASTERKEY.getEncKey());
63 | }
64 |
65 | @Benchmark
66 | public void benchmarkAuthentication() {
67 | fileContentCryptor.checkChunkMac(headerNonce, chunkNumber, ciphertextChunk);
68 | }
69 |
70 | @Benchmark
71 | public void benchmarkDecryption() {
72 | fileContentCryptor.decryptChunk(ciphertextChunk, ciphertextChunk, MASTERKEY.getEncKey());
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/v1/FileContentEncryptorBenchmark.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.v1;
10 |
11 | import java.io.IOException;
12 | import java.nio.ByteBuffer;
13 | import java.nio.channels.SeekableByteChannel;
14 | import java.nio.channels.WritableByteChannel;
15 | import java.security.SecureRandom;
16 | import java.util.concurrent.TimeUnit;
17 |
18 | import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel;
19 | import org.cryptomator.cryptolib.api.Masterkey;
20 | import org.cryptomator.cryptolib.common.SecureRandomMock;
21 | import org.openjdk.jmh.annotations.Benchmark;
22 | import org.openjdk.jmh.annotations.BenchmarkMode;
23 | import org.openjdk.jmh.annotations.Level;
24 | import org.openjdk.jmh.annotations.Measurement;
25 | import org.openjdk.jmh.annotations.Mode;
26 | import org.openjdk.jmh.annotations.OutputTimeUnit;
27 | import org.openjdk.jmh.annotations.Scope;
28 | import org.openjdk.jmh.annotations.Setup;
29 | import org.openjdk.jmh.annotations.State;
30 | import org.openjdk.jmh.annotations.Warmup;
31 |
32 | /**
33 | * Needs to be compiled via maven as the JMH annotation processor needs to do stuff...
34 | */
35 | @State(Scope.Thread)
36 | @Warmup(iterations = 2)
37 | @Measurement(iterations = 2)
38 | @BenchmarkMode(value = {Mode.SingleShotTime})
39 | @OutputTimeUnit(TimeUnit.MILLISECONDS)
40 | public class FileContentEncryptorBenchmark {
41 |
42 | private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM;
43 | private static final Masterkey MASTERKEY = new Masterkey(new byte[64]);
44 |
45 | private CryptorImpl cryptor;
46 |
47 | @Setup(Level.Iteration)
48 | public void shuffleData() {
49 | cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK);
50 | }
51 |
52 | @Benchmark
53 | public void benchmark100MegabytesEncryption() throws IOException {
54 | ByteBuffer megabyte = ByteBuffer.allocate(1 * 1024 * 1024);
55 | try (WritableByteChannel ch = new EncryptingWritableByteChannel(new NullSeekableByteChannel(), cryptor)) {
56 | for (int i = 0; i < 100; i++) {
57 | ch.write(megabyte);
58 | megabyte.clear();
59 | }
60 | }
61 | }
62 |
63 | @Benchmark
64 | public void benchmark10MegabytesEncryption() throws IOException {
65 | ByteBuffer megabyte = ByteBuffer.allocate(1 * 1024 * 1024);
66 | try (WritableByteChannel ch = new EncryptingWritableByteChannel(new NullSeekableByteChannel(), cryptor)) {
67 | for (int i = 0; i < 10; i++) {
68 | ch.write(megabyte);
69 | megabyte.clear();
70 | }
71 | }
72 | }
73 |
74 | @Benchmark
75 | public void benchmark1MegabytesEncryption() throws IOException {
76 | ByteBuffer megabyte = ByteBuffer.allocate(1 * 1024 * 1024);
77 | try (WritableByteChannel ch = new EncryptingWritableByteChannel(new NullSeekableByteChannel(), cryptor)) {
78 | ch.write(megabyte);
79 | megabyte.clear();
80 | }
81 | }
82 |
83 | private static class NullSeekableByteChannel implements SeekableByteChannel {
84 |
85 | boolean open;
86 |
87 | @Override
88 | public boolean isOpen() {
89 | return open;
90 | }
91 |
92 | @Override
93 | public void close() throws IOException {
94 | open = false;
95 | }
96 |
97 | @Override
98 | public int read(ByteBuffer dst) throws IOException {
99 | throw new UnsupportedOperationException();
100 | }
101 |
102 | @Override
103 | public int write(ByteBuffer src) throws IOException {
104 | int delta = src.remaining();
105 | src.position(src.position() + delta);
106 | return delta;
107 | }
108 |
109 | @Override
110 | public long position() throws IOException {
111 | return 0;
112 | }
113 |
114 | @Override
115 | public SeekableByteChannel position(long newPosition) throws IOException {
116 | return this;
117 | }
118 |
119 | @Override
120 | public long size() throws IOException {
121 | return 0;
122 | }
123 |
124 | @Override
125 | public SeekableByteChannel truncate(long size) throws IOException {
126 | return this;
127 | }
128 |
129 | }
130 |
131 | }
132 |
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorBenchmark.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.v1;
10 |
11 | import org.cryptomator.cryptolib.api.AuthenticationFailedException;
12 | import org.cryptomator.cryptolib.api.FileHeader;
13 | import org.cryptomator.cryptolib.api.Masterkey;
14 | import org.cryptomator.cryptolib.common.DestroyableSecretKey;
15 | import org.cryptomator.cryptolib.common.SecureRandomMock;
16 | import org.openjdk.jmh.annotations.Benchmark;
17 | import org.openjdk.jmh.annotations.BenchmarkMode;
18 | import org.openjdk.jmh.annotations.Level;
19 | import org.openjdk.jmh.annotations.Measurement;
20 | import org.openjdk.jmh.annotations.Mode;
21 | import org.openjdk.jmh.annotations.OutputTimeUnit;
22 | import org.openjdk.jmh.annotations.Scope;
23 | import org.openjdk.jmh.annotations.Setup;
24 | import org.openjdk.jmh.annotations.State;
25 | import org.openjdk.jmh.annotations.Warmup;
26 |
27 | import java.nio.ByteBuffer;
28 | import java.security.SecureRandom;
29 | import java.util.concurrent.TimeUnit;
30 |
31 | /**
32 | * Needs to be compiled via maven as the JMH annotation processor needs to do stuff...
33 | */
34 | @State(Scope.Thread)
35 | @Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
36 | @Measurement(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS)
37 | @BenchmarkMode(value = {Mode.AverageTime})
38 | @OutputTimeUnit(TimeUnit.MICROSECONDS)
39 | public class FileHeaderCryptorBenchmark {
40 |
41 | private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM;
42 | private static final Masterkey MASTERKEY = new Masterkey(new byte[64]);
43 | private static final FileHeaderCryptorImpl HEADER_CRYPTOR = new FileHeaderCryptorImpl(MASTERKEY, RANDOM_MOCK);
44 | private FileHeader header;
45 | private ByteBuffer validHeaderCiphertextBuf;
46 |
47 | @Setup(Level.Iteration)
48 | public void shuffleData() {
49 | header = HEADER_CRYPTOR.create();
50 | validHeaderCiphertextBuf = HEADER_CRYPTOR.encryptHeader(header);
51 | }
52 |
53 | @Benchmark
54 | public void benchmarkEncryption() {
55 | HEADER_CRYPTOR.encryptHeader(header);
56 | }
57 |
58 | @Benchmark
59 | public void benchmarkDecryption() throws AuthenticationFailedException {
60 | HEADER_CRYPTOR.decryptHeader(validHeaderCiphertextBuf);
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImplTest.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.v1;
10 |
11 | import com.google.common.io.BaseEncoding;
12 | import org.cryptomator.cryptolib.api.AuthenticationFailedException;
13 | import org.cryptomator.cryptolib.api.FileHeader;
14 | import org.cryptomator.cryptolib.api.Masterkey;
15 | import org.cryptomator.cryptolib.common.SecureRandomMock;
16 | import org.junit.jupiter.api.Assertions;
17 | import org.junit.jupiter.api.BeforeEach;
18 | import org.junit.jupiter.api.Test;
19 |
20 | import java.nio.ByteBuffer;
21 | import java.security.SecureRandom;
22 |
23 | public class FileHeaderCryptorImplTest {
24 |
25 | private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM;
26 | private FileHeaderCryptorImpl headerCryptor;
27 |
28 | @BeforeEach
29 | public void setup() {
30 | Masterkey masterkey = new Masterkey(new byte[64]);
31 | headerCryptor = new FileHeaderCryptorImpl(masterkey, RANDOM_MOCK);
32 | }
33 |
34 | @Test
35 | public void testEncryption() {
36 | // set nonce to: AAAAAAAAAAAAAAAAAAAAAA==
37 | // set payload to: //////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
38 | FileHeaderImpl.Payload payload = new FileHeaderImpl.Payload(-1, new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN]);
39 | FileHeader header = new FileHeaderImpl(new byte[FileHeaderImpl.NONCE_LEN], payload);
40 | // encrypt payload:
41 | // echo -n "//////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" | base64 --decode \
42 | // | openssl enc -aes-256-ctr -K 0000000000000000000000000000000000000000000000000000000000000000 -iv 00000000000000000000000000000000 | base64
43 | // -> I2o/h12/dnatSKIUkoQgh1MPivvHRTa5qWO08cTLc4vOp0A9TWBrbg==
44 |
45 | // mac nonce + encrypted payload:
46 | // (openssl dgst -sha256 -mac HMAC -macopt hexkey:0000000000000000000000000000000000000000000000000000000000000000 -binary)
47 |
48 | // concat nonce + encrypted payload + mac:
49 | final String expected = "AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg==";
50 |
51 | ByteBuffer result = headerCryptor.encryptHeader(header);
52 |
53 | Assertions.assertArrayEquals(BaseEncoding.base64().decode(expected), result.array());
54 | }
55 |
56 | @Test
57 | public void testHeaderSize() {
58 | Assertions.assertEquals(FileHeaderImpl.SIZE, headerCryptor.headerSize());
59 | Assertions.assertEquals(FileHeaderImpl.SIZE, headerCryptor.encryptHeader(headerCryptor.create()).limit());
60 | }
61 |
62 | @Test
63 | @SuppressWarnings("deprecation")
64 | public void testDecryption() throws AuthenticationFailedException {
65 | byte[] ciphertext = BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAAAAAACNqP4ddv3Z2rUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga24VjC86+zlHN49BfMdzvHF3f9EE0LSnRLSsu6ps3IRcJg==");
66 | FileHeader header = headerCryptor.decryptHeader(ByteBuffer.wrap(ciphertext));
67 | Assertions.assertEquals(header.getReserved(), -1l);
68 | }
69 |
70 | @Test
71 | public void testDecryptionWithTooShortHeader() {
72 | ByteBuffer ciphertext = ByteBuffer.allocate(7);
73 |
74 | Assertions.assertThrows(IllegalArgumentException.class, () -> {
75 | headerCryptor.decryptHeader(ciphertext);
76 | });
77 | }
78 |
79 | @Test
80 | public void testDecryptionWithInvalidMac1() {
81 | ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJa=="));
82 |
83 | Assertions.assertThrows(AuthenticationFailedException.class, () -> {
84 | headerCryptor.decryptHeader(ciphertext);
85 | });
86 | }
87 |
88 | @Test
89 | public void testDecryptionWithInvalidMac2() {
90 | ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("aAAAAAAAAAAAAAAAAAAAANyVwHiiQImjrUiiFJKEIIdTD4r7x0U2ualjtPHEy3OLzqdAPU1ga26lJzstK9RUv1hj5zDC4wC9FgMfoVE1mD0HnuENuYXkJA=="));
91 |
92 | Assertions.assertThrows(AuthenticationFailedException.class, () -> {
93 | headerCryptor.decryptHeader(ciphertext);
94 | });
95 | }
96 |
97 | }
98 |
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderImplTest.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.v1;
10 |
11 | import org.junit.jupiter.api.Assertions;
12 | import org.junit.jupiter.api.Test;
13 |
14 | import java.util.Arrays;
15 |
16 | public class FileHeaderImplTest {
17 |
18 | @Test
19 | public void testConstructionFailsWithInvalidNonceSize() {
20 | FileHeaderImpl.Payload payload = new FileHeaderImpl.Payload(-1, new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN]);
21 | Assertions.assertThrows(IllegalArgumentException.class, () -> {
22 | new FileHeaderImpl(new byte[3], payload);
23 | });
24 | }
25 |
26 | @Test
27 | public void testConstructionFailsWithInvalidKeySize() {
28 | Assertions.assertThrows(IllegalArgumentException.class, () -> {
29 | new FileHeaderImpl.Payload(-1, new byte[3]);
30 | });
31 | }
32 |
33 | @Test
34 | public void testDestruction() {
35 | byte[] nonNullKey = new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN];
36 | Arrays.fill(nonNullKey, (byte) 0x42);
37 | FileHeaderImpl.Payload payload = new FileHeaderImpl.Payload(-1, nonNullKey);
38 | FileHeaderImpl header = new FileHeaderImpl(new byte[FileHeaderImpl.NONCE_LEN], payload);
39 | Assertions.assertFalse(header.isDestroyed());
40 | header.destroy();
41 | Assertions.assertTrue(header.isDestroyed());
42 | Assertions.assertTrue(payload.isDestroyed());
43 | Assertions.assertTrue(payload.getContentKey().isDestroyed());
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/v2/BenchmarkTest.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.v2;
10 |
11 | import org.junit.jupiter.api.Disabled;
12 | import org.junit.jupiter.api.Test;
13 | import org.openjdk.jmh.runner.Runner;
14 | import org.openjdk.jmh.runner.RunnerException;
15 | import org.openjdk.jmh.runner.options.Options;
16 | import org.openjdk.jmh.runner.options.OptionsBuilder;
17 |
18 | public class BenchmarkTest {
19 |
20 | @Disabled("only on demand")
21 | @Test
22 | public void runBenchmarks() throws RunnerException {
23 | // Taken from http://stackoverflow.com/a/30486197/4014509:
24 | Options opt = new OptionsBuilder()
25 | // Specify which benchmarks to run
26 | .include(getClass().getPackage().getName() + ".*Benchmark.*")
27 | // Set the following options as needed
28 | .threads(2).forks(1) //
29 | .shouldFailOnError(true).shouldDoGC(true)
30 | // .jvmArgs("-XX:+UnlockDiagnosticVMOptions", "-XX:+PrintInlining")
31 | // .addProfiler(WinPerfAsmProfiler.class)
32 | .build();
33 |
34 | new Runner(opt).run();
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/v2/CryptorImplTest.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.v2;
10 |
11 | import org.cryptomator.cryptolib.api.Masterkey;
12 | import org.cryptomator.cryptolib.common.SecureRandomMock;
13 | import org.hamcrest.CoreMatchers;
14 | import org.hamcrest.MatcherAssert;
15 | import org.junit.jupiter.api.Assertions;
16 | import org.junit.jupiter.api.BeforeEach;
17 | import org.junit.jupiter.api.Test;
18 | import org.mockito.Mockito;
19 |
20 | import java.security.SecureRandom;
21 |
22 | public class CryptorImplTest {
23 |
24 | private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM;
25 |
26 | private Masterkey masterkey;
27 |
28 | @BeforeEach
29 | public void setup() {
30 | this.masterkey = new Masterkey(new byte[64]);
31 | }
32 |
33 | @Test
34 | public void testGetFileContentCryptor() {
35 | try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) {
36 | MatcherAssert.assertThat(cryptor.fileContentCryptor(), CoreMatchers.instanceOf(FileContentCryptorImpl.class));
37 | }
38 | }
39 |
40 | @Test
41 | public void testGetFileHeaderCryptor() {
42 | try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) {
43 | MatcherAssert.assertThat(cryptor.fileHeaderCryptor(), CoreMatchers.instanceOf(FileHeaderCryptorImpl.class));
44 | }
45 | }
46 |
47 | @Test
48 | public void testGetFileNameCryptor() {
49 | try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) {
50 | MatcherAssert.assertThat(cryptor.fileNameCryptor(), CoreMatchers.instanceOf(FileNameCryptorImpl.class));
51 | }
52 | }
53 |
54 | @Test
55 | public void testExplicitDestruction() {
56 | Masterkey masterkey = Mockito.mock(Masterkey.class);
57 | try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) {
58 | cryptor.destroy();
59 | Mockito.verify(masterkey).destroy();
60 | Mockito.when(masterkey.isDestroyed()).thenReturn(true);
61 | Assertions.assertTrue(cryptor.isDestroyed());
62 | }
63 | }
64 |
65 | @Test
66 | public void testImplicitDestruction() {
67 | Masterkey masterkey = Mockito.mock(Masterkey.class);
68 | try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) {
69 | Assertions.assertFalse(cryptor.isDestroyed());
70 | }
71 | Mockito.verify(masterkey).destroy();
72 | }
73 |
74 | }
75 |
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/v2/CryptorProviderImplTest.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.v2;
10 |
11 | import org.cryptomator.cryptolib.api.Masterkey;
12 | import org.cryptomator.cryptolib.common.SecureRandomMock;
13 | import org.junit.jupiter.api.Assertions;
14 | import org.junit.jupiter.api.BeforeEach;
15 | import org.junit.jupiter.api.Test;
16 | import org.mockito.Mockito;
17 |
18 | import java.security.SecureRandom;
19 |
20 | public class CryptorProviderImplTest {
21 |
22 | private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM;
23 |
24 | @Test
25 | public void testProvide() {
26 | Masterkey masterkey = Mockito.mock(Masterkey.class);
27 | CryptorImpl cryptor = new CryptorProviderImpl().provide(masterkey, RANDOM_MOCK);
28 | Assertions.assertNotNull(cryptor);
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplBenchmark.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.v2;
10 |
11 | import org.cryptomator.cryptolib.api.AuthenticationFailedException;
12 | import org.cryptomator.cryptolib.common.DestroyableSecretKey;
13 | import org.cryptomator.cryptolib.common.SecureRandomMock;
14 | import org.openjdk.jmh.annotations.Benchmark;
15 | import org.openjdk.jmh.annotations.BenchmarkMode;
16 | import org.openjdk.jmh.annotations.Level;
17 | import org.openjdk.jmh.annotations.Measurement;
18 | import org.openjdk.jmh.annotations.Mode;
19 | import org.openjdk.jmh.annotations.OutputTimeUnit;
20 | import org.openjdk.jmh.annotations.Scope;
21 | import org.openjdk.jmh.annotations.Setup;
22 | import org.openjdk.jmh.annotations.State;
23 | import org.openjdk.jmh.annotations.Warmup;
24 |
25 | import java.nio.ByteBuffer;
26 | import java.security.SecureRandom;
27 | import java.util.concurrent.TimeUnit;
28 |
29 | /**
30 | * Needs to be compiled via maven as the JMH annotation processor needs to do stuff...
31 | */
32 | @State(Scope.Thread)
33 | @Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
34 | @Measurement(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS)
35 | @BenchmarkMode(value = {Mode.AverageTime})
36 | @OutputTimeUnit(TimeUnit.MICROSECONDS)
37 | public class FileContentCryptorImplBenchmark {
38 |
39 | private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM;
40 | private static final DestroyableSecretKey ENC_KEY = new DestroyableSecretKey(new byte[16], "AES");
41 | private final byte[] headerNonce = new byte[FileHeaderImpl.NONCE_LEN];
42 | private final ByteBuffer cleartextChunk = ByteBuffer.allocate(Constants.PAYLOAD_SIZE);
43 | private final ByteBuffer ciphertextChunk = ByteBuffer.allocate(Constants.CHUNK_SIZE);
44 | private final FileContentCryptorImpl fileContentCryptor = new FileContentCryptorImpl(RANDOM_MOCK);
45 | private long chunkNumber;
46 |
47 | @Setup(Level.Trial)
48 | public void prepareData() {
49 | cleartextChunk.rewind();
50 | fileContentCryptor.encryptChunk(cleartextChunk, ciphertextChunk, 0l, new byte[12], ENC_KEY);
51 | ciphertextChunk.flip();
52 | }
53 |
54 | @Setup(Level.Invocation)
55 | public void shuffleData() {
56 | chunkNumber = RANDOM_MOCK.nextLong();
57 | cleartextChunk.rewind();
58 | ciphertextChunk.rewind();
59 | RANDOM_MOCK.nextBytes(headerNonce);
60 | RANDOM_MOCK.nextBytes(cleartextChunk.array());
61 | }
62 |
63 | @Benchmark
64 | public void benchmarkEncryption() {
65 | fileContentCryptor.encryptChunk(cleartextChunk, ciphertextChunk, chunkNumber, headerNonce, ENC_KEY);
66 | }
67 |
68 | @Benchmark
69 | public void benchmarkDecryption() throws AuthenticationFailedException {
70 | fileContentCryptor.decryptChunk(ciphertextChunk, cleartextChunk, 0l, new byte[12], ENC_KEY);
71 | }
72 |
73 | }
74 |
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/v2/FileContentEncryptorBenchmark.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.v2;
10 |
11 | import java.io.IOException;
12 | import java.nio.ByteBuffer;
13 | import java.nio.channels.SeekableByteChannel;
14 | import java.nio.channels.WritableByteChannel;
15 | import java.security.SecureRandom;
16 | import java.util.concurrent.TimeUnit;
17 |
18 | import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel;
19 | import org.cryptomator.cryptolib.api.Masterkey;
20 | import org.cryptomator.cryptolib.common.SecureRandomMock;
21 | import org.openjdk.jmh.annotations.Benchmark;
22 | import org.openjdk.jmh.annotations.BenchmarkMode;
23 | import org.openjdk.jmh.annotations.Level;
24 | import org.openjdk.jmh.annotations.Measurement;
25 | import org.openjdk.jmh.annotations.Mode;
26 | import org.openjdk.jmh.annotations.OutputTimeUnit;
27 | import org.openjdk.jmh.annotations.Scope;
28 | import org.openjdk.jmh.annotations.Setup;
29 | import org.openjdk.jmh.annotations.State;
30 | import org.openjdk.jmh.annotations.Warmup;
31 |
32 | /**
33 | * Needs to be compiled via maven as the JMH annotation processor needs to do stuff...
34 | */
35 | @State(Scope.Thread)
36 | @Warmup(iterations = 2)
37 | @Measurement(iterations = 2)
38 | @BenchmarkMode(value = {Mode.SingleShotTime})
39 | @OutputTimeUnit(TimeUnit.MILLISECONDS)
40 | public class FileContentEncryptorBenchmark {
41 |
42 | private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM;
43 | private static final Masterkey MASTERKEY = new Masterkey(new byte[64]);
44 |
45 | private CryptorImpl cryptor;
46 |
47 | @Setup(Level.Iteration)
48 | public void shuffleData() {
49 | cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK);
50 | }
51 |
52 | @Benchmark
53 | public void benchmark100MegabytesEncryption() throws IOException {
54 | ByteBuffer megabyte = ByteBuffer.allocate(1 * 1024 * 1024);
55 | try (WritableByteChannel ch = new EncryptingWritableByteChannel(new NullSeekableByteChannel(), cryptor)) {
56 | for (int i = 0; i < 100; i++) {
57 | ch.write(megabyte);
58 | megabyte.clear();
59 | }
60 | }
61 | }
62 |
63 | @Benchmark
64 | public void benchmark10MegabytesEncryption() throws IOException {
65 | ByteBuffer megabyte = ByteBuffer.allocate(1 * 1024 * 1024);
66 | try (WritableByteChannel ch = new EncryptingWritableByteChannel(new NullSeekableByteChannel(), cryptor)) {
67 | for (int i = 0; i < 10; i++) {
68 | ch.write(megabyte);
69 | megabyte.clear();
70 | }
71 | }
72 | }
73 |
74 | @Benchmark
75 | public void benchmark1MegabytesEncryption() throws IOException {
76 | ByteBuffer megabyte = ByteBuffer.allocate(1 * 1024 * 1024);
77 | try (WritableByteChannel ch = new EncryptingWritableByteChannel(new NullSeekableByteChannel(), cryptor)) {
78 | ch.write(megabyte);
79 | megabyte.clear();
80 | }
81 | }
82 |
83 | private static class NullSeekableByteChannel implements SeekableByteChannel {
84 |
85 | boolean open;
86 |
87 | @Override
88 | public boolean isOpen() {
89 | return open;
90 | }
91 |
92 | @Override
93 | public void close() {
94 | open = false;
95 | }
96 |
97 | @Override
98 | public int read(ByteBuffer dst) {
99 | throw new UnsupportedOperationException();
100 | }
101 |
102 | @Override
103 | public int write(ByteBuffer src) {
104 | int delta = src.remaining();
105 | src.position(src.position() + delta);
106 | return delta;
107 | }
108 |
109 | @Override
110 | public long position() {
111 | return 0;
112 | }
113 |
114 | @Override
115 | public SeekableByteChannel position(long newPosition) {
116 | return this;
117 | }
118 |
119 | @Override
120 | public long size() {
121 | return 0;
122 | }
123 |
124 | @Override
125 | public SeekableByteChannel truncate(long size) {
126 | return this;
127 | }
128 |
129 | }
130 |
131 | }
132 |
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorBenchmark.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.v2;
10 |
11 | import org.cryptomator.cryptolib.api.AuthenticationFailedException;
12 | import org.cryptomator.cryptolib.api.FileHeader;
13 | import org.cryptomator.cryptolib.api.Masterkey;
14 | import org.cryptomator.cryptolib.common.DestroyableSecretKey;
15 | import org.cryptomator.cryptolib.common.SecureRandomMock;
16 | import org.openjdk.jmh.annotations.Benchmark;
17 | import org.openjdk.jmh.annotations.BenchmarkMode;
18 | import org.openjdk.jmh.annotations.Level;
19 | import org.openjdk.jmh.annotations.Measurement;
20 | import org.openjdk.jmh.annotations.Mode;
21 | import org.openjdk.jmh.annotations.OutputTimeUnit;
22 | import org.openjdk.jmh.annotations.Scope;
23 | import org.openjdk.jmh.annotations.Setup;
24 | import org.openjdk.jmh.annotations.State;
25 | import org.openjdk.jmh.annotations.Warmup;
26 |
27 | import java.nio.ByteBuffer;
28 | import java.security.SecureRandom;
29 | import java.util.concurrent.TimeUnit;
30 |
31 | /**
32 | * Needs to be compiled via maven as the JMH annotation processor needs to do stuff...
33 | */
34 | @State(Scope.Thread)
35 | @Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
36 | @Measurement(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS)
37 | @BenchmarkMode(value = {Mode.AverageTime})
38 | @OutputTimeUnit(TimeUnit.MICROSECONDS)
39 | public class FileHeaderCryptorBenchmark {
40 |
41 | private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM;
42 | private static final Masterkey MASTERKEY = new Masterkey(new byte[64]);
43 | private static final FileHeaderCryptorImpl HEADER_CRYPTOR = new FileHeaderCryptorImpl(MASTERKEY, RANDOM_MOCK);
44 |
45 | private ByteBuffer validHeaderCiphertextBuf;
46 | private FileHeader header;
47 |
48 | @Setup(Level.Iteration)
49 | public void prepareData() {
50 | validHeaderCiphertextBuf = HEADER_CRYPTOR.encryptHeader(HEADER_CRYPTOR.create());
51 | }
52 |
53 | @Setup(Level.Invocation)
54 | public void shuffleData() {
55 | header = HEADER_CRYPTOR.create();
56 | }
57 |
58 | @Benchmark
59 | public void benchmarkEncryption() {
60 | HEADER_CRYPTOR.encryptHeader(header);
61 | }
62 |
63 | @Benchmark
64 | public void benchmarkDecryption() throws AuthenticationFailedException {
65 | HEADER_CRYPTOR.decryptHeader(validHeaderCiphertextBuf);
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.v2;
10 |
11 | import com.google.common.io.BaseEncoding;
12 | import org.cryptomator.cryptolib.api.AuthenticationFailedException;
13 | import org.cryptomator.cryptolib.api.FileHeader;
14 | import org.cryptomator.cryptolib.api.Masterkey;
15 | import org.cryptomator.cryptolib.common.CipherSupplier;
16 | import org.cryptomator.cryptolib.common.GcmTestHelper;
17 | import org.cryptomator.cryptolib.common.ObjectPool;
18 | import org.cryptomator.cryptolib.common.SecureRandomMock;
19 | import org.junit.jupiter.api.Assertions;
20 | import org.junit.jupiter.api.BeforeEach;
21 | import org.junit.jupiter.api.Test;
22 |
23 | import javax.crypto.Cipher;
24 | import java.nio.ByteBuffer;
25 | import java.security.SecureRandom;
26 |
27 | public class FileHeaderCryptorImplTest {
28 |
29 | private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM;
30 |
31 | private FileHeaderCryptorImpl headerCryptor;
32 |
33 | @BeforeEach
34 | public void setup() {
35 | Masterkey masterkey = new Masterkey(new byte[64]);
36 | headerCryptor = new FileHeaderCryptorImpl(masterkey, RANDOM_MOCK);
37 |
38 | // reset cipher state to avoid InvalidAlgorithmParameterExceptions due to IV-reuse
39 | GcmTestHelper.reset((mode, key, params) -> {
40 | try (ObjectPool.Lease cipher = CipherSupplier.AES_GCM.encryptionCipher(key, params)) {
41 | cipher.get();
42 | }
43 | });
44 | }
45 |
46 | @Test
47 | public void testEncryption() {
48 | // set nonce to: AAAAAAAAAAAAAAAA
49 | // set payload to: //////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
50 | FileHeaderImpl.Payload payload = new FileHeaderImpl.Payload(-1, new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN]);
51 | FileHeader header = new FileHeaderImpl(new byte[FileHeaderImpl.NONCE_LEN], payload);
52 | // encrypt payload:
53 | // echo -n "//////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" | base64 --decode \
54 | // | openssl enc -aes-256-gcm -K 0000000000000000000000000000000000000000000000000000000000000000 -iv 00000000000000000000000000000000 -a
55 | // -> MVi/wrKflJEHTsXTuvOdGHJgA8o3pip00aL1jnUGNY7dSrEoTUrhew==
56 |
57 | // the following string contains nonce + ciphertext + tag. The tag is not produced by openssl, though.
58 | final String expected = "AAAAAAAAAAAAAAAAMVi/wrKflJEHTsXTuvOdGHJgA8o3pip00aL1jnUGNY7dSrEoTUrhey+maVG6P0F2RBmZR74SjU0=";
59 |
60 | ByteBuffer result = headerCryptor.encryptHeader(header);
61 |
62 | Assertions.assertArrayEquals(BaseEncoding.base64().decode(expected), result.array());
63 | }
64 |
65 | @Test
66 | public void testHeaderSize() {
67 | Assertions.assertEquals(org.cryptomator.cryptolib.v2.FileHeaderImpl.SIZE, headerCryptor.headerSize());
68 | Assertions.assertEquals(org.cryptomator.cryptolib.v2.FileHeaderImpl.SIZE, headerCryptor.encryptHeader(headerCryptor.create()).limit());
69 | }
70 |
71 | @Test
72 | @SuppressWarnings("deprecation")
73 | public void testDecryption() throws AuthenticationFailedException {
74 | byte[] ciphertext = BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAMVi/wrKflJEHTsXTuvOdGHJgA8o3pip00aL1jnUGNY7dSrEoTUrhey+maVG6P0F2RBmZR74SjU0=");
75 | FileHeader header = headerCryptor.decryptHeader(ByteBuffer.wrap(ciphertext));
76 | Assertions.assertEquals(header.getReserved(), -1l);
77 | }
78 |
79 | @Test
80 | public void testDecryptionWithTooShortHeader() {
81 | ByteBuffer ciphertext = ByteBuffer.allocate(7);
82 |
83 | Assertions.assertThrows(IllegalArgumentException.class, () -> {
84 | headerCryptor.decryptHeader(ciphertext);
85 | });
86 | }
87 |
88 | @Test
89 | public void testDecryptionWithInvalidTag1() {
90 | ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAMVi/wrKflJEHTsXTuvOdGHJgA8o3pip00aL1jnUGNY7dSrEoTUrhey+maVG6P0F2RBmZR74SjUA="));
91 |
92 | Assertions.assertThrows(AuthenticationFailedException.class, () -> {
93 | headerCryptor.decryptHeader(ciphertext);
94 | });
95 | }
96 |
97 | @Test
98 | public void testDecryptionWithInvalidTag2() {
99 | ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("AAAAAAAAAAAAAAAAMVi/wrKflJEHTsXTuvOdGHJgA8o3pip00aL1jnUGNY7dSrEoTUrhey+maVG6P0F2RBmZR74SjUa="));
100 |
101 | Assertions.assertThrows(AuthenticationFailedException.class, () -> {
102 | headerCryptor.decryptHeader(ciphertext);
103 | });
104 | }
105 |
106 | }
107 |
--------------------------------------------------------------------------------
/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderImplTest.java:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright (c) 2016 Sebastian Stenzel and others.
3 | * All rights reserved. This program and the accompanying materials
4 | * are made available under the terms of the accompanying LICENSE.txt.
5 | *
6 | * Contributors:
7 | * Sebastian Stenzel - initial API and implementation
8 | *******************************************************************************/
9 | package org.cryptomator.cryptolib.v2;
10 |
11 | import org.junit.jupiter.api.Assertions;
12 | import org.junit.jupiter.api.Test;
13 |
14 | import java.util.Arrays;
15 |
16 | public class FileHeaderImplTest {
17 |
18 | @Test
19 | public void testConstructionFailsWithInvalidNonceSize() {
20 | FileHeaderImpl.Payload payload = new FileHeaderImpl.Payload(-1, new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN]);
21 | Assertions.assertThrows(IllegalArgumentException.class, () -> {
22 | new FileHeaderImpl(new byte[3], payload);
23 | });
24 | }
25 |
26 | @Test
27 | public void testConstructionFailsWithInvalidKeySize() {
28 | Assertions.assertThrows(IllegalArgumentException.class, () -> {
29 | new FileHeaderImpl.Payload(-1, new byte[3]);
30 | });
31 | }
32 |
33 | @Test
34 | public void testDestruction() {
35 | byte[] nonNullKey = new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN];
36 | Arrays.fill(nonNullKey, (byte) 0x42);
37 | FileHeaderImpl.Payload payload = new FileHeaderImpl.Payload(-1, nonNullKey);
38 | FileHeaderImpl header = new FileHeaderImpl(new byte[FileHeaderImpl.NONCE_LEN], payload);
39 | Assertions.assertFalse(header.isDestroyed());
40 | header.destroy();
41 | Assertions.assertTrue(header.isDestroyed());
42 | Assertions.assertTrue(payload.isDestroyed());
43 | Assertions.assertTrue(payload.getContentKey().isDestroyed());
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker:
--------------------------------------------------------------------------------
1 | mock-maker-inline
--------------------------------------------------------------------------------
/suppression.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 | org\.cryptomator:.*
10 | cpe:/a:cryptomator:cryptomator
11 | CVE-2022-25366
12 |
13 |
14 |
15 |
18 |
19 | ^pkg:maven/com\.google\.guava/guava@.*$
20 | CVE-2020-8908
21 | CVE-2020-8908
22 |
23 |
24 |
--------------------------------------------------------------------------------