use crate::libsignal::crypto::aes_256_gcm_encrypt;
use crate::libsignal::protocol::{CurveError, KeyPair, PublicKey};
use crate::pair_code::PairCodeUtils;
use buffa::Enumeration;
#[cfg(test)]
use hkdf::Hkdf;
use hmac::{Hmac, KeyInit as _, Mac};
use rand::RngExt;
use sha2::{Digest, Sha256};
use waproto::whatsapp as wa;
const HANDOFF_INFO: &[u8] = b"shortcake-passkey-handoff-v1";
const ENC_KEY_INFO: &[u8] = b"Pairing Information Encryption Key";
const VERIFICATION_CODE_BYTES: usize = 5;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ShortcakeError {
#[error("invalid primary public key: {0}")]
InvalidPrimaryKey(CurveError),
#[error("X25519 agreement failed: {0}")]
KeyAgreement(CurveError),
#[error("HKDF expand failed for {0}")]
Hkdf(&'static str),
#[error("AES-256-GCM encryption failed")]
Aead,
#[error("failed to decode {0} protobuf")]
Decode(&'static str),
#[error("unexpected length: {what} expected {expected} got {got}")]
Length {
what: &'static str,
expected: usize,
got: usize,
},
}
pub struct PrimaryEphemeralIdentity {
pub public_key: [u8; 32],
pub nonce: [u8; 32],
}
pub struct EncryptedPairing {
pub encrypted_payload: Vec<u8>,
pub iv: [u8; 12],
}
pub struct ShortcakeUtils;
impl ShortcakeUtils {
pub fn build_companion_ephemeral_identity(
public_key: &[u8; 32],
device_type: wa::device_props::PlatformType,
ref_str: &str,
) -> Vec<u8> {
waproto::codec::companion_ephemeral_identity_to_vec(&wa::CompanionEphemeralIdentity {
public_key: Some(public_key.to_vec()),
device_type: Some(device_type),
r#ref: Some(ref_str.to_string()),
})
}
pub fn commitment_hash(
companion_ephemeral_identity: &[u8],
companion_nonce: &[u8; 32],
) -> [u8; 32] {
let mut h = Sha256::new();
h.update(companion_ephemeral_identity);
h.update(companion_nonce);
h.finalize().into()
}
pub fn build_prologue_payload(
companion_ephemeral_identity: &[u8],
commitment_hash: &[u8; 32],
) -> Vec<u8> {
waproto::codec::prologue_payload_to_vec(&wa::ProloguePayload {
companion_ephemeral_identity: Some(companion_ephemeral_identity.to_vec()),
commitment: buffa::MessageField::some(wa::CompanionCommitment {
hash: Some(commitment_hash.to_vec()),
}),
})
}
pub fn parse_primary_ephemeral_identity(
bytes: &[u8],
) -> Result<PrimaryEphemeralIdentity, ShortcakeError> {
let parsed = waproto::codec::primary_ephemeral_identity_decode(bytes)
.map_err(|_| ShortcakeError::Decode("primary_ephemeral_identity"))?;
let pk = parsed.public_key.unwrap_or_default();
let nc = parsed.nonce.unwrap_or_default();
let public_key: [u8; 32] =
pk.as_slice()
.try_into()
.map_err(|_| ShortcakeError::Length {
what: "primary_public_key",
expected: 32,
got: pk.len(),
})?;
let nonce: [u8; 32] = nc
.as_slice()
.try_into()
.map_err(|_| ShortcakeError::Length {
what: "primary_nonce",
expected: 32,
got: nc.len(),
})?;
Ok(PrimaryEphemeralIdentity { public_key, nonce })
}
pub fn derive_verification_code(
companion_nonce: &[u8; 32],
primary_public_key: &[u8; 32],
primary_nonce: &[u8; 32],
) -> String {
let mut h = Sha256::new();
h.update(companion_nonce);
h.update(primary_public_key);
let digest = h.finalize();
let mut out = [0u8; VERIFICATION_CODE_BYTES];
for (i, slot) in out.iter_mut().enumerate() {
*slot = primary_nonce[i] ^ digest[i];
}
PairCodeUtils::encode_crockford(&out)
}
pub fn derive_encryption_key_from_shared_secret(
shared_secret: &[u8; 32],
device_type: wa::device_props::PlatformType,
ref_str: &str,
) -> Result<[u8; 32], ShortcakeError> {
let salt = format!(
"Companion Pairing {} with ref {ref_str}",
device_type.to_i32()
);
let mut key = [0u8; 32];
crate::crypto::hkdf_sha256_into(
shared_secret,
Some(salt.as_bytes()),
ENC_KEY_INFO,
&mut key,
)
.map_err(|_| ShortcakeError::Hkdf("encryption_key"))?;
Ok(key)
}
pub fn derive_encryption_key(
companion_keypair: &KeyPair,
primary_public_key: &[u8; 32],
device_type: wa::device_props::PlatformType,
ref_str: &str,
) -> Result<[u8; 32], ShortcakeError> {
let primary = PublicKey::from_djb_public_key_bytes(primary_public_key)
.map_err(ShortcakeError::InvalidPrimaryKey)?;
let shared = companion_keypair
.private_key
.calculate_agreement(&primary)
.map_err(ShortcakeError::KeyAgreement)?;
Self::derive_encryption_key_from_shared_secret(&shared, device_type, ref_str)
}
pub fn build_pairing_request(
companion_public_key: &[u8; 32],
companion_identity_key: &[u8; 32],
adv_secret: &[u8; 32],
) -> Vec<u8> {
waproto::codec::pairing_request_to_vec(&wa::PairingRequest {
companion_public_key: Some(companion_public_key.to_vec()),
companion_identity_key: Some(companion_identity_key.to_vec()),
adv_secret: Some(adv_secret.to_vec()),
})
}
pub fn encrypt_pairing_request(
plaintext: &[u8],
key: &[u8; 32],
) -> Result<EncryptedPairing, ShortcakeError> {
let mut iv = [0u8; 12];
rand::make_rng::<rand::rngs::StdRng>().fill(&mut iv);
let mut encrypted_payload = Vec::with_capacity(plaintext.len() + 16);
aes_256_gcm_encrypt(key, &iv, b"", plaintext, &mut encrypted_payload)
.map_err(|_| ShortcakeError::Aead)?;
Ok(EncryptedPairing {
encrypted_payload,
iv,
})
}
pub fn build_encrypted_pairing_request(enc: &EncryptedPairing) -> Vec<u8> {
waproto::codec::encrypted_pairing_request_to_vec(&wa::EncryptedPairingRequest {
encrypted_payload: Some(enc.encrypted_payload.clone()),
iv: Some(enc.iv.to_vec()),
})
}
pub fn derive_pairing_handoff_hmac_key(
prior_adv_secret: &[u8; 32],
) -> Result<[u8; 32], ShortcakeError> {
let mut key = [0u8; 32];
crate::crypto::hkdf_sha256_into(prior_adv_secret, None, HANDOFF_INFO, &mut key)
.map_err(|_| ShortcakeError::Hkdf("handoff_key"))?;
Ok(key)
}
pub fn compute_pairing_handoff_proof(
handoff_key: &[u8; 32],
prologue_payload: &[u8],
) -> [u8; 32] {
let mut mac =
Hmac::<Sha256>::new_from_slice(handoff_key).expect("HMAC accepts any key length");
mac.update(prologue_payload);
mac.finalize().into_bytes().into()
}
pub fn generate_companion_ephemeral_keypair() -> KeyPair {
let mut rng = rand::make_rng::<rand::rngs::StdRng>();
KeyPair::generate(&mut rng)
}
pub fn generate_companion_nonce() -> [u8; 32] {
let mut nonce = [0u8; 32];
rand::make_rng::<rand::rngs::StdRng>().fill(&mut nonce);
nonce
}
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
use super::*;
use buffa::Message;
#[test]
fn commitment_is_sha256_of_identity_then_nonce() {
let identity = b"identity-bytes";
let nonce = [7u8; 32];
let got = ShortcakeUtils::commitment_hash(identity, &nonce);
let mut h = Sha256::new();
h.update(identity);
h.update(nonce);
let want: [u8; 32] = h.finalize().into();
assert_eq!(got, want);
let mut h2 = Sha256::new();
h2.update(nonce);
h2.update(identity);
let wrong: [u8; 32] = h2.finalize().into();
assert_ne!(got, wrong);
}
#[test]
fn verification_code_format_and_xor_order() {
let companion_nonce = [1u8; 32];
let primary_pub = [2u8; 32];
let primary_nonce = [3u8; 32];
let code = ShortcakeUtils::derive_verification_code(
&companion_nonce,
&primary_pub,
&primary_nonce,
);
assert_eq!(code.len(), 8);
const CROCKFORD: &[u8] = b"123456789ABCDEFGHJKLMNPQRSTVWXYZ";
assert!(code.bytes().all(|b| CROCKFORD.contains(&b)));
let mut h = Sha256::new();
h.update(companion_nonce);
h.update(primary_pub);
let d = h.finalize();
let mut out = [0u8; 5];
for i in 0..5 {
out[i] = primary_nonce[i] ^ d[i];
}
assert_eq!(code, PairCodeUtils::encode_crockford(&out));
assert_eq!(
code,
ShortcakeUtils::derive_verification_code(
&companion_nonce,
&primary_pub,
&primary_nonce
)
);
}
#[test]
fn parse_primary_ephemeral_identity_roundtrip_and_length_validation() {
let proto = wa::PrimaryEphemeralIdentity {
public_key: Some(vec![0xAB; 32]),
nonce: Some(vec![0xCD; 32]),
}
.encode_to_vec();
let parsed = ShortcakeUtils::parse_primary_ephemeral_identity(&proto).unwrap();
assert_eq!(parsed.public_key, [0xAB; 32]);
assert_eq!(parsed.nonce, [0xCD; 32]);
let bad_pk = wa::PrimaryEphemeralIdentity {
public_key: Some(vec![0xAB; 31]),
nonce: Some(vec![0xCD; 32]),
}
.encode_to_vec();
assert!(matches!(
ShortcakeUtils::parse_primary_ephemeral_identity(&bad_pk),
Err(ShortcakeError::Length {
what: "primary_public_key",
..
})
));
let bad_nonce = wa::PrimaryEphemeralIdentity {
public_key: Some(vec![0xAB; 32]),
nonce: Some(vec![0xCD; 1]),
}
.encode_to_vec();
assert!(matches!(
ShortcakeUtils::parse_primary_ephemeral_identity(&bad_nonce),
Err(ShortcakeError::Length {
what: "primary_nonce",
..
})
));
assert!(matches!(
ShortcakeUtils::parse_primary_ephemeral_identity(&[]),
Err(ShortcakeError::Length { got: 0, .. })
));
}
#[test]
fn encryption_key_uses_string_as_salt_not_info() {
let ikm = [9u8; 32];
let key = ShortcakeUtils::derive_encryption_key_from_shared_secret(
&ikm,
wa::device_props::PlatformType::CHROME,
"REF123",
)
.unwrap();
let salt = "Companion Pairing 1 with ref REF123";
let hk = Hkdf::<Sha256>::new(Some(salt.as_bytes()), &ikm);
let mut want = [0u8; 32];
hk.expand(b"Pairing Information Encryption Key", &mut want)
.unwrap();
assert_eq!(key, want);
let hk2 = Hkdf::<Sha256>::new(Some(b"Pairing Information Encryption Key"), &ikm);
let mut wrong = [0u8; 32];
hk2.expand(salt.as_bytes(), &mut wrong).unwrap();
assert_ne!(key, wrong);
assert_ne!(
key,
ShortcakeUtils::derive_encryption_key_from_shared_secret(
&ikm,
wa::device_props::PlatformType::FIREFOX,
"REF123"
)
.unwrap()
);
assert_ne!(
key,
ShortcakeUtils::derive_encryption_key_from_shared_secret(
&ikm,
wa::device_props::PlatformType::CHROME,
"OTHER"
)
.unwrap()
);
}
#[test]
fn handoff_key_and_proof() {
let prior = [5u8; 32];
let k = ShortcakeUtils::derive_pairing_handoff_hmac_key(&prior).unwrap();
let hk = Hkdf::<Sha256>::new(None, &prior);
let mut want = [0u8; 32];
hk.expand(b"shortcake-passkey-handoff-v1", &mut want)
.unwrap();
assert_eq!(k, want);
let proof = ShortcakeUtils::compute_pairing_handoff_proof(&k, b"prologue");
let mut mac = Hmac::<Sha256>::new_from_slice(&k).unwrap();
mac.update(b"prologue");
let want_proof: [u8; 32] = mac.finalize().into_bytes().into();
assert_eq!(proof, want_proof);
}
#[test]
fn protobufs_roundtrip_with_expected_fields() {
let id = ShortcakeUtils::build_companion_ephemeral_identity(
&[0xAA; 32],
wa::device_props::PlatformType::CHROME,
"theref",
);
let decoded = wa::CompanionEphemeralIdentity::decode_from_slice(id.as_slice()).unwrap();
assert_eq!(decoded.public_key.as_deref(), Some(&[0xAA; 32][..]));
assert_eq!(
decoded.device_type,
Some(wa::device_props::PlatformType::CHROME)
);
assert_eq!(decoded.r#ref.as_deref(), Some("theref"));
let prologue = ShortcakeUtils::build_prologue_payload(&id, &[0xBB; 32]);
let dp = wa::ProloguePayload::decode_from_slice(prologue.as_slice()).unwrap();
assert_eq!(
dp.companion_ephemeral_identity.as_deref(),
Some(id.as_slice())
);
assert_eq!(
dp.commitment.into_option().and_then(|c| c.hash).as_deref(),
Some(&[0xBB; 32][..])
);
let pr = ShortcakeUtils::build_pairing_request(&[1; 32], &[2; 32], &[3; 32]);
let dpr = wa::PairingRequest::decode_from_slice(pr.as_slice()).unwrap();
assert_eq!(dpr.companion_public_key.as_deref(), Some(&[1u8; 32][..]));
assert_eq!(dpr.companion_identity_key.as_deref(), Some(&[2u8; 32][..]));
assert_eq!(dpr.adv_secret.as_deref(), Some(&[3u8; 32][..]));
}
#[test]
fn encrypt_pairing_request_shape() {
let key = [4u8; 32];
let enc = ShortcakeUtils::encrypt_pairing_request(b"hello pairing", &key).unwrap();
assert_eq!(enc.iv.len(), 12);
assert_eq!(enc.encrypted_payload.len(), b"hello pairing".len() + 16);
let wire = ShortcakeUtils::build_encrypted_pairing_request(&enc);
let d = wa::EncryptedPairingRequest::decode_from_slice(wire.as_slice()).unwrap();
assert_eq!(d.iv.as_deref(), Some(&enc.iv[..]));
assert_eq!(d.encrypted_payload, Some(enc.encrypted_payload));
}
#[test]
fn full_handshake_interops_with_a_simulated_primary() {
use crate::libsignal::crypto::aes_256_gcm_decrypt;
let device_type = wa::device_props::PlatformType::CHROME;
let pairing_ref = "REF-XYZ";
let prior_adv_secret = [0x11u8; 32]; let new_adv_secret = [0x22u8; 32];
let companion_kp = ShortcakeUtils::generate_companion_ephemeral_keypair();
let companion_nonce = ShortcakeUtils::generate_companion_nonce();
let companion_pub: [u8; 32] = companion_kp
.public_key
.public_key_bytes()
.try_into()
.unwrap();
let identity = ShortcakeUtils::build_companion_ephemeral_identity(
&companion_pub,
device_type,
pairing_ref,
);
let commitment = ShortcakeUtils::commitment_hash(&identity, &companion_nonce);
let prologue = ShortcakeUtils::build_prologue_payload(&identity, &commitment);
let companion_handoff =
ShortcakeUtils::derive_pairing_handoff_hmac_key(&prior_adv_secret).unwrap();
let proof = ShortcakeUtils::compute_pairing_handoff_proof(&companion_handoff, &prologue);
let primary_kp = KeyPair::generate(&mut rand::make_rng::<rand::rngs::StdRng>());
let primary_pub: [u8; 32] = primary_kp.public_key.public_key_bytes().try_into().unwrap();
let primary_nonce = [0x33u8; 32];
let primary_wire = wa::PrimaryEphemeralIdentity {
public_key: Some(primary_pub.to_vec()),
nonce: Some(primary_nonce.to_vec()),
}
.encode_to_vec();
let parsed = ShortcakeUtils::parse_primary_ephemeral_identity(&primary_wire).unwrap();
let primary_handoff =
ShortcakeUtils::derive_pairing_handoff_hmac_key(&prior_adv_secret).unwrap();
assert_eq!(
proof,
ShortcakeUtils::compute_pairing_handoff_proof(&primary_handoff, &prologue),
"handoff proof must verify on the primary"
);
let companion_code = ShortcakeUtils::derive_verification_code(
&companion_nonce,
&parsed.public_key,
&parsed.nonce,
);
let primary_code = ShortcakeUtils::derive_verification_code(
&companion_nonce,
&primary_pub,
&primary_nonce,
);
assert_eq!(companion_code, primary_code);
let companion_key = ShortcakeUtils::derive_encryption_key(
&companion_kp,
&parsed.public_key,
device_type,
pairing_ref,
)
.unwrap();
let primary_shared = primary_kp
.private_key
.calculate_agreement(&PublicKey::from_djb_public_key_bytes(&companion_pub).unwrap())
.unwrap();
let primary_key = ShortcakeUtils::derive_encryption_key_from_shared_secret(
&primary_shared,
device_type,
pairing_ref,
)
.unwrap();
assert_eq!(companion_key, primary_key, "X25519+HKDF keys must agree");
let request =
ShortcakeUtils::build_pairing_request(&[0xAA; 32], &[0xBB; 32], &new_adv_secret);
let enc = ShortcakeUtils::encrypt_pairing_request(&request, &companion_key).unwrap();
let wire = ShortcakeUtils::build_encrypted_pairing_request(&enc);
let decoded = wa::EncryptedPairingRequest::decode_from_slice(wire.as_slice()).unwrap();
let iv: [u8; 12] = decoded.iv.unwrap().as_slice().try_into().unwrap();
let mut plaintext = Vec::new();
aes_256_gcm_decrypt(
&primary_key,
&iv,
b"",
&decoded.encrypted_payload.unwrap(),
&mut plaintext,
)
.unwrap();
let recovered = wa::PairingRequest::decode_from_slice(plaintext.as_slice()).unwrap();
assert_eq!(recovered.adv_secret.as_deref(), Some(&new_adv_secret[..]));
assert_eq!(
recovered.companion_public_key.as_deref(),
Some(&[0xAA; 32][..])
);
assert_eq!(
recovered.companion_identity_key.as_deref(),
Some(&[0xBB; 32][..])
);
}
}