use super::EncryptedLinkRecoveryContext;
use crate::{
pubky_routing::identity_pair_path_domain, PaykitError, PublicKey, Result,
PAYKIT_PRIVATE_PATH_PREFIX,
};
pub(super) const PAYKIT_PATH_DOMAIN: &[u8] = b"paykit-path-v0";
pub(super) fn compute_private_payment_paths(
local_secret_key: &[u8; 32],
local_identity_public_key: &PublicKey,
remote_identity_public_key: &PublicKey,
remote_noise_public_key: &PublicKey,
recovery_context: &EncryptedLinkRecoveryContext,
) -> (String, String) {
let mut path_domain = identity_pair_path_domain(
PAYKIT_PATH_DOMAIN,
local_identity_public_key,
remote_identity_public_key,
);
recovery_context.append_bytes(
&mut path_domain,
local_identity_public_key.to_bytes() < remote_identity_public_key.to_bytes(),
);
pubky_noise::path_derivation::derive_asymmetric_paths(
local_secret_key,
remote_noise_public_key,
&path_domain,
PAYKIT_PRIVATE_PATH_PREFIX,
)
}
pub(super) fn validate_private_payment_paths(
config: &pubky_noise::PubkyNoiseConfig,
remote_identity_public_key: &PublicKey,
remote_noise_public_key: &PublicKey,
recovery_context: &EncryptedLinkRecoveryContext,
) -> Result<()> {
let (write_path, read_path) = compute_private_payment_paths(
&config.pubky_root_keypair.secret_key(),
config.local_session.info().public_key(),
remote_identity_public_key,
remote_noise_public_key,
recovery_context,
);
if config.write_path != write_path || config.read_path != read_path {
return Err(PaykitError::Validation(
"Noise config paths do not match Pubky identity pair".into(),
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{derive_paykit_noise_public_key, derive_paykit_noise_secret_key};
#[test]
fn test_private_payment_paths_match_for_derived_identity_noise_keys() {
let alice = pubky::Keypair::random();
let bob = pubky::Keypair::random();
let alice_noise_secret = derive_paykit_noise_secret_key(&alice.secret_key());
let bob_noise_secret = derive_paykit_noise_secret_key(&bob.secret_key());
let alice_noise_public = derive_paykit_noise_public_key(&alice.secret_key());
let bob_noise_public = derive_paykit_noise_public_key(&bob.secret_key());
let alice_id = "650e8400-e29b-41d4-a716-446655440000";
let bob_id = "550e8400-e29b-41d4-a716-446655440000";
let alice_context =
EncryptedLinkRecoveryContext::new(Some(alice_id), Some(bob_id)).unwrap();
let bob_context = EncryptedLinkRecoveryContext::new(Some(bob_id), Some(alice_id)).unwrap();
let (alice_write, alice_read) = compute_private_payment_paths(
&alice_noise_secret,
&alice.public_key(),
&bob.public_key(),
&bob_noise_public,
&alice_context,
);
let (bob_write, bob_read) = compute_private_payment_paths(
&bob_noise_secret,
&bob.public_key(),
&alice.public_key(),
&alice_noise_public,
&bob_context,
);
assert_eq!(alice_write, bob_read);
assert_eq!(alice_read, bob_write);
assert_ne!(alice_write, alice_read);
for context in [
EncryptedLinkRecoveryContext::default(),
EncryptedLinkRecoveryContext::new(Some(alice_id), None).unwrap(),
EncryptedLinkRecoveryContext::new(None, Some(bob_id)).unwrap(),
] {
let (write, read) = compute_private_payment_paths(
&alice_noise_secret,
&alice.public_key(),
&bob.public_key(),
&bob_noise_public,
&context,
);
assert_ne!(write, alice_write);
assert_ne!(read, alice_read);
}
}
#[test]
fn test_private_payment_paths_bind_both_identities_with_shared_noise_keys() {
let alice = pubky::Keypair::from_secret(&[1; 32]).public_key();
let bob = pubky::Keypair::from_secret(&[2; 32]).public_key();
let other = pubky::Keypair::from_secret(&[3; 32]).public_key();
let secret = derive_paykit_noise_secret_key(&[4; 32]);
let remote_noise = derive_paykit_noise_public_key(&[5; 32]);
let context = EncryptedLinkRecoveryContext::default();
let original =
compute_private_payment_paths(&secret, &alice, &bob, &remote_noise, &context);
for (local, remote) in [(&alice, &other), (&other, &bob)] {
let changed =
compute_private_payment_paths(&secret, local, remote, &remote_noise, &context);
assert_ne!(original.0, changed.0);
assert_ne!(original.1, changed.1);
}
}
}