diff --git a/src/main/java/com/eatthepath/noise/component/NoiseCipher.java b/src/main/java/com/eatthepath/noise/component/NoiseCipher.java index f179fc2..af51884 100644 --- a/src/main/java/com/eatthepath/noise/component/NoiseCipher.java +++ b/src/main/java/com/eatthepath/noise/component/NoiseCipher.java @@ -4,15 +4,45 @@ import javax.annotation.concurrent.ThreadSafe; import javax.crypto.AEADBadTagException; import javax.crypto.ShortBufferException; -import javax.crypto.spec.SecretKeySpec; import java.nio.ByteBuffer; import java.security.Key; +/** + * A Noise cipher is a stateless object that encrypts and decrypts data for use in a Noise protocol. Noise cipher + * implementations must be thread-safe (i.e. calling encryption/decryption methods on different sets of data + * concurrently and from different threads must have no adverse effect). + *

+ * Noise cipher implementations must operate in AEAD mode, produce a 16-byte AEAD tag when encrypting data, and verify + * a 16-byte AEAD tag when decrypting data. + */ @ThreadSafe public interface NoiseCipher { + /** + * Returns the name of this Noise cipher as it would appear in a full Noise protocol name. + * + * @return the name of this Noise cipher as it would appear in a full Noise protocol name + */ String getName(); + /** + * Encrypts the given plaintext using the given key, nonce, and associated data. This method returns a new byte buffer + * sized exactly to contain the resulting ciphertext and AEAD tag. + *

+ * All {@code plaintext.remaining()} bytes starting at {@code plaintext.position()} are processed. Upon return, the + * plaintext buffer's position will be equal to its limit; its limit will not have changed. If associated data is + * provided, the same is true of the associated data buffer. The returned ciphertext buffer's position will be zero, + * and its limit will be equal to its capacity. + * + * @param key the key with which to encrypt the given plaintext + * @param nonce a nonce, which must be unique for the given key + * @param associatedData the associated data to use when calculating an AEAD tag + * @param plaintext the plaintext to encrypt + * + * @return a new byte buffer containing the resulting ciphertext and AEAD tag + * + * @see #getCiphertextLength(int) + */ default ByteBuffer encrypt(final Key key, final long nonce, @Nullable final ByteBuffer associatedData, @@ -31,6 +61,31 @@ default ByteBuffer encrypt(final Key key, return ciphertext; } + /** + * Encrypts the given plaintext using the given key, nonce, and associated data. Callers are responsible for ensuring + * that the given ciphertext buffer has enough remaining capacity to hold the resulting ciphertext and AEAD tag. + *

+ * All {@code plaintext.remaining()} bytes starting at {@code plaintext.position()} are processed. Upon return, the + * plaintext buffer's position will be equal to its limit; its limit will not have changed. If associated data is + * provided, the same will be true of the associated data buffer. The ciphertext buffer's position will have advanced + * by n, where n is the value returned by this method; the ciphertext buffer's limit will not have changed. + *

+ * Note that the ciphertext and plaintext buffers must be different, but may refer to the same underlying byte array + * to facilitate in-place encryption. + * + * @param key the key with which to encrypt the given plaintext + * @param nonce a nonce, which must be unique for the given key + * @param associatedData the associated data to use when calculating an AEAD tag + * @param plaintext the plaintext to encrypt + * @param ciphertext the buffer into which to write the resulting ciphertext and AEAD tag + * + * @return the number of bytes written into the ciphertext buffer + * + * @throws ShortBufferException if the given ciphertext buffer does not have enough remaining capacity to hold the + * resulting ciphertext and AEAD tag + * + * @see #getCiphertextLength(int) + */ int encrypt(final Key key, final long nonce, @Nullable final ByteBuffer associatedData, @@ -38,6 +93,19 @@ int encrypt(final Key key, final ByteBuffer ciphertext) throws ShortBufferException; + /** + * Encrypts the given plaintext using the given key, nonce, and associated data. This method returns a new byte array + * sized exactly to contain the resulting ciphertext and AEAD tag. + * + * @param key the key with which to encrypt the given plaintext + * @param nonce a nonce, which must be unique for the given key + * @param associatedData the associated data to use when calculating an AEAD tag + * @param plaintext the plaintext to encrypt + * + * @return a new byte array containing the resulting ciphertext and AEAD tag + * + * @see #getCiphertextLength(int) + */ default byte[] encrypt(final Key key, final long nonce, @Nullable final byte[] associatedData, @@ -64,6 +132,36 @@ default byte[] encrypt(final Key key, return ciphertext; } + /** + * Encrypts the given plaintext using the given key, nonce, and associated data. Callers are responsible for ensuring + * that the given ciphertext array is large enough to hold the resulting ciphertext and AEAD tag. + *

+ * Note that the ciphertext and plaintext arrays may refer to the same array, allowing for in-place encryption. + * + * @param key the key with which to encrypt the given plaintext + * @param nonce a nonce, which must be unique for the given key + * @param associatedData a byte array containing the associated data (if any) to be used when encrypting the given + * plaintext; may be {@code null} + * @param aadOffset the position within {@code associatedData} where the associated data starts; ignored if + * {@code associatedData} is {@code null} + * @param aadLength the length of the associated data within {@code associatedData}; ignored if {@code associatedData} + * is {@code null} + * @param plaintext a byte array containing the plaintext to encrypt + * @param plaintextOffset the offset within {@code plaintext} where the plaintext begins + * @param plaintextLength the length of the plaintext within {@code plaintext} + * @param ciphertext a byte array into which to write the ciphertext and AEAD tag from this encryption operation + * @param ciphertextOffset the position within {@code ciphertext} at which to begin writing the ciphertext and AEAD + * tag + * + * @return the number of bytes written into the ciphertext array + * + * @throws ShortBufferException if the ciphertext array (after its offset) is too small to hold the resulting + * ciphertext and AEAD tag + * @throws IndexOutOfBoundsException if the given plaintext length exceeds the length of the plaintext array after its + * offset + * + * @see #getCiphertextLength(int) + */ int encrypt(final Key key, final long nonce, @Nullable final byte[] associatedData, @@ -75,6 +173,27 @@ int encrypt(final Key key, final byte[] ciphertext, final int ciphertextOffset) throws ShortBufferException; + /** + * Decrypts the given ciphertext and verifies its AEAD tag using the given key, nonce, and associated data. This + * method returns a new {@link ByteBuffer} sized exactly to contain the resulting plaintext. The returned buffer's + * position will be zero, and its limit and capacity will be equal to the plaintext length. + *

+ * All {@code ciphertext.remaining()} bytes starting at {@code ciphertext.position()} are processed. Upon return, the + * ciphertext buffer's position will be equal to its limit; its limit will not have changed. If associated data is + * provided, the same will be true of the associated data buffer. + * + * @param key the key with which to decrypt the given ciphertext + * @param nonce a nonce, which must be unique for the given key + * @param associatedData the associated data to use when verifying the AEAD tag; may be {@code null} + * @param ciphertext the ciphertext to decrypt + * + * @return a {@code ByteBuffer} containing the resulting plaintext + * + * @throws AEADBadTagException if the AEAD tag in the given ciphertext does not match the calculated value + * @throws IllegalArgumentException if the given ciphertext is too short to contain a valid AEAD tag + * + * @see #getPlaintextLength(int) + */ default ByteBuffer decrypt(final Key key, final long nonce, @Nullable final ByteBuffer associatedData, @@ -84,6 +203,7 @@ default ByteBuffer decrypt(final Key key, try { decrypt(key, nonce, associatedData, ciphertext, plaintext); + plaintext.rewind(); } catch (final ShortBufferException e) { // This should never happen for a buffer we control throw new AssertionError(e); @@ -92,6 +212,31 @@ default ByteBuffer decrypt(final Key key, return plaintext; } + /** + * Decrypts the given ciphertext and verifies its AEAD tag using the given key, nonce, and associated data. This + * method writes the resulting plaintext into the given {@code plaintext} buffer. Callers are responsible for ensuring + * that the given plaintext buffer has enough remaining capacity to hold the resulting plaintext. + *

+ * All {@code ciphertext.remaining()} bytes starting at {@code ciphertext.position()} are processed. Upon return, the + * ciphertext buffer's position will be equal to its limit; its limit will not have changed. If associated data is + * provided, the same will be true of the associated data buffer. The plaintext buffer's position will have advanced + * by n, where n is the value returned by this method; the plaintext buffer's limit will not have changed. + * + * @param key the key with which to decrypt the given ciphertext + * @param nonce a nonce, which must be unique for the given key + * @param associatedData the associated data to use when verifying the AEAD tag; may be {@code null} + * @param ciphertext the ciphertext to decrypt + * @param plaintext the buffer into which to write the resulting plaintext + * + * @return the number of bytes written into the {@code plaintext} buffer + * + * @throws AEADBadTagException if the AEAD tag in the given ciphertext does not match the calculated value + * @throws IllegalArgumentException if the given ciphertext is too short to contain a valid AEAD tag + * @throws ShortBufferException if the given plaintext buffer does not have enough remaining capacity to hold the + * resulting plaintext + * + * @see #getPlaintextLength(int) + */ int decrypt(final Key key, final long nonce, @Nullable final ByteBuffer associatedData, @@ -99,6 +244,22 @@ int decrypt(final Key key, final ByteBuffer plaintext) throws AEADBadTagException, ShortBufferException; + /** + * Decrypts the given ciphertext and verifies its AEAD tag using the given key, nonce, and associated data. This + * method returns a new byte array sized exactly to contain the resulting plaintext. + * + * @param key the key with which to decrypt the given ciphertext + * @param nonce a nonce, which must be unique for the given key + * @param associatedData the associated data to use when verifying the AEAD tag; may be {@code null} + * @param ciphertext the ciphertext to decrypt + * + * @return a byte array containing the resulting plaintext + * + * @throws AEADBadTagException if the AEAD tag in the given ciphertext does not match the calculated value + * @throws IllegalArgumentException if the given ciphertext is too short to contain a valid AEAD tag + * + * @see #getPlaintextLength(int) + */ default byte[] decrypt(final Key key, final long nonce, @Nullable final byte[] associatedData, @@ -125,6 +286,37 @@ default byte[] decrypt(final Key key, return plaintext; } + /** + * Decrypts the given ciphertext and verifies its AEAD tag. This writes the resulting plaintext into a provided byte + * array. + *

+ * Note that {@code ciphertext} and {@code plaintext} may refer to the same byte array, allowing for in-place + * decryption. + * + * @param key the key with which to decrypt the given plaintext + * @param nonce a nonce, which must be unique for the given key + * @param associatedData a byte array containing the associated data (if any) to be used when verifying the AEAD tag + * for the given ciphertext; may be {@code null} + * @param aadOffset the position within {@code associatedData} where the associated data starts; ignored if + * {@code associatedData} is {@code null} + * @param aadLength the length of the associated data within {@code associatedData}; ignored if {@code associatedData} + * is {@code null} + * @param ciphertext a byte array containing the ciphertext and AEAD tag to be decrypted and verified + * @param ciphertextOffset the position within {@code ciphertext} at which to begin reading the ciphertext and AEAD + * tag + * @param ciphertextLength the length of the ciphertext and AEAD tag within {@code ciphertext} + * @param plaintext a byte array into which to write the decrypted plaintext + * @param plaintextOffset the offset within {@code plaintext} where the plaintext begins + * + * @return the number of bytes written to {@code plaintext} + * + * @throws AEADBadTagException if the AEAD tag in the given ciphertext does not match the calculated value + * @throws ShortBufferException if {@code plaintext} is not long enough (after its offset) to contain the resulting + * plaintext + * @throws IllegalArgumentException if the given ciphertext is too short to contain a valid AEAD tag + * + * @see #getPlaintextLength(int) + */ int decrypt(final Key key, final long nonce, @Nullable final byte[] associatedData, @@ -136,10 +328,26 @@ int decrypt(final Key key, final byte[] plaintext, final int plaintextOffset) throws AEADBadTagException, ShortBufferException; + /** + * Returns the size of a buffer needed to hold the ciphertext produced by encrypting a plaintext of the given length + * (the length of the plaintext plus the length of an AEAD tag). + * + * @param plaintextLength the length of a plaintext + * + * @return the length of the ciphertext that would be produced by encrypting a plaintext of the given length + */ default int getCiphertextLength(final int plaintextLength) { return plaintextLength + 16; } + /** + * Returns the size of a buffer needed to hold the plaintext produced by decrypting a ciphertext of the given length + * (the length of the ciphertext minus the length of the AEAD tag). + * + * @param ciphertextLength the length of a ciphertext + * + * @return the length of the plaintext that would be produced by decrypting a ciphertext of the given length + */ default int getPlaintextLength(final int ciphertextLength) { if (ciphertextLength < 16) { throw new IllegalArgumentException("Ciphertexts must be at least 16 bytes long"); @@ -148,9 +356,23 @@ default int getPlaintextLength(final int ciphertextLength) { return ciphertextLength - 16; } + /** + * Converts an array of bytes into a {@link Key} instance suitable for use with this cipher. + * + * @param keyBytes the raw bytes of the key + * + * @return a {@code Key} suitable for use with this cipher + */ Key buildKey(byte[] keyBytes); + /** + * Generates a new pseudo-random key as a function of the given key. + * + * @param key the key from which to derive a new key + * + * @return a new pseudo-random key derived from the given key + */ default Key rekey(final Key key) { - return new SecretKeySpec(encrypt(key, 0xffffffffffffffffL, null, new byte[32]), "RAW"); + return buildKey(encrypt(key, 0xffffffffffffffffL, null, new byte[32])); } } diff --git a/src/test/java/com/eatthepath/noise/component/AbstractNoiseCipherTest.java b/src/test/java/com/eatthepath/noise/component/AbstractNoiseCipherTest.java index 78cb931..d12f26e 100644 --- a/src/test/java/com/eatthepath/noise/component/AbstractNoiseCipherTest.java +++ b/src/test/java/com/eatthepath/noise/component/AbstractNoiseCipherTest.java @@ -17,7 +17,7 @@ abstract class AbstractNoiseCipherTest { protected abstract Key generateKey(); @Test - void encryptDecryptByteArray() throws AEADBadTagException { + void encryptDecryptNewByteArray() throws AEADBadTagException { final Key key = generateKey(); final long nonce = ThreadLocalRandom.current().nextLong(); @@ -27,20 +27,120 @@ void encryptDecryptByteArray() throws AEADBadTagException { final byte[] plaintext = "Hark! Plaintext!".getBytes(StandardCharsets.UTF_8); final byte[] ciphertext = getNoiseCipher().encrypt(key, nonce, hash, plaintext); + assertEquals(ciphertext.length, getNoiseCipher().getCiphertextLength(plaintext.length)); + assertEquals(plaintext.length, getNoiseCipher().getPlaintextLength(ciphertext.length)); + assertArrayEquals(plaintext, getNoiseCipher().decrypt(key, nonce, hash, ciphertext)); } @Test - void encryptDecryptByteBuffer() throws AEADBadTagException { + void encryptDecryptByteArrayInPlace() throws AEADBadTagException, ShortBufferException { final Key key = generateKey(); final long nonce = ThreadLocalRandom.current().nextLong(); final byte[] hash = new byte[32]; ThreadLocalRandom.current().nextBytes(hash); + final byte[] plaintextBytes = "Hark! Plaintext!".getBytes(StandardCharsets.UTF_8); + final byte[] buffer = new byte[getNoiseCipher().getCiphertextLength(plaintextBytes.length)]; + + System.arraycopy(plaintextBytes, 0, buffer, 0, plaintextBytes.length); + + assertEquals(buffer.length, getNoiseCipher().encrypt(key, nonce, + hash, 0, hash.length, + buffer, 0, plaintextBytes.length, + buffer, 0)); + + assertEquals(plaintextBytes.length, getNoiseCipher().decrypt(key, nonce, + hash, 0, hash.length, + buffer, 0, buffer.length, + buffer, 0)); + + final byte[] decryptedPlaintextBytes = new byte[plaintextBytes.length]; + System.arraycopy(buffer, 0, decryptedPlaintextBytes, 0, decryptedPlaintextBytes.length); + + assertArrayEquals(plaintextBytes, decryptedPlaintextBytes); + } + + @Test + void encryptDecryptNewByteBuffer() throws AEADBadTagException { + final Key key = generateKey(); + final long nonce = ThreadLocalRandom.current().nextLong(); + + final ByteBuffer hashBuffer; + { + final byte[] hash = new byte[32]; + ThreadLocalRandom.current().nextBytes(hash); + + hashBuffer = ByteBuffer.wrap(hash); + } + final ByteBuffer plaintext = ByteBuffer.wrap("Hark! Plaintext!".getBytes(StandardCharsets.UTF_8)); - final ByteBuffer ciphertext = getNoiseCipher().encrypt(key, nonce, ByteBuffer.wrap(hash), plaintext); + final ByteBuffer ciphertext = getNoiseCipher().encrypt(key, nonce, hashBuffer, plaintext); + + plaintext.rewind(); + hashBuffer.rewind(); + + assertEquals(ciphertext.remaining(), getNoiseCipher().getCiphertextLength(plaintext.remaining())); + assertEquals(plaintext.remaining(), getNoiseCipher().getPlaintextLength(ciphertext.remaining())); + + assertEquals(plaintext, getNoiseCipher().decrypt(key, nonce, hashBuffer, ciphertext)); + } + + @Test + void encryptDecryptByteBufferInPlace() throws AEADBadTagException, ShortBufferException { + final Key key = generateKey(); + final long nonce = ThreadLocalRandom.current().nextLong(); + + final ByteBuffer hashBuffer; + { + final byte[] hash = new byte[32]; + ThreadLocalRandom.current().nextBytes(hash); + + hashBuffer = ByteBuffer.wrap(hash); + } + + final byte[] plaintextBytes = "Hark! Plaintext!".getBytes(StandardCharsets.UTF_8); + final byte[] sharedByteArray = new byte[getNoiseCipher().getCiphertextLength(plaintextBytes.length)]; + + final ByteBuffer plaintextBuffer = ByteBuffer.wrap(sharedByteArray) + .limit(plaintextBytes.length) + .put(plaintextBytes) + .flip(); + + final ByteBuffer ciphertextBuffer = ByteBuffer.wrap(sharedByteArray); + + assertEquals(sharedByteArray.length, + getNoiseCipher().encrypt(key, nonce, hashBuffer, plaintextBuffer, ciphertextBuffer)); + + assertEquals(plaintextBytes.length, plaintextBuffer.limit()); + assertEquals(plaintextBuffer.limit(), plaintextBuffer.position()); + assertEquals(sharedByteArray.length, ciphertextBuffer.position()); + + hashBuffer.rewind(); + plaintextBuffer.rewind(); + ciphertextBuffer.rewind(); + + assertEquals(plaintextBytes.length, + getNoiseCipher().decrypt(key, nonce, hashBuffer, ciphertextBuffer, plaintextBuffer)); + + assertEquals(plaintextBytes.length, plaintextBuffer.limit()); + assertEquals(plaintextBuffer.limit(), plaintextBuffer.position()); + assertEquals(sharedByteArray.length, ciphertextBuffer.position()); + + plaintextBuffer.flip(); + + final byte[] decryptedPlaintextBytes = new byte[plaintextBytes.length]; + plaintextBuffer.get(decryptedPlaintextBytes); + + assertArrayEquals(plaintextBytes, decryptedPlaintextBytes); + } + + @Test + void decryptShortArray() { + final Key key = generateKey(); + final long nonce = ThreadLocalRandom.current().nextLong(); - assertEquals(plaintext, getNoiseCipher().decrypt(key, nonce, ByteBuffer.wrap(hash), ciphertext)); + assertThrows(IllegalArgumentException.class, () -> getNoiseCipher().decrypt(key, nonce, null, new byte[12])); } }