From 52c6ce484913f6efa9a0c20a67f2a1478cc64159 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 15 Feb 2024 17:42:19 -0500 Subject: [PATCH] Updates scrypt lib to support empty keys. --- quartz/build.gradle | 3 - .../com/vitorpamplona/quartz/NIP49Test.kt | 4 + .../com/vitorpamplona/quartz/crypto/Nip49.kt | 1 - .../vitorpamplona/quartz/crypto/PBKDF.java | 87 +++++++ .../vitorpamplona/quartz/crypto/SCrypt.java | 188 +++++++++++++++ .../quartz/crypto/SecretKeyOrEmptySpec.java | 228 ++++++++++++++++++ 6 files changed, 507 insertions(+), 4 deletions(-) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/crypto/PBKDF.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/crypto/SCrypt.java create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/crypto/SecretKeyOrEmptySpec.java diff --git a/quartz/build.gradle b/quartz/build.gradle index 5145a3faf..8f24aca24 100644 --- a/quartz/build.gradle +++ b/quartz/build.gradle @@ -59,9 +59,6 @@ dependencies { // immutable collections to avoid recomposition api('org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7') - // scrypt for NIP-49 - api('com.lambdaworks:scrypt:1.4.0') - // Parses URLs from Text: api "io.github.url-detector:url-detector:0.1.23" diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP49Test.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP49Test.kt index 01825904a..3513020d8 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP49Test.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP49Test.kt @@ -46,6 +46,10 @@ public class NIP49Test { Nip49TestCase(".ksjabdk.aselqwe", "14c226dbdd865d5e1645e72c7470fd0a17feb42cc87b750bab6538171b3a3f8a", 7, 0x00), Nip49TestCase("skjdaklrnçurbç l", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 8, 0x01), Nip49TestCase("777z7z7z7z7z7z7z", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 9, 0x02), + Nip49TestCase("", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 4, 0x00), + Nip49TestCase("", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 5, 0x01), + Nip49TestCase("", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 1, 0x00), + Nip49TestCase("", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 9, 0x01), ) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip49.kt b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip49.kt index 7863fb953..199965227 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip49.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip49.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.quartz.crypto import android.util.Log import com.goterl.lazysodium.LazySodiumAndroid import com.goterl.lazysodium.SodiumAndroid -import com.lambdaworks.crypto.SCrypt import com.vitorpamplona.quartz.encoders.Bech32 import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.bechToBytes diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/PBKDF.java b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/PBKDF.java new file mode 100644 index 000000000..638a8b315 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/PBKDF.java @@ -0,0 +1,87 @@ +// Copyright (C) 2011 - Will Glozer. All rights reserved. + +package com.vitorpamplona.quartz.crypto; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.security.GeneralSecurityException; +import static java.lang.System.arraycopy; + +/** + * An implementation of the Password-Based Key Derivation Function as specified + * in RFC 2898. + * + * @author Will Glozer + */ +public class PBKDF { + /** + * Implementation of PBKDF2 (RFC2898). + * + * @param alg HMAC algorithm to use. + * @param P Password. + * @param S Salt. + * @param c Iteration count. + * @param dkLen Intended length, in octets, of the derived key. + * + * @return The derived key. + * + * @throws GeneralSecurityException + */ + public static byte[] pbkdf2(String alg, byte[] P, byte[] S, int c, int dkLen) throws GeneralSecurityException { + Mac mac = Mac.getInstance(alg); + mac.init(new SecretKeySpec(P, alg)); + byte[] DK = new byte[dkLen]; + pbkdf2(mac, S, c, DK, dkLen); + return DK; + } + + /** + * Implementation of PBKDF2 (RFC2898). + * + * @param mac Pre-initialized {@link Mac} instance to use. + * @param S Salt. + * @param c Iteration count. + * @param DK Byte array that derived key will be placed in. + * @param dkLen Intended length, in octets, of the derived key. + * + * @throws GeneralSecurityException + */ + public static void pbkdf2(Mac mac, byte[] S, int c, byte[] DK, int dkLen) throws GeneralSecurityException { + int hLen = mac.getMacLength(); + + if (dkLen > (Math.pow(2, 32) - 1) * hLen) { + throw new GeneralSecurityException("Requested key length too long"); + } + + byte[] U = new byte[hLen]; + byte[] T = new byte[hLen]; + byte[] block1 = new byte[S.length + 4]; + + int l = (int) Math.ceil((double) dkLen / hLen); + int r = dkLen - (l - 1) * hLen; + + arraycopy(S, 0, block1, 0, S.length); + + for (int i = 1; i <= l; i++) { + block1[S.length + 0] = (byte) (i >> 24 & 0xff); + block1[S.length + 1] = (byte) (i >> 16 & 0xff); + block1[S.length + 2] = (byte) (i >> 8 & 0xff); + block1[S.length + 3] = (byte) (i >> 0 & 0xff); + + mac.update(block1); + mac.doFinal(U, 0); + arraycopy(U, 0, T, 0, hLen); + + for (int j = 1; j < c; j++) { + mac.update(U); + mac.doFinal(U, 0); + + for (int k = 0; k < hLen; k++) { + T[k] ^= U[k]; + } + } + + arraycopy(T, 0, DK, (i - 1) * hLen, (i == l ? r : hLen)); + } + } +} \ No newline at end of file diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/SCrypt.java b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/SCrypt.java new file mode 100644 index 000000000..c464013a2 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/SCrypt.java @@ -0,0 +1,188 @@ +// Copyright (C) 2011 - Will Glozer. All rights reserved. + +package com.vitorpamplona.quartz.crypto; + +import javax.crypto.Mac; +import java.security.GeneralSecurityException; + +import static java.lang.Integer.MAX_VALUE; +import static java.lang.System.arraycopy; + +/** + * An implementation of the scrypt + * key derivation function. This class will attempt to load a native library + * containing the optimized C implementation from + * http://www.tarsnap.com/scrypt.html and + * fall back to the pure Java version if that fails. + * + * @author Will Glozer + */ +public class SCrypt { + + /** + * Implementation of the scrypt KDF. + * + * @param passwd Password. + * @param salt Salt. + * @param N CPU cost parameter. + * @param r Memory cost parameter. + * @param p Parallelization parameter. + * @param dkLen Intended length of the derived key. + * + * @return The derived key. + * + * @throws GeneralSecurityException when HMAC_SHA256 is not available. + */ + public static byte[] scrypt(byte[] passwd, byte[] salt, int N, int r, int p, int dkLen) throws GeneralSecurityException { + return scryptJ(passwd, salt, N, r, p, dkLen); + } + + /** + * Pure Java implementation of the scrypt KDF. + * + * @param passwd Password. + * @param salt Salt. + * @param N CPU cost parameter. + * @param r Memory cost parameter. + * @param p Parallelization parameter. + * @param dkLen Intended length of the derived key. + * + * @return The derived key. + * + * @throws GeneralSecurityException when HMAC_SHA256 is not available. + */ + public static byte[] scryptJ(byte[] passwd, byte[] salt, int N, int r, int p, int dkLen) throws GeneralSecurityException { + if (N < 2 || (N & (N - 1)) != 0) throw new IllegalArgumentException("N must be a power of 2 greater than 1"); + + if (N > MAX_VALUE / 128 / r) throw new IllegalArgumentException("Parameter N is too large"); + if (r > MAX_VALUE / 128 / p) throw new IllegalArgumentException("Parameter r is too large"); + + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeyOrEmptySpec(passwd, "HmacSHA256")); + + byte[] DK = new byte[dkLen]; + + byte[] B = new byte[128 * r * p]; + byte[] XY = new byte[256 * r]; + byte[] V = new byte[128 * r * N]; + int i; + + PBKDF.pbkdf2(mac, salt, 1, B, p * 128 * r); + + for (i = 0; i < p; i++) { + smix(B, i * 128 * r, r, N, V, XY); + } + + PBKDF.pbkdf2(mac, B, 1, DK, dkLen); + + return DK; + } + + public static void smix(byte[] B, int Bi, int r, int N, byte[] V, byte[] XY) { + int Xi = 0; + int Yi = 128 * r; + int i; + + arraycopy(B, Bi, XY, Xi, 128 * r); + + for (i = 0; i < N; i++) { + arraycopy(XY, Xi, V, i * (128 * r), 128 * r); + blockmix_salsa8(XY, Xi, Yi, r); + } + + for (i = 0; i < N; i++) { + int j = integerify(XY, Xi, r) & (N - 1); + blockxor(V, j * (128 * r), XY, Xi, 128 * r); + blockmix_salsa8(XY, Xi, Yi, r); + } + + arraycopy(XY, Xi, B, Bi, 128 * r); + } + + public static void blockmix_salsa8(byte[] BY, int Bi, int Yi, int r) { + byte[] X = new byte[64]; + int i; + + arraycopy(BY, Bi + (2 * r - 1) * 64, X, 0, 64); + + for (i = 0; i < 2 * r; i++) { + blockxor(BY, i * 64, X, 0, 64); + salsa20_8(X); + arraycopy(X, 0, BY, Yi + (i * 64), 64); + } + + for (i = 0; i < r; i++) { + arraycopy(BY, Yi + (i * 2) * 64, BY, Bi + (i * 64), 64); + } + + for (i = 0; i < r; i++) { + arraycopy(BY, Yi + (i * 2 + 1) * 64, BY, Bi + (i + r) * 64, 64); + } + } + + public static int R(int a, int b) { + return (a << b) | (a >>> (32 - b)); + } + + public static void salsa20_8(byte[] B) { + int[] B32 = new int[16]; + int[] x = new int[16]; + int i; + + for (i = 0; i < 16; i++) { + B32[i] = (B[i * 4 + 0] & 0xff) << 0; + B32[i] |= (B[i * 4 + 1] & 0xff) << 8; + B32[i] |= (B[i * 4 + 2] & 0xff) << 16; + B32[i] |= (B[i * 4 + 3] & 0xff) << 24; + } + + arraycopy(B32, 0, x, 0, 16); + + for (i = 8; i > 0; i -= 2) { + x[ 4] ^= R(x[ 0]+x[12], 7); x[ 8] ^= R(x[ 4]+x[ 0], 9); + x[12] ^= R(x[ 8]+x[ 4],13); x[ 0] ^= R(x[12]+x[ 8],18); + x[ 9] ^= R(x[ 5]+x[ 1], 7); x[13] ^= R(x[ 9]+x[ 5], 9); + x[ 1] ^= R(x[13]+x[ 9],13); x[ 5] ^= R(x[ 1]+x[13],18); + x[14] ^= R(x[10]+x[ 6], 7); x[ 2] ^= R(x[14]+x[10], 9); + x[ 6] ^= R(x[ 2]+x[14],13); x[10] ^= R(x[ 6]+x[ 2],18); + x[ 3] ^= R(x[15]+x[11], 7); x[ 7] ^= R(x[ 3]+x[15], 9); + x[11] ^= R(x[ 7]+x[ 3],13); x[15] ^= R(x[11]+x[ 7],18); + x[ 1] ^= R(x[ 0]+x[ 3], 7); x[ 2] ^= R(x[ 1]+x[ 0], 9); + x[ 3] ^= R(x[ 2]+x[ 1],13); x[ 0] ^= R(x[ 3]+x[ 2],18); + x[ 6] ^= R(x[ 5]+x[ 4], 7); x[ 7] ^= R(x[ 6]+x[ 5], 9); + x[ 4] ^= R(x[ 7]+x[ 6],13); x[ 5] ^= R(x[ 4]+x[ 7],18); + x[11] ^= R(x[10]+x[ 9], 7); x[ 8] ^= R(x[11]+x[10], 9); + x[ 9] ^= R(x[ 8]+x[11],13); x[10] ^= R(x[ 9]+x[ 8],18); + x[12] ^= R(x[15]+x[14], 7); x[13] ^= R(x[12]+x[15], 9); + x[14] ^= R(x[13]+x[12],13); x[15] ^= R(x[14]+x[13],18); + } + + for (i = 0; i < 16; ++i) B32[i] = x[i] + B32[i]; + + for (i = 0; i < 16; i++) { + B[i * 4 + 0] = (byte) (B32[i] >> 0 & 0xff); + B[i * 4 + 1] = (byte) (B32[i] >> 8 & 0xff); + B[i * 4 + 2] = (byte) (B32[i] >> 16 & 0xff); + B[i * 4 + 3] = (byte) (B32[i] >> 24 & 0xff); + } + } + + public static void blockxor(byte[] S, int Si, byte[] D, int Di, int len) { + for (int i = 0; i < len; i++) { + D[Di + i] ^= S[Si + i]; + } + } + + public static int integerify(byte[] B, int Bi, int r) { + int n; + + Bi += (2 * r - 1) * 64; + + n = (B[Bi + 0] & 0xff) << 0; + n |= (B[Bi + 1] & 0xff) << 8; + n |= (B[Bi + 2] & 0xff) << 16; + n |= (B[Bi + 3] & 0xff) << 24; + + return n; + } +} \ No newline at end of file diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/SecretKeyOrEmptySpec.java b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/SecretKeyOrEmptySpec.java new file mode 100644 index 000000000..7e3766bdc --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/SecretKeyOrEmptySpec.java @@ -0,0 +1,228 @@ +/* + * Copyright (c) 1998, 2015, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.vitorpamplona.quartz.crypto; + +import java.security.MessageDigest; +import java.security.spec.KeySpec; +import java.util.Locale; +import javax.crypto.SecretKey; + +/** + * This class specifies a secret key in a provider-independent fashion. + * + *

It can be used to construct a SecretKey from a byte array, + * without having to go through a (provider-based) + * SecretKeyFactory. + * + *

This class is only useful for raw secret keys that can be represented as + * a byte array and have no key parameters associated with them, e.g., DES or + * Triple DES keys. + * + * @author Jan Luehe + * + * @see javax.crypto.SecretKey + * @see javax.crypto.SecretKeyFactory + * @since 1.4 + */ +public class SecretKeyOrEmptySpec implements KeySpec, SecretKey { + + private static final long serialVersionUID = 6577238317307289933L; + + /** + * The secret key. + * + * @serial + */ + private byte[] key; + + /** + * The name of the algorithm associated with this key. + * + * @serial + */ + private String algorithm; + + /** + * Constructs a secret key from the given byte array. + * + *

This constructor does not check if the given bytes indeed specify a + * secret key of the specified algorithm. For example, if the algorithm is + * DES, this constructor does not check if key is 8 bytes + * long, and also does not check for weak or semi-weak keys. + * In order for those checks to be performed, an algorithm-specific + * key specification class (in this case: + * {@link DESKeySpec DESKeySpec}) + * should be used. + * + * @param key the key material of the secret key. The contents of + * the array are copied to protect against subsequent modification. + * @param algorithm the name of the secret-key algorithm to be associated + * with the given key material. + * See Appendix A in the + * Java Cryptography Architecture Reference Guide + * for information about standard algorithm names. + * @exception IllegalArgumentException if algorithm + * is null or key is null or empty. + */ + public SecretKeyOrEmptySpec(byte[] key, String algorithm) { + if (key == null || algorithm == null) { + throw new IllegalArgumentException("Missing argument"); + } + this.key = key.clone(); + this.algorithm = algorithm; + } + + /** + * Constructs a secret key from the given byte array, using the first + * len bytes of key, starting at + * offset inclusive. + * + *

The bytes that constitute the secret key are + * those between key[offset] and + * key[offset+len-1] inclusive. + * + *

This constructor does not check if the given bytes indeed specify a + * secret key of the specified algorithm. For example, if the algorithm is + * DES, this constructor does not check if key is 8 bytes + * long, and also does not check for weak or semi-weak keys. + * In order for those checks to be performed, an algorithm-specific key + * specification class (in this case: + * {@link DESKeySpec DESKeySpec}) + * must be used. + * + * @param key the key material of the secret key. The first + * len bytes of the array beginning at + * offset inclusive are copied to protect + * against subsequent modification. + * @param offset the offset in key where the key material + * starts. + * @param len the length of the key material. + * @param algorithm the name of the secret-key algorithm to be associated + * with the given key material. + * See Appendix A in the + * Java Cryptography Architecture Reference Guide + * for information about standard algorithm names. + * @exception IllegalArgumentException if algorithm + * is null or key is null, empty, or too short, + * i.e. {@code key.length-offsetoffset or len index bytes outside the + * key. + */ + public SecretKeyOrEmptySpec(byte[] key, int offset, int len, String algorithm) { + if (key == null || algorithm == null) { + throw new IllegalArgumentException("Missing argument"); + } + if (key.length-offset < len) { + throw new IllegalArgumentException + ("Invalid offset/length combination"); + } + if (len < 0) { + throw new ArrayIndexOutOfBoundsException("len is negative"); + } + this.key = new byte[len]; + System.arraycopy(key, offset, this.key, 0, len); + this.algorithm = algorithm; + } + + /** + * Returns the name of the algorithm associated with this secret key. + * + * @return the secret key algorithm. + */ + public String getAlgorithm() { + return this.algorithm; + } + + /** + * Returns the name of the encoding format for this secret key. + * + * @return the string "RAW". + */ + public String getFormat() { + return "RAW"; + } + + /** + * Returns the key material of this secret key. + * + * @return the key material. Returns a new array + * each time this method is called. + */ + public byte[] getEncoded() { + return this.key.clone(); + } + + /** + * Calculates a hash code value for the object. + * Objects that are equal will also have the same hashcode. + */ + public int hashCode() { + int retval = 0; + for (int i = 1; i < this.key.length; i++) { + retval += this.key[i] * i; + } + if (this.algorithm.equalsIgnoreCase("TripleDES")) + return (retval ^= "desede".hashCode()); + else + return (retval ^= + this.algorithm.toLowerCase(Locale.ENGLISH).hashCode()); + } + + /** + * Tests for equality between the specified object and this + * object. Two SecretKeySpec objects are considered equal if + * they are both SecretKey instances which have the + * same case-insensitive algorithm name and key encoding. + * + * @param obj the object to test for equality with this object. + * + * @return true if the objects are considered equal, false if + * obj is null or otherwise. + */ + public boolean equals(Object obj) { + if (this == obj) + return true; + + if (!(obj instanceof SecretKey)) + return false; + + String thatAlg = ((SecretKey)obj).getAlgorithm(); + if (!(thatAlg.equalsIgnoreCase(this.algorithm))) { + if ((!(thatAlg.equalsIgnoreCase("DESede")) + || !(this.algorithm.equalsIgnoreCase("TripleDES"))) + && (!(thatAlg.equalsIgnoreCase("TripleDES")) + || !(this.algorithm.equalsIgnoreCase("DESede")))) + return false; + } + + byte[] thatKey = ((SecretKey)obj).getEncoded(); + + return MessageDigest.isEqual(this.key, thatKey); + } +}