Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -38,6 +39,44 @@ interface Derivation {
numberOfAccounts: Int
): Array<String>

/**
* 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<ByteArray>

/**
* 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<ByteArray> =
derivePrivateUseMetadataKey(
accountMetadataKey_sk = accountMetadataKey.sk,
accountMetadataKey_c = accountMetadataKey.chainCode,
ufvk,
privateUseSubject,
networkId
)

override fun deriveArbitraryWalletKey(
contextString: ByteArray,
seed: ByteArray
Expand Down Expand Up @@ -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<ByteArray>

@JvmStatic
private external fun deriveArbitraryWalletKeyFromSeed(
contextString: ByteArray,
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Comment thread
daira marked this conversation as resolved.
*
* @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"
}
}
}
139 changes: 137 additions & 2 deletions backend-lib/src/main/rust/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -78,7 +79,9 @@ use zcash_protocol::{
value::{ZatBalance, Zatoshis},
ShieldedProtocol,
};
use zip32::{fingerprint::SeedFingerprint, ChildIndex, DiversifierIndex};
use zip32::{
fingerprint::SeedFingerprint, registered::PathElement, ChainCode, ChildIndex, DiversifierIndex,
};

use crate::utils::{catch_unwind, exception::unwrap_exc_or};

Expand Down Expand Up @@ -2368,6 +2371,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<JObject<'a>> {
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 = secret_from_jni(env, seed)?;
let account = zip32_account_index_from_jlong(account_index)?;

let key = zip32::registered::SecretKey::from_subpath(
b"MetadataKeys",
seed.expose_secret(),
// TODO: Change this to whatever ZIP number is assigned to the metadata key ZIP draft.
325,
&[
PathElement::new(ChildIndex::hardened(network.coin_type()), &[]),
PathElement::new(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)
};
Comment on lines +2435 to +2455

@daira daira Feb 19, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fn registered_secret_key_from_jni(
    env: &JNIEnv,
    key_sk: JByteArray,
    key_c: JByteArray,
) -> anyhow::Result<zip32::registered::SecretKey> {
    let sk = utils::java_bytes_to_rust(env, &key_sk)?
        .as_slice()
        .try_into()
        .map_err(|_| anyhow!("Incorrect length for key_sk"))?;
    let c = utils::java_bytes_to_rust(env, &key_c)?
        .as_slice()
        .try_into()
        .map_err(|_| anyhow!("Incorrect length for key_c"))?;

    Ok(zip32::registered::SecretKey::from_parts(sk, ChainCode::new(c)))
}
Suggested change
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 account_metadata_key = registered_secret_key_from_jni(env, account_metadata_key_sk, 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 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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -52,6 +54,19 @@ fun Derivation.deriveUnifiedFullViewingKeysTypesafe(
numberOfAccounts
).map { UnifiedFullViewingKey(it) }

fun Derivation.deriveAccountMetadataKeyTypesafe(
Comment thread
daira marked this conversation as resolved.
seed: ByteArray,
network: ZcashNetwork,
accountIndex: Zip32AccountIndex
): JniMetadataKey = deriveAccountMetadataKey(seed, network.id, accountIndex.index)

fun Derivation.derivePrivateUseMetadataKeyTypesafe(
Comment thread
daira marked this conversation as resolved.
accountMetadataKey: AccountMetadataKey,
ufvk: String?,
network: ZcashNetwork,
privateUseSubject: ByteArray
): Array<ByteArray> = derivePrivateUseMetadataKey(accountMetadataKey.toUnsafe(), ufvk, network.id, privateUseSubject)

fun Derivation.deriveArbitraryWalletKeyTypesafe(
contextString: ByteArray,
seed: ByteArray
Expand Down

@daira daira Feb 19, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is useless cruft. The typesafe mapping is already done in DerivationToolExt.

In a follow-up PR, delete Typesafe from the method names in Derivation, then delete this file and the only use of it in DerivationTool, which can then just use:

private val instance = SuspendingLazy<Unit, DerivationTool> { RustDerivationTool.new() }

Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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))
Comment thread
daira marked this conversation as resolved.

override suspend fun derivePrivateUseMetadataKey(
accountMetadataKey: AccountMetadataKey,
ufvk: String?,
network: ZcashNetwork,
privateUseSubject: ByteArray
): Array<ByteArray> =
derivation.derivePrivateUseMetadataKeyTypesafe(
Comment thread
daira marked this conversation as resolved.
accountMetadataKey,
ufvk,
network,
privateUseSubject
)

override suspend fun deriveArbitraryWalletKey(
contextString: ByteArray,
seed: ByteArray
Expand Down
Loading