emiliosetiadarma commented on code in PR #7238:
URL: https://github.com/apache/nifi/pull/7238#discussion_r1194434579


##########
nifi-commons/nifi-security-utils/src/test/java/org/apache/nifi/security/util/crypto/Argon2CipherProviderTest.java:
##########
@@ -0,0 +1,418 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.util.crypto;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.binary.Hex;
+import org.apache.nifi.security.util.EncryptionMethod;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.Security;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class Argon2CipherProviderTest {
+    private static final String PLAINTEXT = "ExactBlockSizeRequiredForProcess";
+
+    private static List<EncryptionMethod> strongKDFEncryptionMethods;
+
+    private static final int DEFAULT_KEY_LENGTH = 128;
+    private final String SALT_HEX = "0123456789ABCDEFFEDCBA9876543210";
+    private static List<Integer> VALID_KEY_LENGTHS;
+    private RandomIVPBECipherProvider cipherProvider;
+    private final List<Integer> FULL_SALT_LENGTH_RANGE = Arrays.asList(49, 50, 
51, 52, 53);
+
+    @BeforeAll
+    static void setUpOnce() throws Exception {
+        Security.addProvider(new BouncyCastleProvider());
+
+        strongKDFEncryptionMethods = Arrays.stream(EncryptionMethod.values())
+                .filter(EncryptionMethod::isCompatibleWithStrongKDFs)
+                .collect(Collectors.toList());
+
+        VALID_KEY_LENGTHS = Arrays.asList(128, 192, 256);
+    }
+
+    @BeforeEach
+    void setUp() {
+        // Very fast parameters to test for correctness rather than production 
values
+        cipherProvider = new Argon2CipherProvider(1024, 1, 3);
+    }
+
+    @Test
+    void testGetCipherShouldBeInternallyConsistent() throws Exception {
+        // Arrange
+        final String PASSWORD = "shortPassword";
+        final byte[] SALT = cipherProvider.generateSalt();
+
+        // Act
+        for (EncryptionMethod em : strongKDFEncryptionMethods) {
+            // Initialize a cipher for encryption
+            Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, 
DEFAULT_KEY_LENGTH, true);
+            byte[] iv = cipher.getIV();
+
+            byte[] cipherBytes = cipher.doFinal(PLAINTEXT.getBytes("UTF-8"));
+
+            cipher = cipherProvider.getCipher(em, PASSWORD, SALT, iv, 
DEFAULT_KEY_LENGTH, false);
+            byte[] recoveredBytes = cipher.doFinal(cipherBytes);
+            String recovered = new String(recoveredBytes, "UTF-8");
+
+            // Assert
+            assertEquals(PLAINTEXT, recovered);
+        }
+    }
+
+    @Test
+    void testArgon2ShouldSupportExternalCompatibility() throws Exception {
+        // Arrange
+
+        // Default values are hashLength = 32, memory = 1024, parallelism = 1, 
iterations = 3, but the provided salt will contain the parameters used
+        cipherProvider = new Argon2CipherProvider();
+
+        final String PLAINTEXT = "This is a plaintext message.";
+        final String PASSWORD = "thisIsABadPassword";
+        final int hashLength = 256;
+
+        // These values can be generated by running `$ ./openssl_argon2.rb` in 
the terminal
+        final byte[] SALT = 
Hex.decodeHex("68d29a1d8021f45954333767358a2492".toCharArray());
+        final byte[] IV = 
Hex.decodeHex("808590f35f9fba14dbda9c2bb2b76a79".toCharArray());
+
+        final String CIPHER_TEXT = 
"d672412857916880c79d573aa4f9d4971b85f07438d6f62f38a0e31314caa2e5";
+        byte[] cipherBytes = Hex.decodeHex(CIPHER_TEXT.toCharArray());
+        EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC;
+
+        // Sanity check
+        String rubyKeyHex = 
"8caf581795886d38f0c605e3d674f4961c658ee3625a8e8868be36c902d234ef";
+        Cipher rubyCipher = 
Cipher.getInstance(encryptionMethod.getAlgorithm(), "BC");
+        SecretKeySpec rubyKey = new 
SecretKeySpec(Hex.decodeHex(rubyKeyHex.toCharArray()), "AES");
+        IvParameterSpec ivSpec = new IvParameterSpec(IV);
+        rubyCipher.init(Cipher.ENCRYPT_MODE, rubyKey, ivSpec);
+        byte[] rubyCipherBytes = rubyCipher.doFinal(PLAINTEXT.getBytes());
+        rubyCipher.init(Cipher.DECRYPT_MODE, rubyKey, ivSpec);
+        assertArrayEquals(PLAINTEXT.getBytes(), 
rubyCipher.doFinal(rubyCipherBytes));
+        assertArrayEquals(PLAINTEXT.getBytes(), 
rubyCipher.doFinal(cipherBytes));
+
+        // $argon2id$v=19$m=memory,t=iterations,p=parallelism$saltB64$hashB64
+        final String FULL_HASH = 
"$argon2id$v=19$m=256,t=3,p=1$aNKaHYAh9FlUMzdnNYokkg$jK9YF5WIbTjwxgXj1nT0lhxljuNiWo6IaL42yQLSNO8";
+
+        final String FULL_SALT = FULL_HASH.substring(0, 
FULL_HASH.lastIndexOf("$"));
+
+        final String[] hashComponents = FULL_HASH.split("\\$");
+        final String saltB64 = hashComponents[4];
+        byte[] salt = Base64.decodeBase64(saltB64);
+        assertArrayEquals(SALT, salt);
+
+        // Act
+        Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, 
FULL_SALT.getBytes(), IV, hashLength, false);
+        byte[] recoveredBytes = cipher.doFinal(cipherBytes);
+        String recovered = new String(recoveredBytes, "UTF-8");
+
+        // Assert
+        assertEquals(PLAINTEXT, recovered);
+    }
+
+    @Test
+    void testGetCipherShouldRejectInvalidIV() throws Exception {
+        // Arrange
+        final String PASSWORD = "shortPassword";
+        final byte[] SALT = Hex.decodeHex(SALT_HEX.toCharArray());
+        final int MAX_LENGTH = 15;
+        final List<byte[]> INVALID_IVS = new ArrayList<>();
+        for (int length = 0; length <= MAX_LENGTH; length++) {
+            INVALID_IVS.add(new byte[length]);
+        }
+
+        EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC;
+
+        // Act
+        for (final byte[] badIV: INVALID_IVS) {
+            // Encrypt should print a warning about the bad IV but overwrite it
+            Cipher cipher = cipherProvider.getCipher(encryptionMethod, 
PASSWORD, SALT, badIV, DEFAULT_KEY_LENGTH, true);
+
+            // Decrypt should fail
+            IllegalArgumentException iae = 
assertThrows(IllegalArgumentException.class,
+                    () -> cipherProvider.getCipher(encryptionMethod, PASSWORD, 
SALT, badIV, DEFAULT_KEY_LENGTH, false));
+
+            // Assert
+            assertTrue(iae.getMessage().contains("Cannot decrypt without a 
valid IV"));
+        }
+    }
+
+    @Test
+    void testGetCipherWithExternalIVShouldBeInternallyConsistent() throws 
Exception {
+        // Arrange
+        final String PASSWORD = "shortPassword";
+        final byte[] SALT = cipherProvider.generateSalt();
+        final byte[] IV = Hex.decodeHex("01".repeat(16).toCharArray());
+
+        // Act
+        for (EncryptionMethod em : strongKDFEncryptionMethods) {
+            // Initialize a cipher for encryption
+            Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, 
DEFAULT_KEY_LENGTH, true);
+
+            byte[] cipherBytes = cipher.doFinal(PLAINTEXT.getBytes("UTF-8"));
+
+            cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, 
DEFAULT_KEY_LENGTH, false);
+            byte[] recoveredBytes = cipher.doFinal(cipherBytes);
+            String recovered = new String(recoveredBytes, "UTF-8");
+
+            // Assert
+            assertEquals(PLAINTEXT, recovered);
+        }
+    }
+
+    @Test
+    void testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() 
throws Exception {
+        // Arrange
+        final String PASSWORD = "shortPassword";
+        final byte[] SALT = cipherProvider.generateSalt();
+
+        final int LONG_KEY_LENGTH = 256;
+
+        // Act
+        for (EncryptionMethod em : strongKDFEncryptionMethods) {
+            // Initialize a cipher for encryption
+            Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, 
LONG_KEY_LENGTH, true);
+            byte[] iv = cipher.getIV();
+
+            byte[] cipherBytes = cipher.doFinal(PLAINTEXT.getBytes("UTF-8"));
+
+            cipher = cipherProvider.getCipher(em, PASSWORD, SALT, iv, 
LONG_KEY_LENGTH, false);
+            byte[] recoveredBytes = cipher.doFinal(cipherBytes);
+            String recovered = new String(recoveredBytes, "UTF-8");
+
+            // Assert
+            assertEquals(PLAINTEXT, recovered);
+        }
+    }
+
+    @Test
+    void testGetCipherShouldNotAcceptInvalidSalts() throws Exception {
+        // Arrange
+        final String PASSWORD = "thisIsABadPassword";
+
+        final List<String> INVALID_SALTS = Arrays.asList("argon2", "$3a$11$", 
"x", "$2a$10$");
+        final String LENGTH_MESSAGE = "The raw salt must be greater than or 
equal to 8 bytes";
+
+        EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC;
+
+        // Act
+        for (final String salt : INVALID_SALTS) {
+            IllegalArgumentException iae = 
assertThrows(IllegalArgumentException.class,
+                    () -> cipherProvider.getCipher(encryptionMethod, PASSWORD, 
salt.getBytes(), DEFAULT_KEY_LENGTH, true));
+
+            // Assert
+            assertTrue(iae.getMessage().contains(LENGTH_MESSAGE));
+        }
+    }
+
+    @Test
+    void testGetCipherShouldHandleUnformattedSalts() throws Exception {
+        // Arrange
+        final String PASSWORD = "thisIsABadPassword";

Review Comment:
   Moved to a static values



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscr...@nifi.apache.org

For queries about this service, please contact Infrastructure at:
us...@infra.apache.org

Reply via email to