aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdam Langley <agl@google.com>2022-03-09 15:56:58 -0800
committerPete Bentley <prb@google.com>2024-04-11 17:34:23 +0100
commit9a0591781a5da76b9fec9b6dc1f0b7a8df6aee4e (patch)
tree3c6209452dcf8fe44b00524ebf3c2477eaca5863
parent54e1a8ba0f82c33d39797c8788b9b1529c128d58 (diff)
downloadconscrypt-9a0591781a5da76b9fec9b6dc1f0b7a8df6aee4e.tar.gz
Add scrypt support.
Cherry-picked from upstream PR #1054. The KeySpec added here has method names that match BouncyCastle, and the factory will use reflection on unknown KeySpec types. Thus it's possible for code that currently uses scrypt from BC to transparently benefit from conscrypt. (The vast majority of this change was written by prb.) Bug: 331613998 Bug: 303794624 Test: MTS Co-authored-by: Adam Langley <agl@chromium.org> Co-authored-by: Pete Bentley <44170157+prbprbprb@users.noreply.github.com> Change-Id: Ib217f7a6b5e3817e68618d2f2a43cd954214a2d3
-rw-r--r--common/src/jni/main/cpp/conscrypt/native_crypto.cc45
-rw-r--r--common/src/main/java/org/conscrypt/NativeCrypto.java5
-rw-r--r--common/src/main/java/org/conscrypt/OpenSSLProvider.java3
-rw-r--r--common/src/main/java/org/conscrypt/ScryptKeySpec.java72
-rw-r--r--common/src/main/java/org/conscrypt/ScryptSecretKeyFactory.java129
-rw-r--r--common/src/test/java/org/conscrypt/javax/crypto/ScryptTest.java192
-rw-r--r--common/src/test/resources/crypto/scrypt.csv8
-rw-r--r--repackaged/common/src/main/java/com/android/org/conscrypt/NativeCrypto.java6
-rw-r--r--repackaged/common/src/main/java/com/android/org/conscrypt/OpenSSLProvider.java3
-rw-r--r--repackaged/common/src/main/java/com/android/org/conscrypt/ScryptKeySpec.java74
-rw-r--r--repackaged/common/src/main/java/com/android/org/conscrypt/ScryptSecretKeyFactory.java133
-rw-r--r--repackaged/common/src/test/java/com/android/org/conscrypt/javax/crypto/ScryptTest.java199
12 files changed, 869 insertions, 0 deletions
diff --git a/common/src/jni/main/cpp/conscrypt/native_crypto.cc b/common/src/jni/main/cpp/conscrypt/native_crypto.cc
index 86336a95..b5403822 100644
--- a/common/src/jni/main/cpp/conscrypt/native_crypto.cc
+++ b/common/src/jni/main/cpp/conscrypt/native_crypto.cc
@@ -10844,6 +10844,50 @@ static jboolean NativeCrypto_usesBoringSsl_FIPS_mode() {
return FIPS_mode();
}
+/**
+ * Scrypt support
+ */
+
+static jbyteArray NativeCrypto_Scrypt_generate_key(JNIEnv* env, jclass, jbyteArray password, jbyteArray salt,
+ jlong n, jlong r, jlong p, jint key_len) {
+ CHECK_ERROR_QUEUE_ON_RETURN;
+ JNI_TRACE("Scrypt_generate_key(%p, %p, %ld, %ld, %ld, %d)", password, salt, n, r, p, key_len);
+
+ if (password == nullptr) {
+ conscrypt::jniutil::throwNullPointerException(env, "password == null");
+ JNI_TRACE("Scrypt_generate_key() => password == null");
+ return nullptr;
+ }
+ if (salt == nullptr) {
+ conscrypt::jniutil::throwNullPointerException(env, "salt == null");
+ JNI_TRACE("Scrypt_generate_key() => salt == null");
+ return nullptr;
+ }
+
+ jbyteArray key_bytes = env->NewByteArray(static_cast<jsize>(key_len));
+ ScopedByteArrayRW out_key(env, key_bytes);
+ if (out_key.get() == nullptr) {
+ conscrypt::jniutil::throwNullPointerException(env, "out_key == null");
+ JNI_TRACE("Scrypt_generate_key() => out_key == null");
+ return nullptr;
+ }
+
+ size_t memory_limit = 1u << 29;
+ ScopedByteArrayRO password_bytes(env, password);
+ ScopedByteArrayRO salt_bytes(env, salt);
+
+ int result = EVP_PBE_scrypt(reinterpret_cast<const char*>(password_bytes.get()), password_bytes.size(),
+ reinterpret_cast<const uint8_t*>(salt_bytes.get()), salt_bytes.size(),
+ n, r, p, memory_limit,
+ reinterpret_cast<uint8_t*>(out_key.get()), key_len);
+
+ if (result <= 0) {
+ conscrypt::jniutil::throwExceptionFromBoringSSLError(env, "Scrypt_generate_key");
+ return nullptr;
+ }
+ return key_bytes;
+}
+
// TESTING METHODS BEGIN
static int NativeCrypto_BIO_read(JNIEnv* env, jclass, jlong bioRef, jbyteArray outputJavaBytes) {
@@ -11321,6 +11365,7 @@ static JNINativeMethod sNativeCryptoMethods[] = {
CONSCRYPT_NATIVE_METHOD(ENGINE_SSL_force_read, "(J" REF_SSL SSL_CALLBACKS ")V"),
CONSCRYPT_NATIVE_METHOD(ENGINE_SSL_shutdown, "(J" REF_SSL SSL_CALLBACKS ")V"),
CONSCRYPT_NATIVE_METHOD(usesBoringSsl_FIPS_mode, "()Z"),
+ CONSCRYPT_NATIVE_METHOD(Scrypt_generate_key, "([B[BJJJI)[B"),
// Used for testing only.
CONSCRYPT_NATIVE_METHOD(BIO_read, "(J[B)I"),
diff --git a/common/src/main/java/org/conscrypt/NativeCrypto.java b/common/src/main/java/org/conscrypt/NativeCrypto.java
index 8c5e1d57..68e41979 100644
--- a/common/src/main/java/org/conscrypt/NativeCrypto.java
+++ b/common/src/main/java/org/conscrypt/NativeCrypto.java
@@ -1511,6 +1511,11 @@ public final class NativeCrypto {
throws IOException;
/**
+ * Generates a key from a password and salt using Scrypt.
+ */
+ static native byte[] Scrypt_generate_key(byte[] password, byte[] salt, long n, long r, long p, int key_len);
+
+ /**
* Return {@code true} if BoringSSL has been built in FIPS mode.
*/
static native boolean usesBoringSsl_FIPS_mode();
diff --git a/common/src/main/java/org/conscrypt/OpenSSLProvider.java b/common/src/main/java/org/conscrypt/OpenSSLProvider.java
index 99547dc9..9c6f0560 100644
--- a/common/src/main/java/org/conscrypt/OpenSSLProvider.java
+++ b/common/src/main/java/org/conscrypt/OpenSSLProvider.java
@@ -217,6 +217,9 @@ public final class OpenSSLProvider extends Provider {
/* == SecretKeyFactory == */
put("SecretKeyFactory.DESEDE", PREFIX + "DESEDESecretKeyFactory");
put("Alg.Alias.SecretKeyFactory.TDEA", "DESEDE");
+ put("SecretKeyFactory.Scrypt", PREFIX + "ScryptSecretKeyFactory");
+ put("SecretKeyFactory.1.3.6.1.4.1.11591.4.11", PREFIX + "ScryptSecretKeyFactory");
+ put("SecretKeyFactory.OID.1.3.6.1.4.1.11591.4.11", PREFIX + "ScryptSecretKeyFactory");
/* == KeyAgreement == */
putECDHKeyAgreementImplClass("OpenSSLECDHKeyAgreement");
diff --git a/common/src/main/java/org/conscrypt/ScryptKeySpec.java b/common/src/main/java/org/conscrypt/ScryptKeySpec.java
new file mode 100644
index 00000000..809459fe
--- /dev/null
+++ b/common/src/main/java/org/conscrypt/ScryptKeySpec.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed 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.conscrypt;
+
+import java.security.spec.KeySpec;
+
+/**
+ * Mirrors the <a
+ * href="https://javadoc.io/static/org.bouncycastle/bcprov-jdk15on/1.68/org/bouncycastle/jcajce/spec/ScryptKeySpec.html">class
+ * of the same name</a> from BouncyCastle.
+ */
+public class ScryptKeySpec implements KeySpec {
+ private final char[] password;
+ private final byte[] salt;
+ private final int costParameter;
+ private final int blockSize;
+ private final int parallelizationParameter;
+ private final int keyOutputBits;
+
+ public ScryptKeySpec(
+ char[] password,
+ byte[] salt,
+ int costParameter,
+ int blockSize,
+ int parallelizationParameter,
+ int keyOutputBits) {
+ this.password = password;
+ this.salt = salt;
+ this.costParameter = costParameter;
+ this.blockSize = blockSize;
+ this.parallelizationParameter = parallelizationParameter;
+ this.keyOutputBits = keyOutputBits;
+ }
+
+ public char[] getPassword() {
+ return password;
+ }
+
+ public byte[] getSalt() {
+ return salt;
+ }
+
+ public int getCostParameter() {
+ return costParameter;
+ }
+
+ public int getBlockSize() {
+ return blockSize;
+ }
+
+ public int getParallelizationParameter() {
+ return parallelizationParameter;
+ }
+
+ public int getKeyLength() {
+ return keyOutputBits;
+ }
+}
diff --git a/common/src/main/java/org/conscrypt/ScryptSecretKeyFactory.java b/common/src/main/java/org/conscrypt/ScryptSecretKeyFactory.java
new file mode 100644
index 00000000..8b7afe34
--- /dev/null
+++ b/common/src/main/java/org/conscrypt/ScryptSecretKeyFactory.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed 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.conscrypt;
+
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.security.InvalidKeyException;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactorySpi;
+
+@Internal
+public class ScryptSecretKeyFactory extends SecretKeyFactorySpi {
+
+ @Override
+ protected SecretKey engineGenerateSecret(KeySpec inKeySpec) throws InvalidKeySpecException {
+
+ char[] password;
+ byte[] salt;
+ int n, r, p, keyOutputBits;
+
+ if (inKeySpec instanceof ScryptKeySpec) {
+ ScryptKeySpec spec = (ScryptKeySpec) inKeySpec;
+ password = spec.getPassword();
+ salt = spec.getSalt();
+ n = spec.getCostParameter();
+ r = spec.getBlockSize();
+ p = spec.getParallelizationParameter();
+ keyOutputBits = spec.getKeyLength();
+ } else {
+ // Extract parameters from any `KeySpec` that has getters with the correct name. This allows,
+ // for example, code to use BouncyCastle's KeySpec with the conscrypt provider.
+ try {
+ password = (char[]) getValue(inKeySpec, "getPassword");
+ salt = (byte[]) getValue(inKeySpec, "getSalt");
+ n = (int) getValue(inKeySpec, "getCostParameter");
+ r = (int) getValue(inKeySpec, "getBlockSize");
+ p = (int) getValue(inKeySpec, "getParallelizationParameter");
+ keyOutputBits = (int) getValue(inKeySpec, "getKeyLength");
+ } catch (Exception e) {
+ throw new InvalidKeySpecException("Not a valid scrypt KeySpec", e);
+ }
+ }
+
+ if (keyOutputBits % 8 != 0) {
+ throw new InvalidKeySpecException("Cannot produce fractional-byte outputs");
+ }
+
+ try {
+ return new ScryptKey(
+ NativeCrypto.Scrypt_generate_key(
+ new String(password).getBytes("UTF-8"), salt, n, r, p, keyOutputBits / 8));
+ } catch (UnsupportedEncodingException e) {
+ // Impossible according to the Java docs: UTF-8 is always supported.
+ throw new RuntimeException(e);
+ }
+ }
+
+ private Object getValue(KeySpec spec, String methodName)
+ throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+ Method method = spec.getClass().getMethod(methodName, (Class<?>[]) null);
+ return method.invoke(spec);
+ }
+
+ @Override
+ protected KeySpec engineGetKeySpec(
+ SecretKey secretKey, @SuppressWarnings("rawtypes") Class aClass)
+ throws InvalidKeySpecException {
+ if (secretKey == null) {
+ throw new InvalidKeySpecException("Null KeySpec");
+ }
+ throw new NotImplementedException();
+ }
+
+ @Override
+ protected SecretKey engineTranslateKey(SecretKey secretKey) throws InvalidKeyException {
+ if (secretKey == null) {
+ throw new InvalidKeyException("Null SecretKey");
+ }
+ throw new NotImplementedException();
+ }
+
+ private static class ScryptKey implements SecretKey {
+ private static final long serialVersionUID = 2024924811854189128L;
+ private final byte[] key;
+
+ public ScryptKey(byte[] key) {
+ this.key = key;
+ }
+
+ @Override
+ public String getAlgorithm() {
+ // capitalised because BouncyCastle does it.
+ return "SCRYPT";
+ }
+
+ @Override
+ public String getFormat() {
+ return "RAW";
+ }
+
+ @Override
+ public byte[] getEncoded() {
+ return key;
+ }
+ }
+
+ private static class NotImplementedException extends RuntimeException {
+ NotImplementedException() {
+ super("Not implemented");
+ }
+ }
+}
diff --git a/common/src/test/java/org/conscrypt/javax/crypto/ScryptTest.java b/common/src/test/java/org/conscrypt/javax/crypto/ScryptTest.java
new file mode 100644
index 00000000..f0183a81
--- /dev/null
+++ b/common/src/test/java/org/conscrypt/javax/crypto/ScryptTest.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed 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.conscrypt.javax.crypto;
+
+import static org.conscrypt.TestUtils.decodeHex;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.spec.KeySpec;
+import java.util.List;
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.SecretKeySpec;
+import org.conscrypt.ScryptKeySpec;
+import org.conscrypt.TestUtils;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class ScryptTest {
+ private final List<String[]> testVectors = readTestVectors();
+
+ // Column indices in test vector CSV file
+ private static final int PASSWORD_INDEX = 0;
+ private static final int SALT_INDEX = 1;
+ private static final int N_INDEX = 2;
+ private static final int R_INDEX = 3;
+ private static final int P_INDEX = 4;
+ private static final int KEY_INDEX = 5;
+
+ @BeforeClass
+ public static void setUp() {
+ TestUtils.assumeAllowsUnsignedCrypto();
+ }
+
+ @Test
+ public void smokeTest() throws Exception {
+ SecretKeyFactory factory = SecretKeyFactory.getInstance("Scrypt");
+ assertNotNull(factory);
+
+ // One of the test vectors from RFC 7914
+ ScryptKeySpec spec =
+ new ScryptKeySpec(
+ "password".toCharArray(),
+ "NaCl".getBytes(StandardCharsets.UTF_8),
+ 1024,
+ 8,
+ 16,
+ 512);
+ SecretKey key = factory.generateSecret(spec);
+ assertNotNull(key);
+
+ assertArrayEquals(
+ decodeHex("fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b3731622eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640"),
+ key.getEncoded());
+
+ // Convert for use with AES
+ SecretKeySpec aesKey = makeAesKeySpec(key);
+
+ // Make sure we can actually use the result
+ checkKeyIsUsableWithAes(aesKey);
+ }
+
+ @Test
+ public void duckTypingTest() throws Exception {
+ SecretKeyFactory factory = SecretKeyFactory.getInstance("Scrypt");
+ assertNotNull(factory);
+
+ // One of the test vectors from RFC 7914
+ KeySpec spec =
+ new MyPrivateKeySpec(
+ "password".toCharArray(), "NaCl".getBytes(StandardCharsets.UTF_8), 1024, 8, 16, 512);
+ SecretKey key = factory.generateSecret(spec);
+ assertNotNull(key);
+
+ assertArrayEquals(
+ decodeHex("fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b3731622eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640"),
+ key.getEncoded());
+ }
+
+ private SecretKeySpec makeAesKeySpec(SecretKey key) {
+ assertEquals("RAW", key.getFormat());
+ byte[] bytes = key.getEncoded();
+ // Truncate to first 32 bytes if necessary
+ int len = Math.min(32, bytes.length);
+ return new SecretKeySpec(bytes, 0, len, "AES");
+ }
+
+ private void checkKeyIsUsableWithAes(SecretKeySpec spec) throws Exception {
+ // Terrible encryption mode but saves messing with IVs and padding which don't matter here.
+ Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
+
+ cipher.init(Cipher.ENCRYPT_MODE, spec);
+ byte[] input = "The quick brown fox jumps over the lazy dog".getBytes(StandardCharsets.UTF_8);
+ byte[] encrypted = cipher.doFinal(input);
+ assertNotEquals(encrypted[0], input[0]);
+
+ cipher.init(Cipher.DECRYPT_MODE, spec);
+ byte[] decrypted = cipher.doFinal(encrypted);
+ assertArrayEquals(input, decrypted);
+ }
+
+ @Test
+ public void knownAnswerTest() throws Exception {
+ for (String[] entry : testVectors) {
+ char[] password = entry[PASSWORD_INDEX].toCharArray();
+ byte[] salt = entry[SALT_INDEX].getBytes(StandardCharsets.UTF_8);
+ int n = Integer.parseInt(entry[N_INDEX]);
+ int r = Integer.parseInt(entry[R_INDEX]);
+ int p = Integer.parseInt(entry[P_INDEX]);
+ byte[] expectedBytes = decodeHex(entry[KEY_INDEX]);
+
+ ScryptKeySpec spec = new ScryptKeySpec(password, salt, n, r, p, expectedBytes.length * 8);
+ SecretKeyFactory factory = SecretKeyFactory.getInstance("Scrypt");
+ SecretKey key = factory.generateSecret(spec);
+ assertNotNull(key);
+ assertArrayEquals(expectedBytes, key.getEncoded());
+ }
+ }
+
+ private List<String[]> readTestVectors() {
+ try {
+ return TestUtils.readCsvResource("crypto/scrypt.csv");
+
+ } catch (IOException e) {
+ throw new AssertionError("Unable to load Scrypt test vectors", e);
+ }
+ }
+
+ public static class MyPrivateKeySpec implements KeySpec {
+ private final char[] password;
+ private final byte[] salt;
+ private final int n;
+ private final int r;
+ private final int p;
+ private final int keyOutputBits;
+
+ public MyPrivateKeySpec(char[] password, byte[] salt, int n, int r, int p, int keyOutputBits) {
+ this.password = password;
+ this.salt = salt;
+ this.n = n;
+ this.r = r;
+ this.p = p;
+ this.keyOutputBits = keyOutputBits;
+ }
+
+ public char[] getPassword() {
+ return password;
+ }
+
+ public byte[] getSalt() {
+ return salt;
+ }
+
+ public int getCostParameter() {
+ return n;
+ }
+
+ public int getBlockSize() {
+ return r;
+ }
+
+ public int getParallelizationParameter() {
+ return p;
+ }
+
+ public int getKeyLength() {
+ return keyOutputBits;
+ }
+ }
+}
diff --git a/common/src/test/resources/crypto/scrypt.csv b/common/src/test/resources/crypto/scrypt.csv
new file mode 100644
index 00000000..88bdfb86
--- /dev/null
+++ b/common/src/test/resources/crypto/scrypt.csv
@@ -0,0 +1,8 @@
+# Scrypt test vectors from RFC 7914
+# Data is in the format:
+# password,salt,n,r,p,key
+,,16,1,1,77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906
+password,NaCl,1024,8,16,fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b3731622eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640
+pleaseletmein,SodiumChloride,16384,8,1,7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2d5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887
+# Following vector exceeds memory limits with defaults
+# pleaseletmein,SodiumChloride,1048576,8,1,2101cb9b6a511aaeaddbbe09cf70f881ec568d574a2ffd4dabe5ee9820adaa478e56fd8f4ba5d09ffa1c6d927c40f4c337304049e8a952fbcbf45c6fa77a41a4
diff --git a/repackaged/common/src/main/java/com/android/org/conscrypt/NativeCrypto.java b/repackaged/common/src/main/java/com/android/org/conscrypt/NativeCrypto.java
index 81906bd9..80f3053c 100644
--- a/repackaged/common/src/main/java/com/android/org/conscrypt/NativeCrypto.java
+++ b/repackaged/common/src/main/java/com/android/org/conscrypt/NativeCrypto.java
@@ -1546,6 +1546,12 @@ public final class NativeCrypto {
throws IOException;
/**
+ * Generates a key from a password and salt using Scrypt.
+ */
+ static native byte[] Scrypt_generate_key(
+ byte[] password, byte[] salt, long n, long r, long p, int key_len);
+
+ /**
* Return {@code true} if BoringSSL has been built in FIPS mode.
*/
static native boolean usesBoringSsl_FIPS_mode();
diff --git a/repackaged/common/src/main/java/com/android/org/conscrypt/OpenSSLProvider.java b/repackaged/common/src/main/java/com/android/org/conscrypt/OpenSSLProvider.java
index 48912950..fda60e88 100644
--- a/repackaged/common/src/main/java/com/android/org/conscrypt/OpenSSLProvider.java
+++ b/repackaged/common/src/main/java/com/android/org/conscrypt/OpenSSLProvider.java
@@ -224,6 +224,9 @@ public final class OpenSSLProvider extends Provider {
/* == SecretKeyFactory == */
put("SecretKeyFactory.DESEDE", PREFIX + "DESEDESecretKeyFactory");
put("Alg.Alias.SecretKeyFactory.TDEA", "DESEDE");
+ put("SecretKeyFactory.Scrypt", PREFIX + "ScryptSecretKeyFactory");
+ put("SecretKeyFactory.1.3.6.1.4.1.11591.4.11", PREFIX + "ScryptSecretKeyFactory");
+ put("SecretKeyFactory.OID.1.3.6.1.4.1.11591.4.11", PREFIX + "ScryptSecretKeyFactory");
/* == KeyAgreement == */
putECDHKeyAgreementImplClass("OpenSSLECDHKeyAgreement");
diff --git a/repackaged/common/src/main/java/com/android/org/conscrypt/ScryptKeySpec.java b/repackaged/common/src/main/java/com/android/org/conscrypt/ScryptKeySpec.java
new file mode 100644
index 00000000..d6e12ddc
--- /dev/null
+++ b/repackaged/common/src/main/java/com/android/org/conscrypt/ScryptKeySpec.java
@@ -0,0 +1,74 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed 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 com.android.org.conscrypt;
+
+import java.security.spec.KeySpec;
+
+/**
+ * Mirrors the <a
+ * href="https://javadoc.io/static/org.bouncycastle/bcprov-jdk15on/1.68/org/bouncycastle/jcajce/spec/ScryptKeySpec.html">class
+ * of the same name</a> from BouncyCastle.
+ * @hide This class is not part of the Android public SDK API
+ */
+public class ScryptKeySpec implements KeySpec {
+ private final char[] password;
+ private final byte[] salt;
+ private final int costParameter;
+ private final int blockSize;
+ private final int parallelizationParameter;
+ private final int keyOutputBits;
+
+ public ScryptKeySpec(
+ char[] password,
+ byte[] salt,
+ int costParameter,
+ int blockSize,
+ int parallelizationParameter,
+ int keyOutputBits) {
+ this.password = password;
+ this.salt = salt;
+ this.costParameter = costParameter;
+ this.blockSize = blockSize;
+ this.parallelizationParameter = parallelizationParameter;
+ this.keyOutputBits = keyOutputBits;
+ }
+
+ public char[] getPassword() {
+ return password;
+ }
+
+ public byte[] getSalt() {
+ return salt;
+ }
+
+ public int getCostParameter() {
+ return costParameter;
+ }
+
+ public int getBlockSize() {
+ return blockSize;
+ }
+
+ public int getParallelizationParameter() {
+ return parallelizationParameter;
+ }
+
+ public int getKeyLength() {
+ return keyOutputBits;
+ }
+}
diff --git a/repackaged/common/src/main/java/com/android/org/conscrypt/ScryptSecretKeyFactory.java b/repackaged/common/src/main/java/com/android/org/conscrypt/ScryptSecretKeyFactory.java
new file mode 100644
index 00000000..bd1c0543
--- /dev/null
+++ b/repackaged/common/src/main/java/com/android/org/conscrypt/ScryptSecretKeyFactory.java
@@ -0,0 +1,133 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed 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 com.android.org.conscrypt;
+
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.security.InvalidKeyException;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactorySpi;
+
+/**
+ * @hide This class is not part of the Android public SDK API
+ */
+@Internal
+public class ScryptSecretKeyFactory extends SecretKeyFactorySpi {
+
+ @Override
+ protected SecretKey engineGenerateSecret(KeySpec inKeySpec) throws InvalidKeySpecException {
+
+ char[] password;
+ byte[] salt;
+ int n, r, p, keyOutputBits;
+
+ if (inKeySpec instanceof ScryptKeySpec) {
+ ScryptKeySpec spec = (ScryptKeySpec) inKeySpec;
+ password = spec.getPassword();
+ salt = spec.getSalt();
+ n = spec.getCostParameter();
+ r = spec.getBlockSize();
+ p = spec.getParallelizationParameter();
+ keyOutputBits = spec.getKeyLength();
+ } else {
+ // Extract parameters from any `KeySpec` that has getters with the correct name. This allows,
+ // for example, code to use BouncyCastle's KeySpec with the conscrypt provider.
+ try {
+ password = (char[]) getValue(inKeySpec, "getPassword");
+ salt = (byte[]) getValue(inKeySpec, "getSalt");
+ n = (int) getValue(inKeySpec, "getCostParameter");
+ r = (int) getValue(inKeySpec, "getBlockSize");
+ p = (int) getValue(inKeySpec, "getParallelizationParameter");
+ keyOutputBits = (int) getValue(inKeySpec, "getKeyLength");
+ } catch (Exception e) {
+ throw new InvalidKeySpecException("Not a valid scrypt KeySpec", e);
+ }
+ }
+
+ if (keyOutputBits % 8 != 0) {
+ throw new InvalidKeySpecException("Cannot produce fractional-byte outputs");
+ }
+
+ try {
+ return new ScryptKey(
+ NativeCrypto.Scrypt_generate_key(
+ new String(password).getBytes("UTF-8"), salt, n, r, p, keyOutputBits / 8));
+ } catch (UnsupportedEncodingException e) {
+ // Impossible according to the Java docs: UTF-8 is always supported.
+ throw new RuntimeException(e);
+ }
+ }
+
+ private Object getValue(KeySpec spec, String methodName)
+ throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+ Method method = spec.getClass().getMethod(methodName, (Class<?>[]) null);
+ return method.invoke(spec);
+ }
+
+ @Override
+ protected KeySpec engineGetKeySpec(
+ SecretKey secretKey, @SuppressWarnings("rawtypes") Class aClass)
+ throws InvalidKeySpecException {
+ if (secretKey == null) {
+ throw new InvalidKeySpecException("Null KeySpec");
+ }
+ throw new NotImplementedException();
+ }
+
+ @Override
+ protected SecretKey engineTranslateKey(SecretKey secretKey) throws InvalidKeyException {
+ if (secretKey == null) {
+ throw new InvalidKeyException("Null SecretKey");
+ }
+ throw new NotImplementedException();
+ }
+
+ private static class ScryptKey implements SecretKey {
+ private static final long serialVersionUID = 2024924811854189128L;
+ private final byte[] key;
+
+ public ScryptKey(byte[] key) {
+ this.key = key;
+ }
+
+ @Override
+ public String getAlgorithm() {
+ // capitalised because BouncyCastle does it.
+ return "SCRYPT";
+ }
+
+ @Override
+ public String getFormat() {
+ return "RAW";
+ }
+
+ @Override
+ public byte[] getEncoded() {
+ return key;
+ }
+ }
+
+ private static class NotImplementedException extends RuntimeException {
+ NotImplementedException() {
+ super("Not implemented");
+ }
+ }
+}
diff --git a/repackaged/common/src/test/java/com/android/org/conscrypt/javax/crypto/ScryptTest.java b/repackaged/common/src/test/java/com/android/org/conscrypt/javax/crypto/ScryptTest.java
new file mode 100644
index 00000000..e5721673
--- /dev/null
+++ b/repackaged/common/src/test/java/com/android/org/conscrypt/javax/crypto/ScryptTest.java
@@ -0,0 +1,199 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed 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 com.android.org.conscrypt.javax.crypto;
+
+import static com.android.org.conscrypt.TestUtils.decodeHex;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.spec.KeySpec;
+import java.util.List;
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.SecretKeySpec;
+import com.android.org.conscrypt.ScryptKeySpec;
+import com.android.org.conscrypt.TestUtils;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * @hide This class is not part of the Android public SDK API
+ */
+@RunWith(JUnit4.class)
+public final class ScryptTest {
+ private final List<String[]> testVectors = readTestVectors();
+
+ // Column indices in test vector CSV file
+ private static final int PASSWORD_INDEX = 0;
+ private static final int SALT_INDEX = 1;
+ private static final int N_INDEX = 2;
+ private static final int R_INDEX = 3;
+ private static final int P_INDEX = 4;
+ private static final int KEY_INDEX = 5;
+
+ @BeforeClass
+ public static void setUp() {
+ TestUtils.assumeAllowsUnsignedCrypto();
+ }
+
+ @Test
+ public void smokeTest() throws Exception {
+ SecretKeyFactory factory = SecretKeyFactory.getInstance("Scrypt");
+ assertNotNull(factory);
+
+ // One of the test vectors from RFC 7914
+ ScryptKeySpec spec =
+ new ScryptKeySpec(
+ "password".toCharArray(),
+ "NaCl".getBytes(StandardCharsets.UTF_8),
+ 1024,
+ 8,
+ 16,
+ 512);
+ SecretKey key = factory.generateSecret(spec);
+ assertNotNull(key);
+
+ assertArrayEquals(
+ decodeHex("fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b3731622eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640"),
+ key.getEncoded());
+
+ // Convert for use with AES
+ SecretKeySpec aesKey = makeAesKeySpec(key);
+
+ // Make sure we can actually use the result
+ checkKeyIsUsableWithAes(aesKey);
+ }
+
+ @Test
+ public void duckTypingTest() throws Exception {
+ SecretKeyFactory factory = SecretKeyFactory.getInstance("Scrypt");
+ assertNotNull(factory);
+
+ // One of the test vectors from RFC 7914
+ KeySpec spec =
+ new MyPrivateKeySpec(
+ "password".toCharArray(), "NaCl".getBytes(StandardCharsets.UTF_8), 1024, 8, 16, 512);
+ SecretKey key = factory.generateSecret(spec);
+ assertNotNull(key);
+
+ assertArrayEquals(
+ decodeHex("fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b3731622eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640"),
+ key.getEncoded());
+ }
+
+ private SecretKeySpec makeAesKeySpec(SecretKey key) {
+ assertEquals("RAW", key.getFormat());
+ byte[] bytes = key.getEncoded();
+ // Truncate to first 32 bytes if necessary
+ int len = Math.min(32, bytes.length);
+ return new SecretKeySpec(bytes, 0, len, "AES");
+ }
+
+ private void checkKeyIsUsableWithAes(SecretKeySpec spec) throws Exception {
+ // Terrible encryption mode but saves messing with IVs and padding which don't matter here.
+ Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
+
+ cipher.init(Cipher.ENCRYPT_MODE, spec);
+ byte[] input = "The quick brown fox jumps over the lazy dog".getBytes(StandardCharsets.UTF_8);
+ byte[] encrypted = cipher.doFinal(input);
+ assertNotEquals(encrypted[0], input[0]);
+
+ cipher.init(Cipher.DECRYPT_MODE, spec);
+ byte[] decrypted = cipher.doFinal(encrypted);
+ assertArrayEquals(input, decrypted);
+ }
+
+ @Test
+ public void knownAnswerTest() throws Exception {
+ for (String[] entry : testVectors) {
+ char[] password = entry[PASSWORD_INDEX].toCharArray();
+ byte[] salt = entry[SALT_INDEX].getBytes(StandardCharsets.UTF_8);
+ int n = Integer.parseInt(entry[N_INDEX]);
+ int r = Integer.parseInt(entry[R_INDEX]);
+ int p = Integer.parseInt(entry[P_INDEX]);
+ byte[] expectedBytes = decodeHex(entry[KEY_INDEX]);
+
+ ScryptKeySpec spec = new ScryptKeySpec(password, salt, n, r, p, expectedBytes.length * 8);
+ SecretKeyFactory factory = SecretKeyFactory.getInstance("Scrypt");
+ SecretKey key = factory.generateSecret(spec);
+ assertNotNull(key);
+ assertArrayEquals(expectedBytes, key.getEncoded());
+ }
+ }
+
+ private List<String[]> readTestVectors() {
+ try {
+ return TestUtils.readCsvResource("crypto/scrypt.csv");
+
+ } catch (IOException e) {
+ throw new AssertionError("Unable to load Scrypt test vectors", e);
+ }
+ }
+
+ /**
+ * @hide This class is not part of the Android public SDK API
+ */
+ public static class MyPrivateKeySpec implements KeySpec {
+ private final char[] password;
+ private final byte[] salt;
+ private final int n;
+ private final int r;
+ private final int p;
+ private final int keyOutputBits;
+
+ public MyPrivateKeySpec(char[] password, byte[] salt, int n, int r, int p, int keyOutputBits) {
+ this.password = password;
+ this.salt = salt;
+ this.n = n;
+ this.r = r;
+ this.p = p;
+ this.keyOutputBits = keyOutputBits;
+ }
+
+ public char[] getPassword() {
+ return password;
+ }
+
+ public byte[] getSalt() {
+ return salt;
+ }
+
+ public int getCostParameter() {
+ return n;
+ }
+
+ public int getBlockSize() {
+ return r;
+ }
+
+ public int getParallelizationParameter() {
+ return p;
+ }
+
+ public int getKeyLength() {
+ return keyOutputBits;
+ }
+ }
+}