diff --git a/CHANGELOG.md b/CHANGELOG.md index f992e099f..5fb32ec6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- `AccountMetadataKey` +- `DerivationTool.deriveAccountMetadataKey` +- `DerivationTool.derivePrivateUseMetadataKey` - `Synchronizer.getTransactionsByMemoSubstring()` has been added - `Synchronizer.redactPcztForSigner` - `Synchronizer.pcztRequiresSaplingProofs` diff --git a/backend-lib/Cargo.lock b/backend-lib/Cargo.lock index 349ed6a11..eb8aa5299 100644 --- a/backend-lib/Cargo.lock +++ b/backend-lib/Cargo.lock @@ -1276,7 +1276,7 @@ dependencies = [ [[package]] name = "equihash" version = "0.2.0" -source = "git+https://github.com/zcash/librustzcash?rev=043286755f36e6e201b22c4b683398ac365b8bfc#043286755f36e6e201b22c4b683398ac365b8bfc" +source = "git+https://github.com/zcash/librustzcash?rev=a84f0644e7b2c1ba5635591bde869abcb2921582#a84f0644e7b2c1ba5635591bde869abcb2921582" dependencies = [ "blake2b_simd", "core2", @@ -1312,7 +1312,7 @@ dependencies = [ [[package]] name = "f4jumble" version = "0.1.1" -source = "git+https://github.com/zcash/librustzcash?rev=043286755f36e6e201b22c4b683398ac365b8bfc#043286755f36e6e201b22c4b683398ac365b8bfc" +source = "git+https://github.com/zcash/librustzcash?rev=a84f0644e7b2c1ba5635591bde869abcb2921582#a84f0644e7b2c1ba5635591bde869abcb2921582" dependencies = [ "blake2b_simd", ] @@ -2532,7 +2532,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "orchard" version = "0.10.1" -source = "git+https://github.com/zcash/orchard.git?rev=b1c22c07300db22239235d16dab096e23369948f#b1c22c07300db22239235d16dab096e23369948f" +source = "git+https://github.com/zcash/orchard.git?rev=9c59e6c5d04248d3548bca1ce61767e469d946c2#9c59e6c5d04248d3548bca1ce61767e469d946c2" dependencies = [ "aes", "bitvec", @@ -2692,7 +2692,7 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pczt" version = "0.1.0" -source = "git+https://github.com/zcash/librustzcash?rev=043286755f36e6e201b22c4b683398ac365b8bfc#043286755f36e6e201b22c4b683398ac365b8bfc" +source = "git+https://github.com/zcash/librustzcash?rev=a84f0644e7b2c1ba5635591bde869abcb2921582#a84f0644e7b2c1ba5635591bde869abcb2921582" dependencies = [ "blake2b_simd", "bls12_381", @@ -5806,7 +5806,7 @@ dependencies = [ [[package]] name = "zcash_address" version = "0.6.2" -source = "git+https://github.com/zcash/librustzcash?rev=043286755f36e6e201b22c4b683398ac365b8bfc#043286755f36e6e201b22c4b683398ac365b8bfc" +source = "git+https://github.com/zcash/librustzcash?rev=a84f0644e7b2c1ba5635591bde869abcb2921582#a84f0644e7b2c1ba5635591bde869abcb2921582" dependencies = [ "bech32", "bs58", @@ -5819,7 +5819,7 @@ dependencies = [ [[package]] name = "zcash_client_backend" version = "0.16.0" -source = "git+https://github.com/zcash/librustzcash?rev=043286755f36e6e201b22c4b683398ac365b8bfc#043286755f36e6e201b22c4b683398ac365b8bfc" +source = "git+https://github.com/zcash/librustzcash?rev=a84f0644e7b2c1ba5635591bde869abcb2921582#a84f0644e7b2c1ba5635591bde869abcb2921582" dependencies = [ "arti-client", "base64", @@ -5883,7 +5883,7 @@ dependencies = [ [[package]] name = "zcash_client_sqlite" version = "0.14.0" -source = "git+https://github.com/zcash/librustzcash?rev=043286755f36e6e201b22c4b683398ac365b8bfc#043286755f36e6e201b22c4b683398ac365b8bfc" +source = "git+https://github.com/zcash/librustzcash?rev=a84f0644e7b2c1ba5635591bde869abcb2921582#a84f0644e7b2c1ba5635591bde869abcb2921582" dependencies = [ "bip32", "bs58", @@ -5922,7 +5922,7 @@ dependencies = [ [[package]] name = "zcash_encoding" version = "0.2.2" -source = "git+https://github.com/zcash/librustzcash?rev=043286755f36e6e201b22c4b683398ac365b8bfc#043286755f36e6e201b22c4b683398ac365b8bfc" +source = "git+https://github.com/zcash/librustzcash?rev=a84f0644e7b2c1ba5635591bde869abcb2921582#a84f0644e7b2c1ba5635591bde869abcb2921582" dependencies = [ "core2", "nonempty", @@ -5931,7 +5931,7 @@ dependencies = [ [[package]] name = "zcash_keys" version = "0.6.0" -source = "git+https://github.com/zcash/librustzcash?rev=043286755f36e6e201b22c4b683398ac365b8bfc#043286755f36e6e201b22c4b683398ac365b8bfc" +source = "git+https://github.com/zcash/librustzcash?rev=a84f0644e7b2c1ba5635591bde869abcb2921582#a84f0644e7b2c1ba5635591bde869abcb2921582" dependencies = [ "bech32", "bip32", @@ -5973,7 +5973,7 @@ dependencies = [ [[package]] name = "zcash_primitives" version = "0.21.0" -source = "git+https://github.com/zcash/librustzcash?rev=043286755f36e6e201b22c4b683398ac365b8bfc#043286755f36e6e201b22c4b683398ac365b8bfc" +source = "git+https://github.com/zcash/librustzcash?rev=a84f0644e7b2c1ba5635591bde869abcb2921582#a84f0644e7b2c1ba5635591bde869abcb2921582" dependencies = [ "bip32", "blake2b_simd", @@ -6012,7 +6012,7 @@ dependencies = [ [[package]] name = "zcash_proofs" version = "0.21.0" -source = "git+https://github.com/zcash/librustzcash?rev=043286755f36e6e201b22c4b683398ac365b8bfc#043286755f36e6e201b22c4b683398ac365b8bfc" +source = "git+https://github.com/zcash/librustzcash?rev=a84f0644e7b2c1ba5635591bde869abcb2921582#a84f0644e7b2c1ba5635591bde869abcb2921582" dependencies = [ "bellman", "blake2b_simd", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "zcash_protocol" version = "0.4.3" -source = "git+https://github.com/zcash/librustzcash?rev=043286755f36e6e201b22c4b683398ac365b8bfc#043286755f36e6e201b22c4b683398ac365b8bfc" +source = "git+https://github.com/zcash/librustzcash?rev=a84f0644e7b2c1ba5635591bde869abcb2921582#a84f0644e7b2c1ba5635591bde869abcb2921582" dependencies = [ "core2", "document-features", @@ -6045,8 +6045,7 @@ dependencies = [ [[package]] name = "zcash_spec" version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cede95491c2191d3e278cab76e097a44b17fde8d6ca0d4e3a22cf4807b2d857" +source = "git+https://github.com/daira/zcash_spec.git?rev=295ed474426a8c2954a534b0bd72b4f4bf0696d9#295ed474426a8c2954a534b0bd72b4f4bf0696d9" dependencies = [ "blake2b_simd", ] @@ -6054,7 +6053,7 @@ dependencies = [ [[package]] name = "zcash_transparent" version = "0.1.0" -source = "git+https://github.com/zcash/librustzcash?rev=043286755f36e6e201b22c4b683398ac365b8bfc#043286755f36e6e201b22c4b683398ac365b8bfc" +source = "git+https://github.com/zcash/librustzcash?rev=a84f0644e7b2c1ba5635591bde869abcb2921582#a84f0644e7b2c1ba5635591bde869abcb2921582" dependencies = [ "bip32", "blake2b_simd", @@ -6117,8 +6116,7 @@ dependencies = [ [[package]] name = "zip32" version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e9943793abf9060b68e1889012dafbd5523ab5b125c0fcc24802d69182f2ac9" +source = "git+https://github.com/daira/zip32.git?rev=fa7805dd7d4c9d37756a0ab1d31f55e97aa57eb4#fa7805dd7d4c9d37756a0ab1d31f55e97aa57eb4" dependencies = [ "blake2b_simd", "memuse", @@ -6129,7 +6127,7 @@ dependencies = [ [[package]] name = "zip321" version = "0.2.0" -source = "git+https://github.com/zcash/librustzcash?rev=043286755f36e6e201b22c4b683398ac365b8bfc#043286755f36e6e201b22c4b683398ac365b8bfc" +source = "git+https://github.com/zcash/librustzcash?rev=a84f0644e7b2c1ba5635591bde869abcb2921582#a84f0644e7b2c1ba5635591bde869abcb2921582" dependencies = [ "base64", "nom", diff --git a/backend-lib/Cargo.toml b/backend-lib/Cargo.toml index a7cc3e175..b3dd33e53 100644 --- a/backend-lib/Cargo.toml +++ b/backend-lib/Cargo.toml @@ -86,17 +86,19 @@ xz2 = { version = "0.1", features = ["static"] } #zcash_protocol = { git = "https://github.com/zcash/librustzcash", branch = "main" } [patch.crates-io] -orchard = { git = "https://github.com/zcash/orchard.git", rev = "b1c22c07300db22239235d16dab096e23369948f" } -pczt = { git = "https://github.com/zcash/librustzcash", rev = "043286755f36e6e201b22c4b683398ac365b8bfc" } +orchard = { git = "https://github.com/zcash/orchard.git", rev = "9c59e6c5d04248d3548bca1ce61767e469d946c2" } +pczt = { git = "https://github.com/zcash/librustzcash", rev = "a84f0644e7b2c1ba5635591bde869abcb2921582" } redjubjub = { git = "https://github.com/ZcashFoundation/redjubjub", rev = "eae848c5c14d9c795d000dd9f4c4762d1aee7ee1" } sapling = { package = "sapling-crypto", git = "https://github.com/zcash/sapling-crypto.git", rev = "6ca338532912adcd82369220faeea31aab4720c5" } -transparent = { package = "zcash_transparent", git = "https://github.com/zcash/librustzcash", rev = "043286755f36e6e201b22c4b683398ac365b8bfc" } -zcash_address = { git = "https://github.com/zcash/librustzcash", rev = "043286755f36e6e201b22c4b683398ac365b8bfc" } -zcash_client_backend = { git = "https://github.com/zcash/librustzcash", rev = "043286755f36e6e201b22c4b683398ac365b8bfc" } -zcash_client_sqlite = { git = "https://github.com/zcash/librustzcash", rev = "043286755f36e6e201b22c4b683398ac365b8bfc" } -zcash_primitives = { git = "https://github.com/zcash/librustzcash", rev = "043286755f36e6e201b22c4b683398ac365b8bfc" } -zcash_proofs = { git = "https://github.com/zcash/librustzcash", rev = "043286755f36e6e201b22c4b683398ac365b8bfc" } -zcash_protocol = { git = "https://github.com/zcash/librustzcash", rev = "043286755f36e6e201b22c4b683398ac365b8bfc" } +transparent = { package = "zcash_transparent", git = "https://github.com/zcash/librustzcash", rev = "a84f0644e7b2c1ba5635591bde869abcb2921582" } +zcash_address = { git = "https://github.com/zcash/librustzcash", rev = "a84f0644e7b2c1ba5635591bde869abcb2921582" } +zcash_client_backend = { git = "https://github.com/zcash/librustzcash", rev = "a84f0644e7b2c1ba5635591bde869abcb2921582" } +zcash_client_sqlite = { git = "https://github.com/zcash/librustzcash", rev = "a84f0644e7b2c1ba5635591bde869abcb2921582" } +zcash_primitives = { git = "https://github.com/zcash/librustzcash", rev = "a84f0644e7b2c1ba5635591bde869abcb2921582" } +zcash_proofs = { git = "https://github.com/zcash/librustzcash", rev = "a84f0644e7b2c1ba5635591bde869abcb2921582" } +zcash_protocol = { git = "https://github.com/zcash/librustzcash", rev = "a84f0644e7b2c1ba5635591bde869abcb2921582" } +zcash_spec = { git = "https://github.com/daira/zcash_spec.git", rev = "295ed474426a8c2954a534b0bd72b4f4bf0696d9" } +zip32 = { git = "https://github.com/daira/zip32.git", rev = "fa7805dd7d4c9d37756a0ab1d31f55e97aa57eb4" } [lib] name = "zcashwalletsdk" diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/Derivation.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/Derivation.kt index fa03d4957..cb90ce02a 100644 --- a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/Derivation.kt +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/Derivation.kt @@ -1,5 +1,6 @@ package cash.z.ecc.android.sdk.internal +import cash.z.ecc.android.sdk.internal.model.JniMetadataKey import cash.z.ecc.android.sdk.internal.model.JniUnifiedSpendingKey interface Derivation { @@ -38,6 +39,44 @@ interface Derivation { numberOfAccounts: Int ): Array + /** + * Derives a ZIP 325 Account Metadata Key from the given seed. + * + * @return an account metadata key. + */ + fun deriveAccountMetadataKey( + seed: ByteArray, + networkId: Int, + accountIndex: Long + ): JniMetadataKey + + /** + * Derives a metadata key for private use from a ZIP 325 Account Metadata Key. + * + * If `ufvk` is non-null, this method will return one metadata key for every FVK item + * contained within the UFVK, in preference order. As UFVKs may in general change over + * time (due to the inclusion of new higher-preference FVK items, or removal of older + * deprecated FVK items), private usage of these keys should always follow preference + * order: + * - For encryption-like private usage, the first key in the array should always be + * used, and all other keys ignored. + * - For decryption-like private usage, each key in the array should be tried in turn + * until metadata can be recovered, and then the metadata should be re-encrypted + * under the first key. + * + * @param ufvk the external UFVK for which a metadata key is required, or `null` if the + * metadata key is "inherent" (for the same account as the Account Metadata Key). + * @param privateUseSubject a globally unique non-empty sequence of at most 252 bytes that + * identifies the desired private-use context. + * @return an array of 32-byte metadata keys in preference order. + */ + fun derivePrivateUseMetadataKey( + accountMetadataKey: JniMetadataKey, + ufvk: String?, + networkId: Int, + privateUseSubject: ByteArray + ): Array + /** * Derives a ZIP 32 Arbitrary Key from the given seed at the "wallet level", i.e. * directly from the seed with no ZIP 32 path applied. diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/JniConstants.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/JniConstants.kt index ff8cea065..1158c5827 100644 --- a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/JniConstants.kt +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/JniConstants.kt @@ -12,3 +12,13 @@ const val JNI_ACCOUNT_UUID_BYTES_SIZE = 16 * The number of bytes in the seed fingerprint parameter. It's used e.g. in [JniAccount.seedFingerprint] */ const val JNI_ACCOUNT_SEED_FP_BYTES_SIZE = 32 + +/** + * The number of bytes in an HD-derived ZIP 32 key. It's used e.g. in [JniMetadataKey.sk] + */ +const val JNI_METADATA_KEY_SK_SIZE = 32 + +/** + * The number of bytes in a chain code. It's used e.g. in [JniMetadataKey.chainCode] + */ +const val JNI_METADATA_KEY_CHAIN_CODE_SIZE = 32 diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/RustDerivationTool.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/RustDerivationTool.kt index 792c9ee19..df874b85c 100644 --- a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/RustDerivationTool.kt +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/RustDerivationTool.kt @@ -1,6 +1,7 @@ package cash.z.ecc.android.sdk.internal.jni import cash.z.ecc.android.sdk.internal.Derivation +import cash.z.ecc.android.sdk.internal.model.JniMetadataKey import cash.z.ecc.android.sdk.internal.model.JniUnifiedSpendingKey class RustDerivationTool private constructor() : Derivation { @@ -40,6 +41,26 @@ class RustDerivationTool private constructor() : Derivation { networkId: Int ): String = deriveUnifiedAddressFromViewingKey(viewingKey, networkId = networkId) + override fun deriveAccountMetadataKey( + seed: ByteArray, + networkId: Int, + accountIndex: Long + ): JniMetadataKey = deriveAccountMetadataKeyFromSeed(seed, accountIndex, networkId) + + override fun derivePrivateUseMetadataKey( + accountMetadataKey: JniMetadataKey, + ufvk: String?, + networkId: Int, + privateUseSubject: ByteArray + ): Array = + derivePrivateUseMetadataKey( + accountMetadataKey_sk = accountMetadataKey.sk, + accountMetadataKey_c = accountMetadataKey.chainCode, + ufvk, + privateUseSubject, + networkId + ) + override fun deriveArbitraryWalletKey( contextString: ByteArray, seed: ByteArray @@ -98,6 +119,22 @@ class RustDerivationTool private constructor() : Derivation { networkId: Int ): String + private external fun deriveAccountMetadataKeyFromSeed( + seed: ByteArray, + accountIndex: Long, + networkId: Int + ): JniMetadataKey + + @Suppress("FunctionParameterNaming") + @JvmStatic + private external fun derivePrivateUseMetadataKey( + accountMetadataKey_sk: ByteArray, + accountMetadataKey_c: ByteArray, + ufvk: String?, + privateUseSubject: ByteArray, + networkId: Int + ): Array + @JvmStatic private external fun deriveArbitraryWalletKeyFromSeed( contextString: ByteArray, diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/JniMetadataKey.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/JniMetadataKey.kt new file mode 100644 index 000000000..b8422ed60 --- /dev/null +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/JniMetadataKey.kt @@ -0,0 +1,29 @@ +package cash.z.ecc.android.sdk.internal.model + +import androidx.annotation.Keep +import cash.z.ecc.android.sdk.internal.jni.JNI_METADATA_KEY_CHAIN_CODE_SIZE +import cash.z.ecc.android.sdk.internal.jni.JNI_METADATA_KEY_SK_SIZE + +/** + * Serves as cross layer (Kotlin, Rust) communication class. + * + * @param sk the ZIP 32 key required to derive child keys. + * @param chainCode The ZIP 32 chain code required to derive child keys. + * + * @throws IllegalArgumentException if the values are inconsistent. + */ +@Keep +class JniMetadataKey( + val sk: ByteArray, + val chainCode: ByteArray, +) { + init { + require(sk.size == JNI_METADATA_KEY_SK_SIZE) { + "Account UUID must be 32 bytes" + } + + require(chainCode.size == JNI_METADATA_KEY_CHAIN_CODE_SIZE) { + "Seed fingerprint must be 32 bytes" + } + } +} diff --git a/backend-lib/src/main/rust/lib.rs b/backend-lib/src/main/rust/lib.rs index df476a3a7..95db0b7c6 100644 --- a/backend-lib/src/main/rust/lib.rs +++ b/backend-lib/src/main/rust/lib.rs @@ -26,7 +26,8 @@ use tracing_subscriber::reload; use transparent::bundle::{OutPoint, TxOut}; use utils::{java_nullable_string_to_rust, java_string_to_rust}; use uuid::Uuid; -use zcash_address::{ToAddress, ZcashAddress}; +use zcash_address::unified::{Container, Encoding, Item as _}; +use zcash_address::{unified, ToAddress, ZcashAddress}; use zcash_client_backend::data_api::{ AccountPurpose, BirthdayError, TransactionDataRequest, TransactionStatus, Zip32Derivation, }; @@ -78,7 +79,7 @@ use zcash_protocol::{ value::{ZatBalance, Zatoshis}, ShieldedProtocol, }; -use zip32::{fingerprint::SeedFingerprint, ChildIndex, DiversifierIndex}; +use zip32::{fingerprint::SeedFingerprint, ChainCode, ChildIndex, DiversifierIndex}; use crate::utils::{catch_unwind, exception::unwrap_exc_or}; @@ -2449,6 +2450,138 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustDerivationTool_de unwrap_exc_or(&mut env, res, ptr::null_mut()) } +fn encode_metadata_key<'a>( + env: &mut JNIEnv<'a>, + key: zip32::registered::SecretKey, +) -> anyhow::Result> { + Ok(env.new_object( + "cash/z/ecc/android/sdk/internal/model/JniMetadataKey", + "([B[B)V", + &[ + (&env.byte_array_from_slice(key.data())?).into(), + (&env.byte_array_from_slice(key.chain_code().as_bytes())?).into(), + ], + )?) +} + +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustDerivationTool_deriveAccountMetadataKeyFromSeed< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + seed: JByteArray<'local>, + account_index: jlong, + network_id: jint, +) -> jobject { + let res = catch_unwind(&mut env, |env| { + let _span = + tracing::info_span!("RustDerivationTool.deriveAccountMetadataKeyFromSeed").entered(); + let network = parse_network(network_id as u32)?; + let seed = parse_secret(env, &seed)?; + let account = zip32_account_index_from_jlong(account_index)?; + + let key = zip32::registered::SecretKey::from_path( + b"MetadataKeys", + seed.expose_secret(), + // TODO: Change this to whatever ZIP number is assigned to the metadata key ZIP draft. + 325, + &[ + (ChildIndex::hardened(network.coin_type()), &[]), + (ChildIndex::hardened(account.into()), &[]), + ], + ); + + Ok(encode_metadata_key(env, key)?.into_raw()) + }); + unwrap_exc_or(&mut env, res, ptr::null_mut()) +} + +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustDerivationTool_derivePrivateUseMetadataKey< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + account_metadata_key_sk: JByteArray<'local>, + account_metadata_key_c: JByteArray<'local>, + ufvk_string: JString<'local>, + private_use_subject: JByteArray<'local>, + network_id: jint, +) -> jobjectArray { + let res = catch_unwind(&mut env, |env| { + let _span = tracing::info_span!("RustDerivationTool.derivePrivateUseMetadataKey").entered(); + let account_metadata_key_sk = utils::java_bytes_to_rust(env, &account_metadata_key_sk)?; + let account_metadata_key_c = utils::java_bytes_to_rust(env, &account_metadata_key_c)?; + let ufvk_string = utils::java_nullable_string_to_rust(env, &ufvk_string)?; + let private_use_subject = utils::java_bytes_to_rust(env, &private_use_subject)?; + let network = parse_network(network_id as u32)?; + + let account_metadata_key = { + let sk = account_metadata_key_sk + .as_slice() + .try_into() + .map_err(|_| anyhow!("Incorrect length for account_metadata_key_sk"))?; + + let chain_code = ChainCode::new( + account_metadata_key_c + .as_slice() + .try_into() + .map_err(|_| anyhow!("Incorrect length for account_metadata_key_c"))?, + ); + + zip32::registered::SecretKey::from_parts(sk, chain_code) + }; + + let private_use_keys = match ufvk_string { + // For the inherent subtree, there is only ever one key. + None => vec![account_metadata_key + .derive_child_with_tag(ChildIndex::hardened(0), &[]) + .derive_child_with_tag(ChildIndex::PRIVATE_USE, &private_use_subject)], + // For the external subtree, we derive keys from the UFVK's items. + Some(ufvk_string) => { + let (net, ufvk) = + unified::Ufvk::decode(&ufvk_string).map_err(|e| anyhow!("{e}"))?; + let expected_net = network.network_type(); + if net != expected_net { + return Err(anyhow!( + "UFVK is for network {:?} but we expected {:?}", + net, + expected_net, + )); + } + + // Any metadata should always be associated with the key derived from the + // most preferred FVK item. However, we don't know which FVK items the + // UFVK contained the last time we were asked to derive keys. So we derive + // every key and return them to the caller in preference order. If the + // caller finds data associated with an older FVK item, they will migrate + // it to the first key we return. + ufvk.items() + .into_iter() + .map(|fvk_item| { + account_metadata_key + .derive_child_with_tag(ChildIndex::hardened(1), &[]) + .derive_child_with_tag( + ChildIndex::hardened(0), + &fvk_item.typed_encoding(), + ) + .derive_child_with_tag(ChildIndex::PRIVATE_USE, &private_use_subject) + }) + .collect() + } + }; + + Ok( + utils::rust_vec_to_java(env, private_use_keys, "[B", |env, key| { + utils::rust_bytes_to_java(env, key.data()) + })? + .into_raw(), + ) + }); + unwrap_exc_or(&mut env, res, ptr::null_mut()) +} + #[unsafe(no_mangle)] pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustDerivationTool_deriveArbitraryWalletKeyFromSeed< 'local, diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/DerivationToolExt.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/DerivationToolExt.kt index 21d0f7450..2c6a86a67 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/DerivationToolExt.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/DerivationToolExt.kt @@ -1,6 +1,8 @@ package cash.z.ecc.android.sdk.internal +import cash.z.ecc.android.sdk.internal.model.JniMetadataKey import cash.z.ecc.android.sdk.internal.model.JniUnifiedSpendingKey +import cash.z.ecc.android.sdk.model.AccountMetadataKey import cash.z.ecc.android.sdk.model.UnifiedFullViewingKey import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.ZcashNetwork @@ -52,6 +54,19 @@ fun Derivation.deriveUnifiedFullViewingKeysTypesafe( numberOfAccounts ).map { UnifiedFullViewingKey(it) } +fun Derivation.deriveAccountMetadataKeyTypesafe( + seed: ByteArray, + network: ZcashNetwork, + accountIndex: Zip32AccountIndex +): JniMetadataKey = deriveAccountMetadataKey(seed, network.id, accountIndex.index) + +fun Derivation.derivePrivateUseMetadataKeyTypesafe( + accountMetadataKey: AccountMetadataKey, + ufvk: String?, + network: ZcashNetwork, + privateUseSubject: ByteArray +): Array = derivePrivateUseMetadataKey(accountMetadataKey.toUnsafe(), ufvk, network.id, privateUseSubject) + fun Derivation.deriveArbitraryWalletKeyTypesafe( contextString: ByteArray, seed: ByteArray diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeDerivationToolImpl.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeDerivationToolImpl.kt index 1e7b97726..2a89bf8df 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeDerivationToolImpl.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeDerivationToolImpl.kt @@ -1,5 +1,6 @@ package cash.z.ecc.android.sdk.internal +import cash.z.ecc.android.sdk.model.AccountMetadataKey import cash.z.ecc.android.sdk.model.UnifiedFullViewingKey import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.ZcashNetwork @@ -35,6 +36,25 @@ internal class TypesafeDerivationToolImpl(private val derivation: Derivation) : network: ZcashNetwork, ): String = derivation.deriveUnifiedAddress(viewingKey, network) + override suspend fun deriveAccountMetadataKey( + seed: ByteArray, + network: ZcashNetwork, + accountIndex: Zip32AccountIndex + ): AccountMetadataKey = AccountMetadataKey(derivation.deriveAccountMetadataKeyTypesafe(seed, network, accountIndex)) + + override suspend fun derivePrivateUseMetadataKey( + accountMetadataKey: AccountMetadataKey, + ufvk: String?, + network: ZcashNetwork, + privateUseSubject: ByteArray + ): Array = + derivation.derivePrivateUseMetadataKeyTypesafe( + accountMetadataKey, + ufvk, + network, + privateUseSubject + ) + override suspend fun deriveArbitraryWalletKey( contextString: ByteArray, seed: ByteArray diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountMetadataKey.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountMetadataKey.kt new file mode 100644 index 000000000..8854bde16 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountMetadataKey.kt @@ -0,0 +1,62 @@ +package cash.z.ecc.android.sdk.model + +import cash.z.ecc.android.sdk.internal.model.JniMetadataKey +import cash.z.ecc.android.sdk.tool.DerivationTool + +/** + * A [ZIP 325](https://zips.z.cash/zip-0325) Account Metadata Key. + */ +class AccountMetadataKey private constructor( + private val sk: FirstClassByteArray, + private val chainCode: FirstClassByteArray +) { + internal constructor(jniMetadataKey: JniMetadataKey) : this( + FirstClassByteArray(jniMetadataKey.sk.copyOf()), + FirstClassByteArray(jniMetadataKey.chainCode.copyOf()) + ) + + // Override to prevent leaking key to logs + override fun toString() = "AccountMetadataKey(bytes=***)" + + /** + * Derives a metadata key for private use from this ZIP 325 Account Metadata Key. + * + * If `ufvk` is non-null, this method will return one metadata key for every FVK item + * contained within the UFVK, in preference order. As UFVKs may in general change over + * time (due to the inclusion of new higher-preference FVK items, or removal of older + * deprecated FVK items), private usage of these keys should always follow preference + * order: + * - For encryption-like private usage, the first key in the array should always be + * used, and all other keys ignored. + * - For decryption-like private usage, each key in the array should be tried in turn + * until metadata can be recovered, and then the metadata should be re-encrypted + * under the first key. + * + * @param ufvk the external UFVK for which a metadata key is required, or `null` if the + * metadata key is "inherent" (for the same account as the Account Metadata Key). + * @param privateUseSubject a globally unique non-empty sequence of at most 252 bytes that + * identifies the desired private-use context. + * @return an array of 32-byte metadata keys in preference order. + */ + suspend fun derivePrivateUseMetadataKey( + ufvk: String?, + network: ZcashNetwork, + privateUseSubject: ByteArray + ): Array = + // TODO [#1685]: I don't want DerivationTool.derivePrivateUseMetadataKey in the + // public API, but the way DerivationTool is constructed, I don't see how to expose + // this only to AccountMetadataKey. + DerivationTool.getInstance().derivePrivateUseMetadataKey( + this, + ufvk, + network, + privateUseSubject + ) + + /** + * Exposes the type-unsafe variant for passing across the JNI. + */ + fun toUnsafe(): JniMetadataKey { + return JniMetadataKey(sk.byteArray, chainCode.byteArray) + } +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/DerivationTool.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/DerivationTool.kt index eb164a3db..d5d513442 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/DerivationTool.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/DerivationTool.kt @@ -4,6 +4,7 @@ import cash.z.ecc.android.sdk.internal.Derivation import cash.z.ecc.android.sdk.internal.SuspendingLazy import cash.z.ecc.android.sdk.internal.TypesafeDerivationToolImpl import cash.z.ecc.android.sdk.internal.jni.RustDerivationTool +import cash.z.ecc.android.sdk.model.AccountMetadataKey import cash.z.ecc.android.sdk.model.UnifiedFullViewingKey import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.ZcashNetwork @@ -81,6 +82,44 @@ interface DerivationTool { network: ZcashNetwork ): String + /** + * Derives a ZIP 325 Account Metadata Key from the given seed. + * + * @return an account metadata key. + */ + suspend fun deriveAccountMetadataKey( + seed: ByteArray, + network: ZcashNetwork, + accountIndex: Zip32AccountIndex + ): AccountMetadataKey + + /** + * Derives a metadata key for private use from a ZIP 325 Account Metadata Key. + * + * If `ufvk` is non-null, this method will return one metadata key for every FVK item + * contained within the UFVK, in preference order. As UFVKs may in general change over + * time (due to the inclusion of new higher-preference FVK items, or removal of older + * deprecated FVK items), private usage of these keys should always follow preference + * order: + * - For encryption-like private usage, the first key in the array should always be + * used, and all other keys ignored. + * - For decryption-like private usage, each key in the array should be tried in turn + * until metadata can be recovered, and then the metadata should be re-encrypted + * under the first key. + * + * @param ufvk the external UFVK for which a metadata key is required, or `null` if the + * metadata key is "inherent" (for the same account as the Account Metadata Key). + * @param privateUseSubject a globally unique non-empty sequence of at most 252 bytes that + * identifies the desired private-use context. + * @return an array of 32-byte metadata keys in preference order. + */ + suspend fun derivePrivateUseMetadataKey( + accountMetadataKey: AccountMetadataKey, + ufvk: String?, + network: ZcashNetwork, + privateUseSubject: ByteArray + ): Array + /** * Derives a [ZIP 32 Arbitrary Key] from the given seed at the "wallet level", i.e. * directly from the seed with no ZIP 32 path applied.