lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

short_link.rs (12280B)


      1 use crate::auth::{RadrootsSimplexSmpEd25519Keypair, verify_signature};
      2 use crate::error::RadrootsSimplexSmpCryptoError;
      3 use crate::message::{
      4     RADROOTS_SIMPLEX_SMP_NONCE_LENGTH, RADROOTS_SIMPLEX_SMP_SHARED_SECRET_LENGTH, decrypt_padded,
      5     encrypt_padded, random_nonce,
      6 };
      7 use alloc::vec;
      8 use alloc::vec::Vec;
      9 use ed25519_dalek::Signer;
     10 use hkdf::Hkdf;
     11 use radroots_simplex_smp_proto::prelude::RadrootsSimplexSmpQueueLinkData;
     12 use sha2::Sha512;
     13 use sha3::{Digest, Sha3_256};
     14 
     15 pub const RADROOTS_SIMPLEX_SMP_SHORT_LINK_ID_LENGTH: usize = 24;
     16 pub const RADROOTS_SIMPLEX_SMP_SHORT_LINK_KEY_LENGTH: usize = 32;
     17 pub const RADROOTS_SIMPLEX_SMP_SHORT_LINK_CONTACT_KDF_OUTPUT_LENGTH: usize = 56;
     18 pub const RADROOTS_SIMPLEX_SMP_SHORT_LINK_FIXED_DATA_PADDED_LENGTH: usize = 2008;
     19 pub const RADROOTS_SIMPLEX_SMP_SHORT_LINK_USER_DATA_PADDED_LENGTH: usize = 13784;
     20 pub const RADROOTS_SIMPLEX_SMP_SHORT_LINK_CONTACT_INFO: &[u8] = b"SimpleXContactLink";
     21 pub const RADROOTS_SIMPLEX_SMP_SHORT_LINK_INVITATION_INFO: &[u8] = b"SimpleXInvLink";
     22 pub const RADROOTS_SIMPLEX_SMP_SHORT_LINK_SIGNATURE_LENGTH: usize = 64;
     23 
     24 #[derive(Debug, Clone, PartialEq, Eq)]
     25 pub struct RadrootsSimplexSmpContactShortLinkKeyMaterial {
     26     pub link_id: Vec<u8>,
     27     pub link_data_key: Vec<u8>,
     28 }
     29 
     30 #[derive(Debug, Clone, PartialEq, Eq)]
     31 pub struct RadrootsSimplexSmpVerifiedShortLinkData {
     32     pub fixed_data: Vec<u8>,
     33     pub user_data: Vec<u8>,
     34 }
     35 
     36 pub fn derive_invitation_short_link_data_key(
     37     link_key: &[u8],
     38 ) -> Result<Vec<u8>, RadrootsSimplexSmpCryptoError> {
     39     validate_link_key(link_key)?;
     40     hkdf_expand(
     41         link_key,
     42         RADROOTS_SIMPLEX_SMP_SHORT_LINK_INVITATION_INFO,
     43         RADROOTS_SIMPLEX_SMP_SHORT_LINK_KEY_LENGTH,
     44     )
     45 }
     46 
     47 pub fn derive_contact_short_link_key_material(
     48     link_key: &[u8],
     49 ) -> Result<RadrootsSimplexSmpContactShortLinkKeyMaterial, RadrootsSimplexSmpCryptoError> {
     50     validate_link_key(link_key)?;
     51     let output = hkdf_expand(
     52         link_key,
     53         RADROOTS_SIMPLEX_SMP_SHORT_LINK_CONTACT_INFO,
     54         RADROOTS_SIMPLEX_SMP_SHORT_LINK_CONTACT_KDF_OUTPUT_LENGTH,
     55     )?;
     56     let (link_id, link_data_key) = output.split_at(RADROOTS_SIMPLEX_SMP_SHORT_LINK_ID_LENGTH);
     57     Ok(RadrootsSimplexSmpContactShortLinkKeyMaterial {
     58         link_id: link_id.to_vec(),
     59         link_data_key: link_data_key.to_vec(),
     60     })
     61 }
     62 
     63 pub fn sign_short_link_data(
     64     root_keypair: &RadrootsSimplexSmpEd25519Keypair,
     65     fixed_data: &[u8],
     66     user_data: &[u8],
     67 ) -> Result<(Vec<u8>, RadrootsSimplexSmpQueueLinkData), RadrootsSimplexSmpCryptoError> {
     68     let link_key = short_link_data_hash(fixed_data);
     69     let signing_key = root_keypair.signing_key()?;
     70     Ok((
     71         link_key,
     72         RadrootsSimplexSmpQueueLinkData {
     73             fixed_data: sign_payload(&signing_key, fixed_data),
     74             user_data: sign_payload(&signing_key, user_data),
     75         },
     76     ))
     77 }
     78 
     79 pub fn encrypt_short_link_data(
     80     link_data_key: &[u8],
     81     signed_data: &RadrootsSimplexSmpQueueLinkData,
     82 ) -> Result<RadrootsSimplexSmpQueueLinkData, RadrootsSimplexSmpCryptoError> {
     83     let fixed_nonce = random_nonce()?;
     84     let user_nonce = random_nonce()?;
     85     encrypt_short_link_data_with_nonces(link_data_key, signed_data, &fixed_nonce, &user_nonce)
     86 }
     87 
     88 pub fn encrypt_short_link_data_with_nonces(
     89     link_data_key: &[u8],
     90     signed_data: &RadrootsSimplexSmpQueueLinkData,
     91     fixed_nonce: &[u8],
     92     user_nonce: &[u8],
     93 ) -> Result<RadrootsSimplexSmpQueueLinkData, RadrootsSimplexSmpCryptoError> {
     94     validate_link_key(link_data_key)?;
     95     Ok(RadrootsSimplexSmpQueueLinkData {
     96         fixed_data: encrypt_link_data_part(
     97             link_data_key,
     98             fixed_nonce,
     99             &signed_data.fixed_data,
    100             RADROOTS_SIMPLEX_SMP_SHORT_LINK_FIXED_DATA_PADDED_LENGTH,
    101         )?,
    102         user_data: encrypt_link_data_part(
    103             link_data_key,
    104             user_nonce,
    105             &signed_data.user_data,
    106             RADROOTS_SIMPLEX_SMP_SHORT_LINK_USER_DATA_PADDED_LENGTH,
    107         )?,
    108     })
    109 }
    110 
    111 pub fn decrypt_short_link_data(
    112     link_data_key: &[u8],
    113     encrypted_data: &RadrootsSimplexSmpQueueLinkData,
    114 ) -> Result<RadrootsSimplexSmpQueueLinkData, RadrootsSimplexSmpCryptoError> {
    115     validate_link_key(link_data_key)?;
    116     Ok(RadrootsSimplexSmpQueueLinkData {
    117         fixed_data: decrypt_link_data_part(
    118             "fixed_data",
    119             link_data_key,
    120             &encrypted_data.fixed_data,
    121         )?,
    122         user_data: decrypt_link_data_part("user_data", link_data_key, &encrypted_data.user_data)?,
    123     })
    124 }
    125 
    126 pub fn verify_signed_short_link_data(
    127     link_key: &[u8],
    128     root_public_key: &[u8],
    129     signed_data: &RadrootsSimplexSmpQueueLinkData,
    130 ) -> Result<RadrootsSimplexSmpVerifiedShortLinkData, RadrootsSimplexSmpCryptoError> {
    131     validate_link_key(link_key)?;
    132     let fixed = split_signed_payload("fixed_data", &signed_data.fixed_data)?;
    133     let user = split_signed_payload("user_data", &signed_data.user_data)?;
    134 
    135     if short_link_data_hash(fixed.payload).as_slice() != link_key {
    136         return Err(RadrootsSimplexSmpCryptoError::ShortLinkDataHashMismatch);
    137     }
    138     verify_signature(fixed.payload, root_public_key, fixed.signature)?;
    139     verify_signature(user.payload, root_public_key, user.signature)?;
    140     Ok(RadrootsSimplexSmpVerifiedShortLinkData {
    141         fixed_data: fixed.payload.to_vec(),
    142         user_data: user.payload.to_vec(),
    143     })
    144 }
    145 
    146 pub fn decrypt_verify_short_link_data(
    147     link_key: &[u8],
    148     link_data_key: &[u8],
    149     root_public_key: &[u8],
    150     encrypted_data: &RadrootsSimplexSmpQueueLinkData,
    151 ) -> Result<RadrootsSimplexSmpVerifiedShortLinkData, RadrootsSimplexSmpCryptoError> {
    152     let signed_data = decrypt_short_link_data(link_data_key, encrypted_data)?;
    153     verify_signed_short_link_data(link_key, root_public_key, &signed_data)
    154 }
    155 
    156 fn short_link_data_hash(data: &[u8]) -> Vec<u8> {
    157     Sha3_256::digest(data).to_vec()
    158 }
    159 
    160 fn sign_payload(signing_key: &ed25519_dalek::SigningKey, payload: &[u8]) -> Vec<u8> {
    161     let signature = signing_key.sign(payload);
    162     let mut signed =
    163         Vec::with_capacity(RADROOTS_SIMPLEX_SMP_SHORT_LINK_SIGNATURE_LENGTH + payload.len());
    164     signed.extend_from_slice(&signature.to_bytes());
    165     signed.extend_from_slice(payload);
    166     signed
    167 }
    168 
    169 fn encrypt_link_data_part(
    170     link_data_key: &[u8],
    171     nonce: &[u8],
    172     data: &[u8],
    173     padded_len: usize,
    174 ) -> Result<Vec<u8>, RadrootsSimplexSmpCryptoError> {
    175     let mut encrypted = Vec::with_capacity(RADROOTS_SIMPLEX_SMP_NONCE_LENGTH + 16 + padded_len);
    176     let nonce_bytes: [u8; RADROOTS_SIMPLEX_SMP_NONCE_LENGTH] = nonce
    177         .try_into()
    178         .map_err(|_| RadrootsSimplexSmpCryptoError::InvalidNonceLength(nonce.len()))?;
    179     encrypted.extend_from_slice(&nonce_bytes);
    180     encrypted.extend_from_slice(&encrypt_padded(
    181         link_data_key,
    182         &nonce_bytes,
    183         data,
    184         padded_len,
    185     )?);
    186     Ok(encrypted)
    187 }
    188 
    189 fn decrypt_link_data_part(
    190     field: &'static str,
    191     link_data_key: &[u8],
    192     data: &[u8],
    193 ) -> Result<Vec<u8>, RadrootsSimplexSmpCryptoError> {
    194     if data.len() <= RADROOTS_SIMPLEX_SMP_NONCE_LENGTH {
    195         return Err(RadrootsSimplexSmpCryptoError::InvalidShortLinkDataLength {
    196             field,
    197             length: data.len(),
    198         });
    199     }
    200     let (nonce, ciphertext) = data.split_at(RADROOTS_SIMPLEX_SMP_NONCE_LENGTH);
    201     decrypt_padded(link_data_key, nonce, ciphertext)
    202 }
    203 
    204 struct SignedPayload<'a> {
    205     signature: &'a [u8],
    206     payload: &'a [u8],
    207 }
    208 
    209 fn split_signed_payload<'a>(
    210     field: &'static str,
    211     data: &'a [u8],
    212 ) -> Result<SignedPayload<'a>, RadrootsSimplexSmpCryptoError> {
    213     if data.len() <= RADROOTS_SIMPLEX_SMP_SHORT_LINK_SIGNATURE_LENGTH {
    214         return Err(RadrootsSimplexSmpCryptoError::InvalidShortLinkDataLength {
    215             field,
    216             length: data.len(),
    217         });
    218     }
    219     let (signature, payload) = data.split_at(RADROOTS_SIMPLEX_SMP_SHORT_LINK_SIGNATURE_LENGTH);
    220     Ok(SignedPayload { signature, payload })
    221 }
    222 
    223 fn validate_link_key(link_key: &[u8]) -> Result<(), RadrootsSimplexSmpCryptoError> {
    224     if link_key.len() != RADROOTS_SIMPLEX_SMP_SHARED_SECRET_LENGTH {
    225         return Err(RadrootsSimplexSmpCryptoError::InvalidShortLinkKeyLength(
    226             link_key.len(),
    227         ));
    228     }
    229     Ok(())
    230 }
    231 
    232 fn hkdf_expand(
    233     ikm: &[u8],
    234     info: &[u8],
    235     output_len: usize,
    236 ) -> Result<Vec<u8>, RadrootsSimplexSmpCryptoError> {
    237     let hkdf = Hkdf::<Sha512>::new(Some(b""), ikm);
    238     let mut output = vec![0_u8; output_len];
    239     hkdf.expand(info, &mut output)
    240         .map_err(|_| RadrootsSimplexSmpCryptoError::InvalidKeyDerivationLength(output_len))?;
    241     Ok(output)
    242 }
    243 
    244 #[cfg(test)]
    245 mod tests {
    246     use super::*;
    247     use ed25519_dalek::SigningKey;
    248 
    249     fn keypair(seed: u8) -> RadrootsSimplexSmpEd25519Keypair {
    250         let private_key = [seed; 32];
    251         let signing_key = SigningKey::from_bytes(&private_key);
    252         RadrootsSimplexSmpEd25519Keypair {
    253             public_key: signing_key.verifying_key().to_bytes().to_vec(),
    254             private_key: private_key.to_vec(),
    255         }
    256     }
    257 
    258     #[test]
    259     fn derives_invitation_and_contact_short_link_keys() {
    260         let link_key = [7_u8; RADROOTS_SIMPLEX_SMP_SHORT_LINK_KEY_LENGTH];
    261 
    262         let invitation = derive_invitation_short_link_data_key(&link_key).unwrap();
    263         let contact = derive_contact_short_link_key_material(&link_key).unwrap();
    264 
    265         assert_eq!(invitation.len(), RADROOTS_SIMPLEX_SMP_SHORT_LINK_KEY_LENGTH);
    266         assert_eq!(
    267             contact.link_id.len(),
    268             RADROOTS_SIMPLEX_SMP_SHORT_LINK_ID_LENGTH
    269         );
    270         assert_eq!(
    271             contact.link_data_key.len(),
    272             RADROOTS_SIMPLEX_SMP_SHORT_LINK_KEY_LENGTH
    273         );
    274         assert_ne!(invitation, contact.link_data_key);
    275     }
    276 
    277     #[test]
    278     fn signs_encrypts_decrypts_and_verifies_short_link_data() {
    279         let root = keypair(11);
    280         let fixed_data = b"rr-synth-fixed-link-data".to_vec();
    281         let user_data = b"rr-synth-user-link-data".to_vec();
    282         let (link_key, signed_data) = sign_short_link_data(&root, &fixed_data, &user_data).unwrap();
    283         let link_data_key = derive_invitation_short_link_data_key(&link_key).unwrap();
    284         let fixed_nonce = [1_u8; RADROOTS_SIMPLEX_SMP_NONCE_LENGTH];
    285         let user_nonce = [2_u8; RADROOTS_SIMPLEX_SMP_NONCE_LENGTH];
    286 
    287         let encrypted = encrypt_short_link_data_with_nonces(
    288             &link_data_key,
    289             &signed_data,
    290             &fixed_nonce,
    291             &user_nonce,
    292         )
    293         .unwrap();
    294 
    295         assert_eq!(
    296             encrypted.fixed_data.len(),
    297             RADROOTS_SIMPLEX_SMP_NONCE_LENGTH
    298                 + 16
    299                 + RADROOTS_SIMPLEX_SMP_SHORT_LINK_FIXED_DATA_PADDED_LENGTH
    300         );
    301         assert_eq!(
    302             encrypted.user_data.len(),
    303             RADROOTS_SIMPLEX_SMP_NONCE_LENGTH
    304                 + 16
    305                 + RADROOTS_SIMPLEX_SMP_SHORT_LINK_USER_DATA_PADDED_LENGTH
    306         );
    307         let verified =
    308             decrypt_verify_short_link_data(&link_key, &link_data_key, &root.public_key, &encrypted)
    309                 .unwrap();
    310         assert_eq!(verified.fixed_data, fixed_data);
    311         assert_eq!(verified.user_data, user_data);
    312     }
    313 
    314     #[test]
    315     fn rejects_short_link_hash_mismatch() {
    316         let root = keypair(13);
    317         let (link_key, signed_data) =
    318             sign_short_link_data(&root, b"rr-synth-fixed", b"rr-synth-user").unwrap();
    319         let mut wrong_link_key = link_key;
    320         wrong_link_key[0] ^= 0xff;
    321 
    322         let error = verify_signed_short_link_data(&wrong_link_key, &root.public_key, &signed_data)
    323             .unwrap_err();
    324 
    325         assert!(matches!(
    326             error,
    327             RadrootsSimplexSmpCryptoError::ShortLinkDataHashMismatch
    328         ));
    329     }
    330 
    331     #[test]
    332     fn rejects_tampered_signed_user_data() {
    333         let root = keypair(17);
    334         let (link_key, mut signed_data) =
    335             sign_short_link_data(&root, b"rr-synth-fixed", b"rr-synth-user").unwrap();
    336         let last = signed_data.user_data.last_mut().unwrap();
    337         *last ^= 0xff;
    338 
    339         let error =
    340             verify_signed_short_link_data(&link_key, &root.public_key, &signed_data).unwrap_err();
    341 
    342         assert!(matches!(
    343             error,
    344             RadrootsSimplexSmpCryptoError::SignatureVerificationFailed
    345         ));
    346     }
    347 }