Repository: hyperledger/quilt (java)

Issue:
Change AuthTag ByteOrdering in STREAM RFC
File issue in Rust and JS and Quilt to not [re-arrange the bytes](https://github.com/hyperledger/quilt/blob/master/stream-parent/stream-core/src/main/java/org/interledger/stream/crypto/JavaxStreamEncryptionService.java#L72) - the tag should go last so that we can easily conform to (standard?) or at least typical library implementations.

Pull request:
Support standard NIST-recommended AuthTag ByteOrdering in STREAM Encryption Service
Fixes #237

* Fix the AuthTag byte ordering in the default StreamEncryptionService in a fully-backward compatible manner.
* Deprecate JavaxStreamEncryptionService and rename to the more appropriate AesGcmStreamEncryptionService.
* Add unit tests.

Signed-off-by: David Fuelling <sappenin@gmail.com>

Patch:
diff --git a/stream-parent/stream-core/pom.xml b/stream-parent/stream-core/pom.xml
index e7c18bb..ce6b89d 100644
--- a/stream-parent/stream-core/pom.xml
+++ b/stream-parent/stream-core/pom.xml
@@ -43,5 +43,9 @@
       <artifactId>mockito-core</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-api</artifactId>
+    </dependency>
   </dependencies>
 </project>
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/crypto/AesGcmStreamEncryptionService.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/crypto/AesGcmStreamEncryptionService.java
new file mode 100644
index 0000000..542e72d
--- /dev/null
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/crypto/AesGcmStreamEncryptionService.java
@@ -0,0 +1,377 @@
+package org.interledger.stream.crypto;
+
+import org.interledger.core.SharedSecret;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.hash.Hashing;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.Objects;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * An {@link StreamEncryptionService} that uses a JavaKeystore for underlying key storage.
+ */
+public class AesGcmStreamEncryptionService implements StreamEncryptionService {
+
+  /**
+   * For GCM a 12 byte random byte-array is recommend by NIST because it's faster and more secure (See page 8 in the PDF
+   * document below).
+   *
+   * @see "https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf"
+   */
+  @VisibleForTesting
+  static final int AES_GCM_NONCE_IV_LENGTH = 12;
+
+  private static final String CIPHER_ALGO = "AES/GCM/NoPadding";
+  private static final byte[] ENCRYPTION_KEY_STRING = "ilp_stream_encryption".getBytes(StandardCharsets.US_ASCII);
+  private static final int AUTH_TAG_LENGTH_BITS = 128;
+  private static final int AUTH_TAG_LENGTH_BYTES = AUTH_TAG_LENGTH_BITS / 8;
+  private final EncryptionMode encryptionMode;
+  private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+  /**
+   * No-args Constructor that initializes {@link #encryptionMode} to be {@link EncryptionMode#ENCRYPT_NON_STANDARD}
+   * because this is the default because it's currently the most widely deploy mechanism.
+   */
+  public AesGcmStreamEncryptionService() {
+    this(EncryptionMode.ENCRYPT_NON_STANDARD);
+  }
+
+  /**
+   * Required-args Constructor.
+   *
+   * @param encryptionMode The {@link EncryptionMode} to use when encrypting and decrypting.
+   */
+  public AesGcmStreamEncryptionService(final EncryptionMode encryptionMode) {
+    this.encryptionMode = Objects.requireNonNull(encryptionMode);
+  }
+
+  @Override
+  public byte[] encrypt(final SharedSecret sharedSecret, final byte[] plainText) throws EncryptionException {
+    Objects.requireNonNull(sharedSecret);
+    Objects.requireNonNull(plainText);
+
+    // Create an initialization vector. For GCM a 12 byte random byte-array is recommend by NIST
+    // because it's faster and more secure. See here, page 8:
+    // https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
+    final byte[] iv = Random.randBytes(AES_GCM_NONCE_IV_LENGTH);
+
+    return encryptWithIv(sharedSecret, plainText, iv);
+  }
+
+  @VisibleForTesting
+  byte[] encryptWithIv(final SharedSecret sharedSecret, final byte[] plainText, final byte[] iv)
+      throws EncryptionException {
+    Objects.requireNonNull(sharedSecret);
+    Objects.requireNonNull(plainText);
+    Objects.requireNonNull(iv);
+
+    if (this.encryptionMode == EncryptionMode.ENCRYPT_NON_STANDARD) {
+      return this.nonStandardModeEncryptWithIv(sharedSecret, plainText, iv);
+    } else {
+      return this.standardModeEncryptWithIv(sharedSecret, plainText, iv);
+    }
+  }
+
+  /**
+   * <p>Encrypts {@code plainText} with {@code iv} using the standard byte arrangement of the ciphertext where the
+   * AuthTag goes last, as specified by NIST.</p>
+   *
+   * @param sharedSecret A {@link SharedSecret} used for encryption.
+   * @param plainText    A byte-array to encrypt.
+   * @param iv           An initialization vector used AES/GCM.
+   *
+   * @return A byte-array containing encrypted cipherMessage, which consists of the iv plus ciphertext (note this is
+   *     inverted from the NIST specification).
+   *
+   * @see "https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf"
+   */
+  private byte[] standardModeEncryptWithIv(
+      final SharedSecret sharedSecret, final byte[] plainText, final byte[] iv
+  ) throws EncryptionException {
+    Objects.requireNonNull(plainText);
+    Preconditions.checkArgument(iv.length == AES_GCM_NONCE_IV_LENGTH);
+
+    byte[] encryptionKey = Hashing.hmacSha256(sharedSecret.key()).hashBytes(ENCRYPTION_KEY_STRING).asBytes();
+    final SecretKey typedEncryptionKey = new SecretKeySpec(encryptionKey, "AES");
+
+    // See https://proandroiddev.com/security-best-practices-symmetric-encryption-with-aes-in-java-7616beaaade9
+    try {
+      final Cipher cipher = Cipher.getInstance(CIPHER_ALGO);
+      // 128 is the recommended authentication tag length for GCM. More info can be found in pdf mentioned above.
+      // After each encryption operation using GCM mode, callers should re-initialize the cipher objects with GCM
+      // parameters using a different IV value.
+      final GCMParameterSpec parameterSpec = new GCMParameterSpec(AUTH_TAG_LENGTH_BITS, iv);
+      cipher.init(Cipher.ENCRYPT_MODE, typedEncryptionKey, parameterSpec);
+      byte[] cipherText = cipher.doFinal(plainText);
+
+      // Concatenate to a single message
+      final ByteBuffer cipherMessageByteBuffer = ByteBuffer.allocate(iv.length + cipherText.length);
+      cipherMessageByteBuffer.put(iv);
+      cipherMessageByteBuffer.put(cipherText);
+      final byte[] cipherMessage = cipherMessageByteBuffer.array();
+
+      // It is best practice to try to wipe sensible data like a cryptographic key or IV from memory as fast as
+      // possible. Since Java is a language with automatic memory management, we don’t have any guarantees that the
+      // following works as intended, but it should in most cases
+      Arrays.fill(iv, (byte) 0); //overwrite the content of key with zeros
+      Arrays.fill(encryptionKey, (byte) 0);
+
+      return cipherMessage;
+    } catch (NoSuchAlgorithmException
+        | NoSuchPaddingException
+        | InvalidAlgorithmParameterException
+        | InvalidKeyException
+        | BadPaddingException
+        | IllegalBlockSizeException e
+    ) {
+      throw new EncryptionException("Unable to Encrypt: ", e);
+    }
+  }
+
+  /**
+   * <p>Encrypts {@code plainText} with {@code iv} using a non-standard byte arrangement where the AuthTag goes first,
+   * followed by ciphertext.</p>
+   *
+   * <p>This arrangement was introduced in the JS and Rust Connectors by mistake, and by the time Java was implemented,
+   * JS and Rust were widely deployed enough that Java had to follow suit. While this implementation is non-standard, it
+   * isn't technically broken, and  works as long as everyone uses the same arrangement when encrypting/decrypting,
+   * which is the case in most Interledger deployments.</p>
+   *
+   * @param sharedSecret A {@link SharedSecret} used for encryption.
+   * @param plainText    A byte-array to encrypt.
+   * @param iv           An initialization vector used AES/GCM.
+   *
+   * @return A byte-array containing encrypted cipherMessage, which consists of the iv plus ciphertext (note this is
+   *     inverted from the NIST specification).
+   *
+   * @see "https://github.com/hyperledger/quilt/issues/237"
+   * @deprecated This method will be removed in a future version. Prefer {@link #standardModeEncryptWithIv(SharedSecret,
+   *     byte[], byte[])} instead.
+   */
+  @Deprecated
+  private byte[] nonStandardModeEncryptWithIv(
+      final SharedSecret sharedSecret, final byte[] plainText, final byte[] iv
+  ) throws EncryptionException {
+    Objects.requireNonNull(plainText);
+    Preconditions.checkArgument(iv.length == AES_GCM_NONCE_IV_LENGTH);
+
+    byte[] encryptionKey = Hashing.hmacSha256(sharedSecret.key()).hashBytes(ENCRYPTION_KEY_STRING).asBytes();
+    final SecretKey typedEncryptionKey = new SecretKeySpec(encryptionKey, "AES");
+
+    // See https://proandroiddev.com/security-best-practices-symmetric-encryption-with-aes-in-java-7616beaaade9
+    try {
+      final Cipher cipher = Cipher.getInstance(CIPHER_ALGO);
+      // 128 is the recommended authentication tag length for GCM. More info can be found in pdf mentioned above.
+      // After each encryption operation using GCM mode, callers should re-initialize the cipher objects with GCM
+      // parameters using a different IV value.
+      final GCMParameterSpec parameterSpec = new GCMParameterSpec(AUTH_TAG_LENGTH_BITS, iv);
+      cipher.init(Cipher.ENCRYPT_MODE, typedEncryptionKey, parameterSpec);
+      byte[] cipherText = cipher.doFinal(plainText);
+
+      byte[] rearrangedCipherText = new byte[cipherText.length];
+      // Rearrange the bytes so that the tag goes first (should have put it last in the JS implementation, but oh well)
+      System.arraycopy(
+          cipherText, cipherText.length - AUTH_TAG_LENGTH_BYTES, rearrangedCipherText, 0, AUTH_TAG_LENGTH_BYTES
+      );
+      // Put the cipherText last...
+      System.arraycopy(
+          cipherText, 0, rearrangedCipherText, AUTH_TAG_LENGTH_BYTES, cipherText.length - AUTH_TAG_LENGTH_BYTES
+      );
+
+      // Concatenate to a single message
+      final ByteBuffer cipherMessageByteBuffer = ByteBuffer.allocate(iv.length + cipherText.length);
+      cipherMessageByteBuffer.put(iv);
+      cipherMessageByteBuffer.put(rearrangedCipherText);
+      final byte[] cipherMessage = cipherMessageByteBuffer.array();
+
+      // It is best practice to try to wipe sensible data like a cryptographic key or IV from memory as fast as
+      // possible. Since Java is a language with automatic memory management, we don’t have any guarantees that the
+      // following works as intended, but it should in most cases
+      Arrays.fill(iv, (byte) 0); //overwrite the content of key with zeros
+      Arrays.fill(encryptionKey, (byte) 0);
+
+      return cipherMessage;
+    } catch (NoSuchAlgorithmException
+        | NoSuchPaddingException
+        | InvalidAlgorithmParameterException
+        | InvalidKeyException
+        | BadPaddingException
+        | IllegalBlockSizeException e
+    ) {
+      throw new EncryptionException("Unable to Encrypt: ", e);
+    }
+  }
+
+  @Override
+  public byte[] decrypt(final SharedSecret sharedSecret, final byte[] cipherMessage) {
+    Objects.requireNonNull(sharedSecret);
+    Objects.requireNonNull(cipherMessage);
+
+    if (this.encryptionMode == EncryptionMode.ENCRYPT_NON_STANDARD) {
+      try {
+        return this.nonStandardModeDecrypt(sharedSecret, cipherMessage);
+      } catch (EncryptionException e) {
+        logger.warn(
+            "Unable to decrypt payload in {} mode. Attempting {} mode as a fallback.",
+            EncryptionMode.ENCRYPT_NON_STANDARD,
+            EncryptionMode.ENCRYPT_STANDARD,
+            e
+        );
+        return this.standardModeDecrypt(sharedSecret, cipherMessage);
+      }
+    } else {
+      try {
+        return this.standardModeDecrypt(sharedSecret, cipherMessage);
+      } catch (EncryptionException e) {
+        logger.warn(
+            "Unable to decrypt payload in {} mode. Attempting {} mode as a fallback.",
+            EncryptionMode.ENCRYPT_STANDARD,
+            EncryptionMode.ENCRYPT_NON_STANDARD,
+            e
+        );
+        return this.nonStandardModeDecrypt(sharedSecret, cipherMessage);
+      }
+    }
+  }
+
+  /**
+   * <p>Decrypts {@code cipherMessage} using the standard byte arrangement of the embedded ciphertext where the
+   * AuthTag goes last, as specified by NIST.</p>
+   *
+   * @param sharedSecret  A {@link SharedSecret} used for encryption.
+   * @param cipherMessage A byte-array to decrypt.
+   *
+   * @return A byte-array containing decrypted plaintext.
+   *
+   * @see "https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf"
+   */
+  private byte[] standardModeDecrypt(final SharedSecret sharedSecret, final byte[] cipherMessage) {
+    Objects.requireNonNull(cipherMessage);
+
+    byte[] encryptionKey = Hashing.hmacSha256(sharedSecret.key()).hashBytes(ENCRYPTION_KEY_STRING).asBytes();
+    final SecretKey typedEncryptionKey = new SecretKeySpec(encryptionKey, "AES");
+
+    // First, deconstruct the message
+    try {
+      ByteBuffer byteBuffer = ByteBuffer.wrap(cipherMessage);
+      byte[] iv = new byte[AES_GCM_NONCE_IV_LENGTH];
+      byteBuffer.get(iv);
+      byte[] cipherText = new byte[byteBuffer.remaining()];
+      byteBuffer.get(cipherText);
+
+      final Cipher cipher = Cipher.getInstance(CIPHER_ALGO);
+      cipher.init(Cipher.DECRYPT_MODE, typedEncryptionKey, new GCMParameterSpec(AUTH_TAG_LENGTH_BITS, iv));
+      byte[] plainText = cipher.doFinal(cipherText);
+
+      // It is best practice to try to wipe sensible data like a cryptographic key or IV from memory as fast as
+      // possible. Since Java is a language with automatic memory management, we don’t have any guarantees that the
+      // following works as intended, but it should in most cases:
+      Arrays.fill(iv, (byte) 0); //overwrite the content of key with zeros
+      Arrays.fill(encryptionKey, (byte) 0);
+
+      return plainText;
+    } catch (Exception e) {
+      throw new EncryptionException(e.getMessage(), e);
+    }
+  }
+
+  /**
+   * <p>Decrypts {@code cipherMessage} using the non-standard byte arrangement of the embedded ciphertext where the
+   * AuthTag goes first, which is non-standard but commonly deployed.</p>
+   *
+   * <p>This arrangement was introduced in the JS and Rust Connectors by mistake, and by the time Java was implemented,
+   * JS and Rust were widely deployed enough that Java had to follow suit. While this implementation is non-standard, it
+   * isn't technically broken, and  works as long as everyone uses the same arrangement when encrypting/decrypting,
+   * which is the case in most Interledger deployments.</p>
+   *
+   * @param sharedSecret  A {@link SharedSecret} used for encryption.
+   * @param cipherMessage A byte-array to decrypt.
+   *
+   * @return A byte-array containing decrypted plaintext.
+   *
+   * @see "https://github.com/hyperledger/quilt/issues/237"
+   * @see "https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf"
+   * @deprecated This method will be removed in a future version. Prefer {@link #standardModeDecrypt(SharedSecret,
+   *     byte[])} instead.
+   */
+  @Deprecated
+  private byte[] nonStandardModeDecrypt(final SharedSecret sharedSecret, final byte[] cipherMessage) {
+    Objects.requireNonNull(cipherMessage);
+
+    byte[] encryptionKey = Hashing.hmacSha256(sharedSecret.key()).hashBytes(ENCRYPTION_KEY_STRING).asBytes();
+    final SecretKey typedEncryptionKey = new SecretKeySpec(encryptionKey, "AES");
+
+    // First, deconstruct the message
+    try {
+      ByteBuffer byteBuffer = ByteBuffer.wrap(cipherMessage);
+      byte[] iv = new byte[AES_GCM_NONCE_IV_LENGTH];
+      byteBuffer.get(iv);
+      byte[] cipherText = new byte[byteBuffer.remaining()];
+      byteBuffer.get(cipherText);
+
+      // TODO: See https://github.com/hyperledger/quilt/issues/237
+      // Rearrange the bytes so that the `tag` goes last, after tha Additionally Authenticated Data (AAD). Prior to this
+      // reversal, the data is inverted because that's what the RFC specifies, and that's what JS and Rust do.
+      byte[] rearrangedCipherText = new byte[cipherText.length];
+      System.arraycopy(
+          cipherText, 0, rearrangedCipherText, cipherText.length - AUTH_TAG_LENGTH_BYTES, AUTH_TAG_LENGTH_BYTES
+      );
+      // Put the cipherText last...
+      System.arraycopy(
+          cipherText, AUTH_TAG_LENGTH_BYTES, rearrangedCipherText, 0, cipherText.length - AUTH_TAG_LENGTH_BYTES
+      );
+
+      final Cipher cipher = Cipher.getInstance(CIPHER_ALGO);
+      cipher.init(Cipher.DECRYPT_MODE, typedEncryptionKey, new GCMParameterSpec(AUTH_TAG_LENGTH_BITS, iv));
+      byte[] plainText = cipher.doFinal(rearrangedCipherText);
+
+      // It is best practice to try to wipe sensible data like a cryptographic key or IV from memory as fast as
+      // possible. Since Java is a language with automatic memory management, we don’t have any guarantees that the
+      // following works as intended, but it should in most cases:
+      Arrays.fill(iv, (byte) 0); //overwrite the content of key with zeros
+      Arrays.fill(encryptionKey, (byte) 0);
+
+      return plainText;
+    } catch (Exception e) {
+      throw new EncryptionException(e.getMessage(), e);
+    }
+  }
+
+  /**
+   * Defines the mode this service will operate in, based upon the options founds in issue #237.
+   *
+   * @see "https://github.com/hyperledger/quilt/issues/237"
+   */
+  public enum EncryptionMode {
+    /**
+     * Encrypts payloads with ciphertext that has the AuthTag last, which is non-standard but commonly deployed in JS
+     * and RUST implementations. Decryption attempts non-standard-mode first, and falls-back to standard-mode in the
+     * event of an {@link EncryptionException}.
+     */
+    ENCRYPT_NON_STANDARD,
+
+    /**
+     * Encrypts payloads with ciphertext that has the AuthTag first, which is implied by the NIST standard but not
+     * commonly deployed in JS or RUST implementations. Decryption attempts standard-mode first, and falls-back to
+     * non-standard mode in the event of an {@link EncryptionException}.
+     */
+    ENCRYPT_STANDARD
+  }
+}
diff --git a/stream-parent/stream-core/src/main/java/org/interledger/stream/crypto/JavaxStreamEncryptionService.java b/stream-parent/stream-core/src/main/java/org/interledger/stream/crypto/JavaxStreamEncryptionService.java
index aea9894..f2942d7 100644
--- a/stream-parent/stream-core/src/main/java/org/interledger/stream/crypto/JavaxStreamEncryptionService.java
+++ b/stream-parent/stream-core/src/main/java/org/interledger/stream/crypto/JavaxStreamEncryptionService.java
@@ -1,152 +1,12 @@
 package org.interledger.stream.crypto;
 
-import org.interledger.core.SharedSecret;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.hash.Hashing;
-
-import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.util.Arrays;
-import java.util.Objects;
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.GCMParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
 /**
  * An {@link StreamEncryptionService} that uses a JavaKeystore for underlying key storage.
+ *
+ * @deprecated This implementation will be removed in a future version. Prefer {@link AesGcmStreamEncryptionService}
+ *     instead as a valid replacement.
  */
-public class JavaxStreamEncryptionService implements StreamEncryptionService {
-
-  /**
-   * For GCM a 12 byte random byte-array is recommend by NIST because it's faster and more secure (See page 8 in the PDF
-   * document below).
-   *
-   * @see "https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf"
-   */
-  @VisibleForTesting
-  static final int AES_GCM_NONCE_IV_LENGTH = 12;
-
-  private static final String CIPHER_ALGO = "AES/GCM/NoPadding";
-
-  private static final byte[] ENCRYPTION_KEY_STRING = "ilp_stream_encryption".getBytes(StandardCharsets.US_ASCII);
-
-  private static final int AUTH_TAG_LENGTH_BITS = 128;
-  private static final int AUTH_TAG_LENGTH_BYTES = AUTH_TAG_LENGTH_BITS / 8;
-
-  @Override
-  public byte[] encrypt(final SharedSecret sharedSecret, final byte[] plainText) throws EncryptionException {
-    Objects.requireNonNull(plainText);
-
-    // Create an initialization vector. For GCM a 12 byte random byte-array is recommend by NIST
-    // because it's faster and more secure. See here, page 8:
-    // https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
-    final byte[] iv = Random.randBytes(AES_GCM_NONCE_IV_LENGTH);
-
-    return encryptWithIv(sharedSecret, plainText, iv);
-  }
-
-  @VisibleForTesting
-  byte[] encryptWithIv(final SharedSecret sharedSecret, final byte[] plainText, final byte[] iv)
-      throws EncryptionException {
-    Objects.requireNonNull(plainText);
-
-    byte[] encryptionKey = Hashing.hmacSha256(sharedSecret.key()).hashBytes(ENCRYPTION_KEY_STRING).asBytes();
-    final SecretKey typedEncryptionKey = new SecretKeySpec(encryptionKey, "AES");
-
-    // See https://proandroiddev.com/security-best-practices-symmetric-encryption-with-aes-in-java-7616beaaade9
-    try {
-      final Cipher cipher = Cipher.getInstance(CIPHER_ALGO);
-      // 128 is the recommended authentication tag length for GCM. More info can be found in pdf mentioned above.
-      // After each encryption operation using GCM mode, callers should re-initialize the cipher objects with GCM
-      // parameters using a different IV value.
-      final GCMParameterSpec parameterSpec = new GCMParameterSpec(AUTH_TAG_LENGTH_BITS, iv);
-      cipher.init(Cipher.ENCRYPT_MODE, typedEncryptionKey, parameterSpec);
-      byte[] cipherText = cipher.doFinal(plainText);
-
-      // TODO: Fix this in JS and Rust so that the typical ordering is not wonky, and avoids this copy.
-      byte[] rearrangedCipherText = new byte[cipherText.length];
-      // Rearrange the bytes so that the tag goes first (should have put it last in the JS implementation, but oh well)
-      System.arraycopy(
-          cipherText, cipherText.length - AUTH_TAG_LENGTH_BYTES, rearrangedCipherText, 0, AUTH_TAG_LENGTH_BYTES
-      );
-      // Put the cipherText last...
-      System.arraycopy(
-          cipherText, 0, rearrangedCipherText, AUTH_TAG_LENGTH_BYTES, cipherText.length - AUTH_TAG_LENGTH_BYTES
-      );
-
-      // Concatenate to a single message
-      final ByteBuffer cipherMessageByteBuffer = ByteBuffer.allocate(iv.length + cipherText.length);
-      cipherMessageByteBuffer.put(iv);
-      cipherMessageByteBuffer.put(rearrangedCipherText);
-      final byte[] cipherMessage = cipherMessageByteBuffer.array();
-
-      // It is best practice to try to wipe sensible data like a cryptographic key or IV from memory as fast as
-      // possible. Since Java is a language with automatic memory management, we don’t have any guarantees that the
-      // following works as intended, but it should in most cases
-      Arrays.fill(iv, (byte) 0); //overwrite the content of key with zeros
-      Arrays.fill(encryptionKey, (byte) 0);
-
-      return cipherMessage;
-    } catch (NoSuchAlgorithmException
-        | NoSuchPaddingException
-        | InvalidAlgorithmParameterException
-        | InvalidKeyException
-        | BadPaddingException
-        | IllegalBlockSizeException e
-    ) {
-      throw new EncryptionException("Unable to Encrypt: ", e);
-    }
-  }
-
-  @Override
-  public byte[] decrypt(final SharedSecret sharedSecret, final byte[] cipherMessage) {
-    Objects.requireNonNull(cipherMessage);
-
-    byte[] encryptionKey = Hashing.hmacSha256(sharedSecret.key()).hashBytes(ENCRYPTION_KEY_STRING).asBytes();
-    final SecretKey typedEncryptionKey = new SecretKeySpec(encryptionKey, "AES");
-
-    // First, deconstruct the message
-    try {
-      ByteBuffer byteBuffer = ByteBuffer.wrap(cipherMessage);
-      byte[] iv = new byte[AES_GCM_NONCE_IV_LENGTH];
-      byteBuffer.get(iv);
-      byte[] cipherText = new byte[byteBuffer.remaining()];
-      byteBuffer.get(cipherText);
-
-      // TODO: See https://github.com/hyperledger/quilt/issues/237
-      // Rearrange the bytes so that the `tag` goes last, after tha Additionally Authenticated Data (AAD). Prior to this
-      // reversal, the data is inverted because that's what the RFC specifies, and that's what JS and Rust do.
-      byte[] rearrangedCipherText = new byte[cipherText.length];
-      System.arraycopy(
-          cipherText, 0, rearrangedCipherText, cipherText.length - AUTH_TAG_LENGTH_BYTES, AUTH_TAG_LENGTH_BYTES
-      );
-      // Put the cipherText last...
-      System.arraycopy(
-          cipherText, AUTH_TAG_LENGTH_BYTES, rearrangedCipherText, 0, cipherText.length - AUTH_TAG_LENGTH_BYTES
-      );
-
-      final Cipher cipher = Cipher.getInstance(CIPHER_ALGO);
-      cipher.init(Cipher.DECRYPT_MODE, typedEncryptionKey, new GCMParameterSpec(AUTH_TAG_LENGTH_BITS, iv));
-      byte[] plainText = cipher.doFinal(rearrangedCipherText);
-
-      // It is best practice to try to wipe sensible data like a cryptographic key or IV from memory as fast as
-      // possible. Since Java is a language with automatic memory management, we don’t have any guarantees that the
-      // following works as intended, but it should in most cases:
-      Arrays.fill(iv, (byte) 0); //overwrite the content of key with zeros
-      Arrays.fill(encryptionKey, (byte) 0);
-
-      return plainText;
-    } catch (Exception e) {
-      throw new EncryptionException(e.getMessage(), e);
-    }
-  }
+@Deprecated
+public class JavaxStreamEncryptionService extends AesGcmStreamEncryptionService implements StreamEncryptionService {
 
 }


Test patch:
diff --git a/stream-parent/stream-core/src/test/java/org/interledger/stream/crypto/AesGcmStreamEncryptionServiceTest.java b/stream-parent/stream-core/src/test/java/org/interledger/stream/crypto/AesGcmStreamEncryptionServiceTest.java
new file mode 100644
index 0000000..65dc635
--- /dev/null
+++ b/stream-parent/stream-core/src/test/java/org/interledger/stream/crypto/AesGcmStreamEncryptionServiceTest.java
@@ -0,0 +1,105 @@
+package org.interledger.stream.crypto;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.interledger.core.SharedSecret;
+import org.interledger.stream.crypto.AesGcmStreamEncryptionService.EncryptionMode;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link AesGcmStreamEncryptionService}.
+ *
+ * <p>Note: Test values were taken from the Interledger-rs project, which mimic the Interledger-js test values.</p>
+ *
+ * @see "https://github.com/interledger-rs/interledger-rs/blob/master/crates/interledger-stream/src/crypto.rs"
+ */
+public class AesGcmStreamEncryptionServiceTest {
+
+  private static final SharedSecret SHARED_SECRET = SharedSecret.of(new byte[] {
+      (byte) 126, (byte) 219, (byte) 117, (byte) 93, (byte) 118, (byte) 248, (byte) 249, (byte) 211, (byte) 20,
+      (byte) 211, (byte) 65, 110, (byte) 237, (byte) 80, (byte) 253, (byte) 179, (byte) 81, (byte) 146, (byte) 229,
+      (byte) 67, (byte) 231, (byte) 49, (byte) 92, (byte) 127, (byte) 254, (byte) 230, (byte) 144, (byte) 102,
+      (byte) 103, (byte) 166, (byte) 150, (byte) 36
+  });
+
+  private static final byte[] PLAINTEXT = new byte[] {99, 0, 12, (byte) 255, 77, 31};
+
+  private static final byte[] CIPHERTEXT = new byte[] {
+      (byte) 119, (byte) 248, (byte) 213, (byte) 234, (byte) 63, (byte) 200, (byte) 224, (byte) 140, (byte) 212,
+      (byte) 222, (byte) 105, (byte) 159, (byte) 246, (byte) 203, (byte) 66, (byte) 155, (byte) 151, (byte) 172,
+      (byte) 68, (byte) 24, (byte) 76, (byte) 232, (byte) 90, (byte) 10, (byte) 237, (byte) 146, (byte) 189, (byte) 73,
+      (byte) 248, (byte) 196, (byte) 177, (byte) 108, (byte) 115, (byte) 223
+  };
+
+  private static final byte[] NONCE_IV = new byte[] {(byte) 119, (byte) 248, (byte) 213, (byte) 234, (byte) 63,
+      (byte) 200, (byte) 224, (byte) 140, (byte) 212, (byte) 222, (byte) 105, (byte) 159};
+
+  private AesGcmStreamEncryptionService streamEncryptionService;
+
+  @Before
+  public void setUp() {
+    this.streamEncryptionService = new AesGcmStreamEncryptionService();
+  }
+
+  @Test
+  public void testEncryptToSameRustJs() {
+    byte[] encryptedValue = streamEncryptionService.encryptWithIv(SHARED_SECRET, PLAINTEXT, NONCE_IV);
+    assertThat(encryptedValue).isEqualTo(CIPHERTEXT);
+  }
+
+  @Test
+  public void testDecryptToSameAsRustJs() {
+    byte[] decryptedValue = streamEncryptionService.decrypt(SHARED_SECRET, CIPHERTEXT);
+    assertThat(decryptedValue).isEqualTo(PLAINTEXT);
+  }
+
+  /**
+   * Even though we're in standard encryption mode, the decryption should fallback in the event of a decryption error.
+   */
+  @Test
+  public void testDecryptToSameAsRustJsInStandardMode() {
+    this.streamEncryptionService = new AesGcmStreamEncryptionService(EncryptionMode.ENCRYPT_STANDARD);
+    byte[] decryptedValue = streamEncryptionService.decrypt(SHARED_SECRET, CIPHERTEXT);
+    assertThat(decryptedValue).isEqualTo(PLAINTEXT);
+  }
+
+  @Test
+  public void losslesslyEncryptAndDecryptsNonStandardMode() {
+    byte[] cipherMessage = streamEncryptionService.encrypt(SHARED_SECRET, PLAINTEXT);
+    byte[] decryptedValue = streamEncryptionService.decrypt(SHARED_SECRET, cipherMessage);
+    assertThat(decryptedValue).isEqualTo(PLAINTEXT);
+  }
+
+  @Test
+  public void losslesslyEncryptAndDecryptsStandardMode() {
+    this.streamEncryptionService = new AesGcmStreamEncryptionService(EncryptionMode.ENCRYPT_STANDARD);
+    byte[] cipherMessage = streamEncryptionService.encrypt(SHARED_SECRET, PLAINTEXT);
+    byte[] decryptedValue = streamEncryptionService.decrypt(SHARED_SECRET, cipherMessage);
+    assertThat(decryptedValue).isEqualTo(PLAINTEXT);
+  }
+
+  @Test
+  public void testIvLengthInNonStandardMode() {
+    this.streamEncryptionService = new AesGcmStreamEncryptionService(EncryptionMode.ENCRYPT_NON_STANDARD);
+    final int actualIvLength = AesGcmStreamEncryptionService.AES_GCM_NONCE_IV_LENGTH;
+    // See NIST suggestion in JavaxStreamEncryptionService. This test should ALWAYS pass!
+    assertThat(actualIvLength >= 12 || actualIvLength < 16);
+  }
+
+  @Test
+  public void testIvLengthInStandardMode() {
+    this.streamEncryptionService = new AesGcmStreamEncryptionService(EncryptionMode.ENCRYPT_STANDARD);
+    final int actualIvLength = AesGcmStreamEncryptionService.AES_GCM_NONCE_IV_LENGTH;
+    // See NIST suggestion in JavaxStreamEncryptionService. This test should ALWAYS pass!
+    assertThat(actualIvLength >= 12 || actualIvLength < 16);
+  }
+
+  @Test
+  public void testIvLength() {
+    final int actualIvLength = AesGcmStreamEncryptionService.AES_GCM_NONCE_IV_LENGTH;
+    // See NIST suggestion in JavaxStreamEncryptionService. This test should ALWAYS pass!
+    assertThat(actualIvLength >= 12 || actualIvLength < 16);
+  }
+}