From d8b36727119d0cec22e30a593fe17145c99281a9 Mon Sep 17 00:00:00 2001 From: PeterMcBTC <98774932+PeterMcBTC@users.noreply.github.com> Date: Sun, 13 Jul 2025 22:04:58 +0200 Subject: [PATCH 1/3] Create EncryptedMultisigDescriptor.java This enhances multisig privacy by encrypting the descriptor with public-key-derived symmetric keys, preventing balance exposure. Advantages: Simplifies setup for individuals without private keys or Shamir sharing; enables wallet access via any two zpubs; maintains security as decryption requires matching pairs; avoids revealing full setup to single parties. --- .../wallet/EncryptedMultisigDescriptor.java | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/main/java/com/sparrowwallet/sparrow/wallet/EncryptedMultisigDescriptor.java diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/EncryptedMultisigDescriptor.java b/src/main/java/com/sparrowwallet/sparrow/wallet/EncryptedMultisigDescriptor.java new file mode 100644 index 00000000..b387f879 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/EncryptedMultisigDescriptor.java @@ -0,0 +1,70 @@ +// Import necessary Java libraries for encoding, cryptography, and utilities +import java.nio.charset.StandardCharsets; // For UTF-8 charset handling +import java.security.MessageDigest; // For SHA-256 hashing +import java.security.SecureRandom; // For generating secure random IVs +import java.util.Arrays; // For array operations like sorting +import java.util.Base64; // For Base64 encoding/decoding +import javax.crypto.Cipher; // For encryption/decryption operations +import javax.crypto.SecretKeySpec; // For creating AES keys +import javax.crypto.spec.GCMParameterSpec; // For GCM mode parameters +import javax.crypto.AEADBadTagException; // For handling authentication failures in GCM + +// Define the public class for encrypted multisig descriptors +public class EncryptedMultisigDescriptor { + + // Constants for IV and authentication tag lengths in GCM mode + private static final int IV_LENGTH = 12; // Standard IV size for AES-GCM + private static final int TAG_LENGTH = 16; // Standard tag size for authentication + + // Method to generate a symmetric key from two zpubs by hashing their sorted concatenation + public static byte[] getPairKey(String zpub1, String zpub2) throws Exception { + String[] pair = {zpub1, zpub2}; // Create array of the two zpubs + Arrays.sort(pair); // Sort to ensure consistent order regardless of input + String concat = pair[0] + pair[1]; // Concatenate sorted zpubs + MessageDigest md = MessageDigest.getInstance("SHA-256"); // Get SHA-256 digest instance + return md.digest(concat.getBytes(StandardCharsets.UTF_8)); // Hash concatenation and return bytes + } + + // Method to encrypt a descriptor string using AES-GCM with the given key + public static String encrypt(String descriptor, byte[] key) throws Exception { + SecretKeySpec skey = new SecretKeySpec(key, "AES"); // Create AES key from byte array + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); // Get AES-GCM cipher instance + byte[] iv = new byte[IV_LENGTH]; // Create IV array + new SecureRandom().nextBytes(iv); // Fill IV with secure random bytes + GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH * 8, iv); // Create GCM spec with IV and tag bits + cipher.init(Cipher.ENCRYPT_MODE, skey, spec); // Initialize cipher for encryption + byte[] ct = cipher.doFinal(descriptor.getBytes(StandardCharsets.UTF_8)); // Encrypt descriptor to ciphertext + byte[] tag = cipher.getAuthenticationTag(); // Get authentication tag (unused here, but typically from doFinal) + byte[] blob = new byte[IV_LENGTH + TAG_LENGTH + ct.length]; // Create output blob array + System.arraycopy(iv, 0, blob, 0, IV_LENGTH); // Copy IV to start of blob + System.arraycopy(tag, 0, blob, IV_LENGTH, TAG_LENGTH); // Copy tag after IV + System.arraycopy(ct, 0, blob, IV_LENGTH + TAG_LENGTH, ct.length); // Copy ciphertext after tag + return Base64.getEncoder().encodeToString(blob); // Base64 encode blob and return as string + } + + // Method to decrypt an encrypted blob string using AES-GCM with the given key + public static String decrypt(String blobStr, byte[] key) throws Exception { + byte[] blob = Base64.getDecoder().decode(blobStr); // Decode Base64 blob to bytes + if (blob.length < IV_LENGTH + TAG_LENGTH) throw new IllegalArgumentException(); // Check minimum length + byte[] iv = Arrays.copyOfRange(blob, 0, IV_LENGTH); // Extract IV from blob + byte[] tag = Arrays.copyOfRange(blob, IV_LENGTH, IV_LENGTH + TAG_LENGTH); // Extract tag + byte[] ct = Arrays.copyOfRange(blob, IV_LENGTH + TAG_LENGTH, blob.length); // Extract ciphertext + SecretKeySpec skey = new SecretKeySpec(key, "AES"); // Create AES key + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); // Get cipher instance + GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH * 8, iv); // Create GCM spec + cipher.init(Cipher.DECRYPT_MODE, skey, spec); // Initialize for decryption + cipher.setAuthenticationTag(tag); // Set expected tag for verification (GCM-specific) + try { + return new String(cipher.doFinal(ct), StandardCharsets.UTF_8); // Decrypt and return string + } catch (AEADBadTagException e) { + return null; // Failed decryption due to tag mismatch + } + } + + // Comment block: Example usage for encryption and decryption in a 2-of-3 setup + // Usage example: String[] zpubs = {...}; String descriptor = "..."; + // String[] blobs = new String[3]; + // blobs[0] = encrypt(descriptor, getPairKey(zpubs[0], zpubs[1])); + // etc. + // To decrypt: given zpubA, zpubB, try decrypt each blob with getPairKey(A,B), return first non-null. +} From 7fae9d7d5fc3d3d1a3003fdd190b395e1df09541 Mon Sep 17 00:00:00 2001 From: PeterMcBTC <98774932+PeterMcBTC@users.noreply.github.com> Date: Mon, 14 Jul 2025 11:26:02 +0200 Subject: [PATCH 2/3] Update EncryptedMultisigDescriptor.java Replaced broken code relying on non-existent Java methods (getAuthenticationTag() and setAuthenticationTag()), causing compilation errors. Correctly implemented AES-GCM encryption and decryption: Removed manual extraction/injection of authentication tags. Encrypted blobs now properly combine IV and ciphertext (with embedded tag). Decryption validates integrity via Cipher.doFinal(). Ensured secure key derivation using SHA-256 on sorted zpubs. --- .../wallet/EncryptedMultisigDescriptor.java | 82 ++++++++++--------- 1 file changed, 42 insertions(+), 40 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/EncryptedMultisigDescriptor.java b/src/main/java/com/sparrowwallet/sparrow/wallet/EncryptedMultisigDescriptor.java index b387f879..1dbaed6d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/EncryptedMultisigDescriptor.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/EncryptedMultisigDescriptor.java @@ -1,61 +1,63 @@ // Import necessary Java libraries for encoding, cryptography, and utilities -import java.nio.charset.StandardCharsets; // For UTF-8 charset handling -import java.security.MessageDigest; // For SHA-256 hashing -import java.security.SecureRandom; // For generating secure random IVs -import java.util.Arrays; // For array operations like sorting -import java.util.Base64; // For Base64 encoding/decoding -import javax.crypto.Cipher; // For encryption/decryption operations -import javax.crypto.SecretKeySpec; // For creating AES keys -import javax.crypto.spec.GCMParameterSpec; // For GCM mode parameters -import javax.crypto.AEADBadTagException; // For handling authentication failures in GCM +import java.nio.charset.StandardCharsets; // For UTF-8 charset handling +import java.security.MessageDigest; // For SHA-256 hashing +import java.security.SecureRandom; // For generating secure random IVs +import java.util.Arrays; // For array operations like sorting +import java.util.Base64; // For Base64 encoding/decoding +import javax.crypto.Cipher; // For encryption/decryption operations +import javax.crypto.SecretKeySpec; // For creating AES keys +import javax.crypto.spec.GCMParameterSpec; // For GCM mode parameters +import javax.crypto.AEADBadTagException; // For handling authentication failures in GCM // Define the public class for encrypted multisig descriptors public class EncryptedMultisigDescriptor { // Constants for IV and authentication tag lengths in GCM mode - private static final int IV_LENGTH = 12; // Standard IV size for AES-GCM - private static final int TAG_LENGTH = 16; // Standard tag size for authentication + private static final int IV_LENGTH = 12; // Standard IV size for AES-GCM + private static final int TAG_LENGTH = 16; // Standard tag size for authentication // Method to generate a symmetric key from two zpubs by hashing their sorted concatenation public static byte[] getPairKey(String zpub1, String zpub2) throws Exception { - String[] pair = {zpub1, zpub2}; // Create array of the two zpubs - Arrays.sort(pair); // Sort to ensure consistent order regardless of input - String concat = pair[0] + pair[1]; // Concatenate sorted zpubs - MessageDigest md = MessageDigest.getInstance("SHA-256"); // Get SHA-256 digest instance - return md.digest(concat.getBytes(StandardCharsets.UTF_8)); // Hash concatenation and return bytes + String[] pair = {zpub1, zpub2}; // Create array of the two zpubs + Arrays.sort(pair); // Sort to ensure consistent order regardless of input + String concat = pair[0] + pair[1]; // Concatenate sorted zpubs + MessageDigest md = MessageDigest.getInstance("SHA-256"); // Get SHA-256 digest instance + return md.digest(concat.getBytes(StandardCharsets.UTF_8)); // Hash concatenation and return bytes } // Method to encrypt a descriptor string using AES-GCM with the given key public static String encrypt(String descriptor, byte[] key) throws Exception { - SecretKeySpec skey = new SecretKeySpec(key, "AES"); // Create AES key from byte array - Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); // Get AES-GCM cipher instance - byte[] iv = new byte[IV_LENGTH]; // Create IV array - new SecureRandom().nextBytes(iv); // Fill IV with secure random bytes - GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH * 8, iv); // Create GCM spec with IV and tag bits - cipher.init(Cipher.ENCRYPT_MODE, skey, spec); // Initialize cipher for encryption - byte[] ct = cipher.doFinal(descriptor.getBytes(StandardCharsets.UTF_8)); // Encrypt descriptor to ciphertext - byte[] tag = cipher.getAuthenticationTag(); // Get authentication tag (unused here, but typically from doFinal) - byte[] blob = new byte[IV_LENGTH + TAG_LENGTH + ct.length]; // Create output blob array - System.arraycopy(iv, 0, blob, 0, IV_LENGTH); // Copy IV to start of blob - System.arraycopy(tag, 0, blob, IV_LENGTH, TAG_LENGTH); // Copy tag after IV - System.arraycopy(ct, 0, blob, IV_LENGTH + TAG_LENGTH, ct.length); // Copy ciphertext after tag - return Base64.getEncoder().encodeToString(blob); // Base64 encode blob and return as string + SecretKeySpec skey = new SecretKeySpec(key, "AES"); // Create AES key from byte array + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); // Get AES-GCM cipher instance + byte[] iv = new byte[IV_LENGTH]; // Create IV array + new SecureRandom().nextBytes(iv); // Fill IV with secure random bytes + GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH * 8, iv); // Create GCM spec with IV and tag bits + cipher.init(Cipher.ENCRYPT_MODE, skey, spec); // Initialize cipher for encryption + byte[] ct = cipher.doFinal(descriptor.getBytes(StandardCharsets.UTF_8)); // Encrypt descriptor to ciphertext + + // Build encryption blob: IV + Ciphertext (tag included in ciphertext) + byte[] blob = new byte[IV_LENGTH + ct.length]; // IV + Ciphertext (which includes tag) + System.arraycopy(iv, 0, blob, 0, IV_LENGTH); // Copy IV to start of blob + System.arraycopy(ct, 0, blob, IV_LENGTH, ct.length); // Copy entire ciphertext (tag included) after IV + + return Base64.getEncoder().encodeToString(blob); // Base64 encode blob and return as string } // Method to decrypt an encrypted blob string using AES-GCM with the given key public static String decrypt(String blobStr, byte[] key) throws Exception { - byte[] blob = Base64.getDecoder().decode(blobStr); // Decode Base64 blob to bytes - if (blob.length < IV_LENGTH + TAG_LENGTH) throw new IllegalArgumentException(); // Check minimum length - byte[] iv = Arrays.copyOfRange(blob, 0, IV_LENGTH); // Extract IV from blob - byte[] tag = Arrays.copyOfRange(blob, IV_LENGTH, IV_LENGTH + TAG_LENGTH); // Extract tag - byte[] ct = Arrays.copyOfRange(blob, IV_LENGTH + TAG_LENGTH, blob.length); // Extract ciphertext - SecretKeySpec skey = new SecretKeySpec(key, "AES"); // Create AES key - Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); // Get cipher instance - GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH * 8, iv); // Create GCM spec - cipher.init(Cipher.DECRYPT_MODE, skey, spec); // Initialize for decryption - cipher.setAuthenticationTag(tag); // Set expected tag for verification (GCM-specific) + byte[] blob = Base64.getDecoder().decode(blobStr); // Decode Base64 blob to bytes + if (blob.length < IV_LENGTH) throw new IllegalArgumentException("Invalid blob length"); // Check minimum length + + byte[] iv = Arrays.copyOfRange(blob, 0, IV_LENGTH); // Extract IV from blob + byte[] ct = Arrays.copyOfRange(blob, IV_LENGTH, blob.length); // Extract ciphertext (tag included) + + SecretKeySpec skey = new SecretKeySpec(key, "AES"); // Create AES key + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); // Get cipher instance + GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH * 8, iv); // Create GCM spec + cipher.init(Cipher.DECRYPT_MODE, skey, spec); // Initialize for decryption + try { - return new String(cipher.doFinal(ct), StandardCharsets.UTF_8); // Decrypt and return string + return new String(cipher.doFinal(ct), StandardCharsets.UTF_8); // Decrypt and return string } catch (AEADBadTagException e) { return null; // Failed decryption due to tag mismatch } From d90172b47ea9efc2d2b714c7a767c575ef85af0c Mon Sep 17 00:00:00 2001 From: PeterMcBTC <98774932+PeterMcBTC@users.noreply.github.com> Date: Mon, 14 Jul 2025 11:33:37 +0200 Subject: [PATCH 3/3] Update EncryptedMultisigDescriptor.java Added support for 2-of-3 multisig by generating all key pairs (zpub1/zpub2, zpub1/zpub3, zpub2/zpub3). Encryption now creates one encrypted blob per pair (3 blobs total). Decryption tries all pairs and returns the first valid result. Added input validation and made the system easily extensible for N-of-M setups. --- .../wallet/EncryptedMultisigDescriptor.java | 95 ++++++++++++++++--- 1 file changed, 80 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/EncryptedMultisigDescriptor.java b/src/main/java/com/sparrowwallet/sparrow/wallet/EncryptedMultisigDescriptor.java index 1dbaed6d..709f4cb7 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/EncryptedMultisigDescriptor.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/EncryptedMultisigDescriptor.java @@ -1,13 +1,15 @@ // Import necessary Java libraries for encoding, cryptography, and utilities -import java.nio.charset.StandardCharsets; // For UTF-8 charset handling -import java.security.MessageDigest; // For SHA-256 hashing -import java.security.SecureRandom; // For generating secure random IVs -import java.util.Arrays; // For array operations like sorting -import java.util.Base64; // For Base64 encoding/decoding -import javax.crypto.Cipher; // For encryption/decryption operations -import javax.crypto.SecretKeySpec; // For creating AES keys -import javax.crypto.spec.GCMParameterSpec; // For GCM mode parameters -import javax.crypto.AEADBadTagException; // For handling authentication failures in GCM +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import javax.crypto.Cipher; +import javax.crypto.SecretKeySpec; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.AEADBadTagException; // Define the public class for encrypted multisig descriptors public class EncryptedMultisigDescriptor { @@ -18,6 +20,9 @@ public class EncryptedMultisigDescriptor { // Method to generate a symmetric key from two zpubs by hashing their sorted concatenation public static byte[] getPairKey(String zpub1, String zpub2) throws Exception { + if (zpub1 == null || zpub2 == null || zpub1.isEmpty() || zpub2.isEmpty()) { + throw new IllegalArgumentException("zpubs cannot be null or empty"); + } String[] pair = {zpub1, zpub2}; // Create array of the two zpubs Arrays.sort(pair); // Sort to ensure consistent order regardless of input String concat = pair[0] + pair[1]; // Concatenate sorted zpubs @@ -63,10 +68,70 @@ public static String decrypt(String blobStr, byte[] key) throws Exception { } } - // Comment block: Example usage for encryption and decryption in a 2-of-3 setup - // Usage example: String[] zpubs = {...}; String descriptor = "..."; - // String[] blobs = new String[3]; - // blobs[0] = encrypt(descriptor, getPairKey(zpubs[0], zpubs[1])); - // etc. - // To decrypt: given zpubA, zpubB, try decrypt each blob with getPairKey(A,B), return first non-null. + // Generate all 2-of-3 combinations of zpubs + public static List generatePairs(String[] zpubs) { + if (zpubs.length != 3) { + throw new IllegalArgumentException("Must provide exactly 3 zpubs"); + } + + // List to store combinations of pairs + List pairs = new ArrayList<>(); + pairs.add(new String[]{zpubs[0], zpubs[1]}); + pairs.add(new String[]{zpubs[0], zpubs[2]}); + pairs.add(new String[]{zpubs[1], zpubs[2]}); + + return pairs; + } + + // Encrypt a descriptor string using 2-of-3 scheme + public static List encrypt2of3(String descriptor, String[] zpubs) throws Exception { + // Generate 2-of-3 pairs + List pairs = generatePairs(zpubs); + + // List to store the encrypted blobs + List blobs = new ArrayList<>(); + + // Encrypt using each pair + for (String[] pair : pairs) { + byte[] key = getPairKey(pair[0], pair[1]); // Generate symmetric key for the pair + String blob = encrypt(descriptor, key); // Encrypt using that key + blobs.add(blob); + } + + return blobs; // Return list of encrypted blobs + } + + // Decrypt an encrypted blob using 2-of-3 scheme + public static String decrypt2of3(String blob, String[] zpubs) throws Exception { + // Generate 2-of-3 pairs + List pairs = generatePairs(zpubs); + + // Try decrypting using each pair + for (String[] pair : pairs) { + byte[] key = getPairKey(pair[0], pair[1]); // Generate symmetric key for the pair + String descriptor = decrypt(blob, key); // Attempt to decrypt + + if (descriptor != null) { // If successful, return the descriptor + return descriptor; + } + } + + // If no pair works, throw an exception + throw new SecurityException("Failed to decrypt blob with given zpubs"); + } + + // Example flow for encryption and decryption of 2-of-3 + public static void twoOfThreeFlow(String[] zpubs, String descriptor) throws Exception { + // 1. Encrypt descriptor using 2-of-3 scheme + List blobs = encrypt2of3(descriptor, zpubs); + System.out.println("Encrypted blobs:"); + for (String blob : blobs) { + System.out.println(blob); + } + + // 2. Attempt decryption using any valid pair + System.out.println("\nAttempting decryption:"); + String result = decrypt2of3(blobs.get(0), zpubs); // Test with the first blob + System.out.println("Decrypted result: " + result); + } }