use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine as _;
use serde_json::{json, Value};
use crate::remote::util::{hex, random_bytes, sha256_hex, unhex};
const COMMIT_DOMAIN: &[u8] = b"rk-pair/1 commit";
const SAS_DOMAIN: &[u8] = b"rk-pair/1 sas";
pub const NONCE_BYTES: usize = 16;
pub const MAX_LABEL_CHARS: usize = 64;
pub const CODE_DIGITS: usize = 6;
fn framed(parts: &[&[u8]]) -> Vec<u8> {
let mut out = Vec::new();
for part in parts {
out.extend_from_slice(&(part.len() as u32).to_be_bytes());
out.extend_from_slice(part);
}
out
}
fn label_bytes(label: Option<&str>) -> Vec<u8> {
match label {
Some(l) => [&[1u8][..], l.as_bytes()].concat(),
None => vec![0u8],
}
}
pub fn pairing_commit(public_key: &[u8], nonce_d: &[u8], label: Option<&str>) -> String {
let lb = label_bytes(label);
sha256_hex(&framed(&[COMMIT_DOMAIN, public_key, nonce_d, &lb]))
}
pub fn pairing_code(
public_key: &[u8],
nonce_d: &[u8],
nonce_h: &[u8],
label: Option<&str>,
host_binding: &[u8],
) -> String {
let lb = label_bytes(label);
let digest = ring::digest::digest(
&ring::digest::SHA256,
&framed(&[SAS_DOMAIN, public_key, nonce_d, nonce_h, &lb, host_binding]),
);
let d = digest.as_ref();
let n = u32::from_be_bytes([d[0], d[1], d[2], d[3]]) % 1_000_000;
format!("{n:06}")
}
pub struct PairingInitiator {
public_key: Vec<u8>,
label: Option<String>,
nonce: [u8; NONCE_BYTES],
host_binding: Vec<u8>,
}
impl PairingInitiator {
pub fn new(public_key: &[u8], label: Option<&str>, host_binding: &[u8]) -> Self {
Self {
public_key: public_key.to_vec(),
label: label.map(str::to_string),
nonce: random_bytes::<NONCE_BYTES>(),
host_binding: host_binding.to_vec(),
}
}
pub fn begin_body(&self) -> Value {
let mut body = json!({
"publicKey": URL_SAFE_NO_PAD.encode(&self.public_key),
"commit": pairing_commit(&self.public_key, &self.nonce, self.label.as_deref()),
});
if let Some(label) = &self.label {
body["label"] = Value::String(label.clone());
}
body
}
pub fn reveal_body(&self) -> Value {
json!({ "nonce": hex(&self.nonce) })
}
pub fn code(&self, nonce_h_hex: &str) -> Option<String> {
let nonce_h = unhex(nonce_h_hex).filter(|n| n.len() == NONCE_BYTES)?;
Some(pairing_code(
&self.public_key,
&self.nonce,
&nonce_h,
self.label.as_deref(),
&self.host_binding,
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_code_changes_with_every_transcript_field() {
let (k, nd, nh) = ([1u8; 32], [2u8; 16], [3u8; 16]);
let base = pairing_code(&k, &nd, &nh, Some("a"), b"b");
assert_eq!(base.len(), CODE_DIGITS);
assert_eq!(base, pairing_code(&k, &nd, &nh, Some("a"), b"b"));
assert_ne!(base, pairing_code(&[9u8; 32], &nd, &nh, Some("a"), b"b"));
assert_ne!(base, pairing_code(&k, &[9u8; 16], &nh, Some("a"), b"b"));
assert_ne!(base, pairing_code(&k, &nd, &[9u8; 16], Some("a"), b"b"));
assert_ne!(base, pairing_code(&k, &nd, &nh, Some("z"), b"b"));
assert_ne!(base, pairing_code(&k, &nd, &nh, None, b"b"));
assert_ne!(base, pairing_code(&k, &nd, &nh, Some("a"), b"c"));
}
#[test]
fn framing_keeps_field_boundaries() {
assert_ne!(
pairing_commit(&[1, 2], &[3], None),
pairing_commit(&[1], &[2, 3], None)
);
assert_ne!(
pairing_commit(&[1], &[2], Some("")),
pairing_commit(&[1], &[2], None)
);
}
}