diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d37b981b..8513e1f73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- `DerivationTool.deriveArbitraryWalletKey` +- `DerivationTool.deriveArbitraryAccountKey` - `Synchronizer.getTransactionOutputs` API has been added. It enables to fetch all transaction outputs from database. ## [2.2.5] - 2024-10-22 diff --git a/backend-lib/Cargo.lock b/backend-lib/Cargo.lock index 3cd7b7119..c708ca4e8 100644 --- a/backend-lib/Cargo.lock +++ b/backend-lib/Cargo.lock @@ -5019,6 +5019,7 @@ dependencies = [ "zcash_client_sqlite", "zcash_primitives", "zcash_proofs", + "zip32", ] [[package]] @@ -5257,9 +5258,9 @@ dependencies = [ [[package]] name = "zcash_spec" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a3bf58b673cb3dacd8ae09ba345998923a197ab0da70d6239d8e8838949e9b" +checksum = "9cede95491c2191d3e278cab76e097a44b17fde8d6ca0d4e3a22cf4807b2d857" dependencies = [ "blake2b_simd", ] @@ -5327,13 +5328,14 @@ dependencies = [ [[package]] name = "zip32" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4226d0aee9c9407c27064dfeec9d7b281c917de3374e1e5a2e2cfad9e09de19e" +checksum = "92022ac1e47c7b78f9cee29efac8a1a546e189506f3bb5ad46d525be7c519bf6" dependencies = [ "blake2b_simd", "memuse", "subtle", + "zcash_spec", ] [[package]] diff --git a/backend-lib/Cargo.toml b/backend-lib/Cargo.toml index dcb5c4217..d1b34d2ce 100644 --- a/backend-lib/Cargo.toml +++ b/backend-lib/Cargo.toml @@ -19,6 +19,7 @@ zcash_client_backend = { version = "0.14", features = ["orchard", "tor", "transp zcash_client_sqlite = { version = "0.12.2", features = ["orchard", "transparent-inputs", "unstable"] } zcash_primitives = "0.19" zcash_proofs = "0.19" +zip32 = "0.1.2" # Infrastructure prost = "0.13" 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 3f61d1598..229649e1f 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 @@ -38,6 +38,37 @@ interface Derivation { numberOfAccounts: Int ): 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. + * + * The resulting key will be the same across all networks (Zcash mainnet, Zcash + * testnet, OtherCoin mainnet, and so on). You can think of it as a context-specific + * seed fingerprint that can be used as (static) key material. + * + * @param contextString a globally-unique non-empty sequence of at most 252 bytes that + * identifies the desired context. + * @return an array of 32 bytes. + */ + fun deriveArbitraryWalletKey( + contextString: ByteArray, + seed: ByteArray + ): ByteArray + + /** + * Derives a ZIP 32 Arbitrary Key from the given seed at the account level. + * + * @param contextString a globally-unique non-empty sequence of at most 252 bytes that + * identifies the desired context. + * @return an array of 32 bytes. + */ + fun deriveArbitraryAccountKey( + contextString: ByteArray, + seed: ByteArray, + networkId: Int, + accountIndex: Int + ): ByteArray + companion object { const val DEFAULT_NUMBER_OF_ACCOUNTS = 1 } 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 be2143bf5..371c35531 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 @@ -40,6 +40,24 @@ class RustDerivationTool private constructor() : Derivation { networkId: Int ): String = deriveUnifiedAddressFromViewingKey(viewingKey, networkId = networkId) + override fun deriveArbitraryWalletKey( + contextString: ByteArray, + seed: ByteArray + ): ByteArray = deriveArbitraryWalletKeyFromSeed(contextString, seed) + + override fun deriveArbitraryAccountKey( + contextString: ByteArray, + seed: ByteArray, + networkId: Int, + accountIndex: Int + ): ByteArray = + deriveArbitraryAccountKeyFromSeed( + contextString = contextString, + seed = seed, + accountIndex = accountIndex, + networkId = networkId + ) + companion object { suspend fun new(): Derivation { RustBackend.loadLibrary() @@ -79,5 +97,19 @@ class RustDerivationTool private constructor() : Derivation { key: String, networkId: Int ): String + + @JvmStatic + private external fun deriveArbitraryWalletKeyFromSeed( + contextString: ByteArray, + seed: ByteArray + ): ByteArray + + @JvmStatic + private external fun deriveArbitraryAccountKeyFromSeed( + contextString: ByteArray, + seed: ByteArray, + accountIndex: Int, + networkId: Int + ): ByteArray } } diff --git a/backend-lib/src/main/rust/lib.rs b/backend-lib/src/main/rust/lib.rs index 0482aa849..bd7555269 100644 --- a/backend-lib/src/main/rust/lib.rs +++ b/backend-lib/src/main/rust/lib.rs @@ -47,6 +47,7 @@ use zcash_client_sqlite::{ wallet::init::{init_wallet_db, WalletMigrationError}, AccountId, FsBlockDb, WalletDb, }; +use zcash_primitives::consensus::NetworkConstants; use zcash_primitives::{ block::BlockHash, consensus::{ @@ -65,6 +66,7 @@ use zcash_primitives::{ zip32::{self, DiversifierIndex}, }; use zcash_proofs::prover::LocalTxProver; +use zip32::ChildIndex; use crate::utils::{catch_unwind, exception::unwrap_exc_or}; @@ -404,174 +406,6 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_isSeedRel unwrap_exc_or(&mut env, res, JNI_FALSE) } -/// Derives and returns a unified spending key from the given seed for the given account ID. -/// -/// Returns the newly created [ZIP 316] account identifier, along with the binary encoding -/// of the [`UnifiedSpendingKey`] for the newly created account. The caller should store -/// the returned spending key in a secure fashion. -#[unsafe(no_mangle)] -pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustDerivationTool_deriveSpendingKey< - 'local, ->( - mut env: JNIEnv<'local>, - _: JClass<'local>, - seed: JByteArray<'local>, - account: jint, - network_id: jint, -) -> jobject { - let res = catch_unwind(&mut env, |env| { - let _span = tracing::info_span!("RustDerivationTool.deriveSpendingKey").entered(); - let network = parse_network(network_id as u32)?; - let seed = SecretVec::new(env.convert_byte_array(seed).unwrap()); - let account = account_id_from_jint(account)?; - - let usk = UnifiedSpendingKey::from_seed(&network, seed.expose_secret(), account) - .map_err(|e| anyhow!("error generating unified spending key from seed: {:?}", e))?; - - Ok(encode_usk(env, account, usk)?.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_deriveUnifiedFullViewingKeysFromSeed< - 'local, ->( - mut env: JNIEnv<'local>, - _: JClass<'local>, - seed: JByteArray<'local>, - accounts: jint, - network_id: jint, -) -> jobjectArray { - let res = catch_unwind(&mut env, |env| { - let _span = tracing::info_span!("RustDerivationTool.deriveUnifiedFullViewingKeysFromSeed") - .entered(); - let network = parse_network(network_id as u32)?; - let seed = env.convert_byte_array(seed).unwrap(); - let accounts = if accounts > 0 { - accounts as u32 - } else { - return Err(anyhow!("accounts argument must be greater than zero")); - }; - - let ufvks: Vec<_> = (0..accounts) - .map(|account| { - let account_id = zip32::AccountId::try_from(account) - .map_err(|_| anyhow!("Invalid account ID"))?; - UnifiedSpendingKey::from_seed(&network, &seed, account_id) - .map_err(|e| { - anyhow!("error generating unified spending key from seed: {:?}", e) - }) - .map(|usk| usk.to_unified_full_viewing_key().encode(&network)) - }) - .collect::>()?; - - Ok(utils::rust_vec_to_java( - env, - ufvks, - "java/lang/String", - |env, ufvk| env.new_string(ufvk), - |env| env.new_string(""), - )? - .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_deriveUnifiedAddressFromSeed< - 'local, ->( - mut env: JNIEnv<'local>, - _: JClass<'local>, - seed: JByteArray<'local>, - account_index: jint, - network_id: jint, -) -> jstring { - let res = panic::catch_unwind(|| { - let _span = - tracing::info_span!("RustDerivationTool.deriveUnifiedAddressFromSeed").entered(); - let network = parse_network(network_id as u32)?; - let seed = env.convert_byte_array(seed).unwrap(); - let account_id = account_id_from_jint(account_index)?; - - let ufvk = UnifiedSpendingKey::from_seed(&network, &seed, account_id) - .map_err(|e| anyhow!("error generating unified spending key from seed: {:?}", e)) - .map(|usk| usk.to_unified_full_viewing_key())?; - - let (ua, _) = ufvk - .find_address(DiversifierIndex::new(), DEFAULT_ADDRESS_REQUEST) - .expect("At least one Unified Address should be derivable"); - let address_str = ua.encode(&network); - let output = env - .new_string(address_str) - .expect("Couldn't create Java string!"); - Ok(output.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_deriveUnifiedAddressFromViewingKey< - 'local, ->( - mut env: JNIEnv<'local>, - _: JClass<'local>, - ufvk_string: JString<'local>, - network_id: jint, -) -> jstring { - let res = catch_unwind(&mut env, |env| { - let _span = - tracing::info_span!("RustDerivationTool.deriveUnifiedAddressFromViewingKey").entered(); - let network = parse_network(network_id as u32)?; - let ufvk_string = utils::java_string_to_rust(env, &ufvk_string); - let ufvk = match UnifiedFullViewingKey::decode(&network, &ufvk_string) { - Ok(ufvk) => ufvk, - Err(e) => { - return Err(anyhow!( - "Error while deriving viewing key from string input: {}", - e, - )); - } - }; - - // Derive the default Unified Address (containing the default Sapling payment - // address that older SDKs used). - let (ua, _) = ufvk.default_address(DEFAULT_ADDRESS_REQUEST)?; - let address_str = ua.encode(&network); - let output = env - .new_string(address_str) - .expect("Couldn't create Java string!"); - Ok(output.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_deriveUnifiedFullViewingKey< - 'local, ->( - mut env: JNIEnv<'local>, - _: JClass<'local>, - usk: JByteArray<'local>, - network_id: jint, -) -> jstring { - let res = panic::catch_unwind(|| { - let _span = tracing::info_span!("RustDerivationTool.deriveUnifiedFullViewingKey").entered(); - let usk = decode_usk(&env, usk)?; - let network = parse_network(network_id as u32)?; - - let ufvk = usk.to_unified_full_viewing_key(); - - let output = env - .new_string(ufvk.encode(&network)) - .expect("Couldn't create Java string!"); - - Ok(output.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_RustBackend_getCurrentAddress<'local>( mut env: JNIEnv<'local>, @@ -2003,6 +1837,235 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_branchIdF unwrap_exc_or(&mut env, res, -1) } +// +// Derivation tool +// + +/// Derives and returns a unified spending key from the given seed for the given account ID. +/// +/// Returns the newly created [ZIP 316] account identifier, along with the binary encoding +/// of the [`UnifiedSpendingKey`] for the newly created account. The caller should store +/// the returned spending key in a secure fashion. +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustDerivationTool_deriveSpendingKey< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + seed: JByteArray<'local>, + account: jint, + network_id: jint, +) -> jobject { + let res = catch_unwind(&mut env, |env| { + let _span = tracing::info_span!("RustDerivationTool.deriveSpendingKey").entered(); + let network = parse_network(network_id as u32)?; + let seed = SecretVec::new(env.convert_byte_array(seed).unwrap()); + let account = account_id_from_jint(account)?; + + let usk = UnifiedSpendingKey::from_seed(&network, seed.expose_secret(), account) + .map_err(|e| anyhow!("error generating unified spending key from seed: {:?}", e))?; + + Ok(encode_usk(env, account, usk)?.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_deriveUnifiedFullViewingKeysFromSeed< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + seed: JByteArray<'local>, + accounts: jint, + network_id: jint, +) -> jobjectArray { + let res = catch_unwind(&mut env, |env| { + let _span = tracing::info_span!("RustDerivationTool.deriveUnifiedFullViewingKeysFromSeed") + .entered(); + let network = parse_network(network_id as u32)?; + let seed = env.convert_byte_array(seed).unwrap(); + let accounts = if accounts > 0 { + accounts as u32 + } else { + return Err(anyhow!("accounts argument must be greater than zero")); + }; + + let ufvks: Vec<_> = (0..accounts) + .map(|account| { + let account_id = zip32::AccountId::try_from(account) + .map_err(|_| anyhow!("Invalid account ID"))?; + UnifiedSpendingKey::from_seed(&network, &seed, account_id) + .map_err(|e| { + anyhow!("error generating unified spending key from seed: {:?}", e) + }) + .map(|usk| usk.to_unified_full_viewing_key().encode(&network)) + }) + .collect::>()?; + + Ok(utils::rust_vec_to_java( + env, + ufvks, + "java/lang/String", + |env, ufvk| env.new_string(ufvk), + |env| env.new_string(""), + )? + .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_deriveUnifiedAddressFromSeed< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + seed: JByteArray<'local>, + account_index: jint, + network_id: jint, +) -> jstring { + let res = panic::catch_unwind(|| { + let _span = + tracing::info_span!("RustDerivationTool.deriveUnifiedAddressFromSeed").entered(); + let network = parse_network(network_id as u32)?; + let seed = env.convert_byte_array(seed).unwrap(); + let account_id = account_id_from_jint(account_index)?; + + let ufvk = UnifiedSpendingKey::from_seed(&network, &seed, account_id) + .map_err(|e| anyhow!("error generating unified spending key from seed: {:?}", e)) + .map(|usk| usk.to_unified_full_viewing_key())?; + + let (ua, _) = ufvk + .find_address(DiversifierIndex::new(), DEFAULT_ADDRESS_REQUEST) + .expect("At least one Unified Address should be derivable"); + let address_str = ua.encode(&network); + let output = env + .new_string(address_str) + .expect("Couldn't create Java string!"); + Ok(output.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_deriveUnifiedAddressFromViewingKey< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + ufvk_string: JString<'local>, + network_id: jint, +) -> jstring { + let res = catch_unwind(&mut env, |env| { + let _span = + tracing::info_span!("RustDerivationTool.deriveUnifiedAddressFromViewingKey").entered(); + let network = parse_network(network_id as u32)?; + let ufvk_string = utils::java_string_to_rust(env, &ufvk_string); + let ufvk = match UnifiedFullViewingKey::decode(&network, &ufvk_string) { + Ok(ufvk) => ufvk, + Err(e) => { + return Err(anyhow!( + "Error while deriving viewing key from string input: {}", + e, + )); + } + }; + + // Derive the default Unified Address (containing the default Sapling payment + // address that older SDKs used). + let (ua, _) = ufvk.default_address(DEFAULT_ADDRESS_REQUEST)?; + let address_str = ua.encode(&network); + let output = env + .new_string(address_str) + .expect("Couldn't create Java string!"); + Ok(output.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_deriveUnifiedFullViewingKey< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + usk: JByteArray<'local>, + network_id: jint, +) -> jstring { + let res = panic::catch_unwind(|| { + let _span = tracing::info_span!("RustDerivationTool.deriveUnifiedFullViewingKey").entered(); + let usk = decode_usk(&env, usk)?; + let network = parse_network(network_id as u32)?; + + let ufvk = usk.to_unified_full_viewing_key(); + + let output = env + .new_string(ufvk.encode(&network)) + .expect("Couldn't create Java string!"); + + Ok(output.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, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + context_string: JByteArray<'local>, + seed: JByteArray<'local>, +) -> jbyteArray { + let res = panic::catch_unwind(|| { + let _span = + tracing::info_span!("RustDerivationTool.deriveArbitraryWalletKeyFromSeed").entered(); + let context_string = env.convert_byte_array(context_string)?; + let seed = SecretVec::new(env.convert_byte_array(seed)?); + + let key = + zip32::arbitrary::SecretKey::from_path(&context_string, seed.expose_secret(), &[]); + + Ok(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_deriveArbitraryAccountKeyFromSeed< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + context_string: JByteArray<'local>, + seed: JByteArray<'local>, + account: jint, + network_id: jint, +) -> jbyteArray { + let res = panic::catch_unwind(|| { + let _span = + tracing::info_span!("RustDerivationTool.deriveArbitraryAccountKeyFromSeed").entered(); + let network = parse_network(network_id as u32)?; + let context_string = env.convert_byte_array(context_string)?; + let seed = SecretVec::new(env.convert_byte_array(seed)?); + let account = account_id_from_jint(account)?; + + let key = zip32::arbitrary::SecretKey::from_path( + &context_string, + seed.expose_secret(), + &[ + ChildIndex::hardened(32), + ChildIndex::hardened(network.coin_type()), + ChildIndex::hardened(account.into()), + ], + ); + + Ok(utils::rust_bytes_to_java(&env, key.data())?.into_raw()) + }); + unwrap_exc_or(&mut env, res, ptr::null_mut()) +} + // // Tor support // diff --git a/sdk-incubator-lib/src/androidTest/kotlin/cash/z/ecc/android/sdk/internal/DerivationToolImplTest.kt b/sdk-incubator-lib/src/androidTest/kotlin/cash/z/ecc/android/sdk/internal/DerivationToolImplTest.kt new file mode 100644 index 000000000..eaf2ff360 --- /dev/null +++ b/sdk-incubator-lib/src/androidTest/kotlin/cash/z/ecc/android/sdk/internal/DerivationToolImplTest.kt @@ -0,0 +1,45 @@ +package cash.z.ecc.android.sdk.internal + +import cash.z.ecc.android.sdk.fixture.WalletFixture +import cash.z.ecc.android.sdk.model.Account +import cash.z.ecc.android.sdk.model.ZcashNetwork +import cash.z.ecc.android.sdk.tool.DerivationTool +import kotlinx.coroutines.test.runTest +import org.junit.Test +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi +import kotlin.test.assertEquals + +class DerivationToolImplTest { + private val seedPhrase = WalletFixture.Alice.seedPhrase + private val network = ZcashNetwork.Mainnet + private val account = Account.DEFAULT + + @OptIn(ExperimentalEncodingApi::class) + @Test + fun testDerivedArbitraryAccountKey() = + runTest { + val key = + DerivationTool.getInstance().deriveArbitraryAccountKey( + contextString = CONTEXT.toByteArray(), + seed = seedPhrase.toByteArray(), + network = network, + account = account, + ) + assertEquals(Base64.encode(key), "byyNHiMfj8N2tiCHc4Mv/0ts0IuUqDPe99MvW8B03IY=") + } + + @OptIn(ExperimentalEncodingApi::class) + @Test + fun testDerivedArbitraryWalletKey() = + runTest { + val key = + DerivationTool.getInstance().deriveArbitraryWalletKey( + contextString = CONTEXT.toByteArray(), + seed = seedPhrase.toByteArray(), + ) + assertEquals(Base64.encode(key), "1xm/qCXZqXgiRFT1IVk97+5gv9BE5AjkOVWHwYU9RYQ=") + } +} + +private const val CONTEXT = "ZashiAddressBookEncryptionV1" 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 799cad5e3..d5691ea09 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 @@ -47,3 +47,15 @@ fun Derivation.deriveUnifiedFullViewingKeysTypesafe( network.id, numberOfAccounts ).map { UnifiedFullViewingKey(it) } + +fun Derivation.deriveArbitraryWalletKeyTypesafe( + contextString: ByteArray, + seed: ByteArray +): ByteArray = deriveArbitraryWalletKey(contextString, seed) + +fun Derivation.deriveArbitraryAccountKeyTypesafe( + contextString: ByteArray, + seed: ByteArray, + network: ZcashNetwork, + account: Account +): ByteArray = deriveArbitraryAccountKey(contextString, seed, network.id, account.value) 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 bdf340af3..3356c0aae 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 @@ -34,4 +34,16 @@ internal class TypesafeDerivationToolImpl(private val derivation: Derivation) : viewingKey: String, network: ZcashNetwork, ): String = derivation.deriveUnifiedAddress(viewingKey, network) + + override suspend fun deriveArbitraryWalletKey( + contextString: ByteArray, + seed: ByteArray + ): ByteArray = derivation.deriveArbitraryWalletKeyTypesafe(contextString, seed) + + override suspend fun deriveArbitraryAccountKey( + contextString: ByteArray, + seed: ByteArray, + network: ZcashNetwork, + account: Account + ): ByteArray = derivation.deriveArbitraryAccountKeyTypesafe(contextString, seed, network, account) } 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 a1d7ef326..a21eeaa42 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 @@ -82,6 +82,41 @@ interface DerivationTool { network: ZcashNetwork ): String + /** + * 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. + * + * The resulting key will be the same across all networks (Zcash mainnet, Zcash + * testnet, OtherCoin mainnet, and so on). You can think of it as a context-specific + * seed fingerprint that can be used as (static) key material. + * + * [ZIP 32 Arbitrary Key]: https://zips.z.cash/zip-0032#specification-arbitrary-key-derivation + * + * @param contextString a globally-unique non-empty sequence of at most 252 bytes that + * identifies the desired context. + * @return an array of 32 bytes. + */ + suspend fun deriveArbitraryWalletKey( + contextString: ByteArray, + seed: ByteArray + ): ByteArray + + /** + * Derives a [ZIP 32 Arbitrary Key] from the given seed at the account level. + * + * [ZIP 32 Arbitrary Key]: https://zips.z.cash/zip-0032#specification-arbitrary-key-derivation + * + * @param contextString a globally-unique non-empty sequence of at most 252 bytes that + * identifies the desired context. + * @return an array of 32 bytes. + */ + suspend fun deriveArbitraryAccountKey( + contextString: ByteArray, + seed: ByteArray, + network: ZcashNetwork, + account: Account + ): ByteArray + companion object { const val DEFAULT_NUMBER_OF_ACCOUNTS = Derivation.DEFAULT_NUMBER_OF_ACCOUNTS