//! The phone as a second factor (oxygenhub#356): a P-256 key made in the //! phone's Keystore, usable only after a fingerprint, face or the device //! PIN, signs each approval; the browser shows one number and the phone //! offers three, so a tap without reading approves nothing. use crate::db::{self, accounts::Account, approvals}; use crate::Shared; use rand::{seq::SliceRandom, Rng, RngCore}; use ring::signature::{UnparsedPublicKey, ECDSA_P256_SHA256_ASN1}; /// How long a phone has to answer. pub const APPROVAL_MS: i64 = 2 * 60_000; /// How long a proved second factor lets a session past the sudo step. pub const FRESH_MS: i64 = 15 * 60_000; /// The DER header of a P-256 SubjectPublicKeyInfo (what Android's /// `PublicKey.getEncoded()` gives), before the 65-byte point. const P256_SPKI: [u8; 26] = [ 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, ]; /// A phone's public key, given as hex of its SubjectPublicKeyInfo or of /// the bare uncompressed point: the point, as hex. None when it is /// neither, or not on the curve. pub fn public_point(hex_key: &str) -> Option { let bytes = hex::decode(hex_key.trim()).ok()?; let point = match bytes.len() { 91 if bytes[..26] == P256_SPKI => &bytes[26..], 65 => &bytes[..], _ => return None, }; if point[0] != 0x04 { return None; } // ring checks that the point is on the curve when it verifies, so a // bad key only ever fails to verify. Some(hex::encode(point)) } /// What the phone signs to answer approval `id`. pub fn message(id: &str, number: i64, approve: bool) -> Vec { format!("oxygenhub approval v1\n{id}\n{number}\n{}", if approve { "approve" } else { "deny" }).into_bytes() } /// Whether `signature` (DER, as ECDSA gives it) is the phone's over `msg`. pub fn verify(point_hex: &str, msg: &[u8], signature: &[u8]) -> bool { let Ok(point) = hex::decode(point_hex) else { return false }; UnparsedPublicKey::new(&ECDSA_P256_SHA256_ASN1, point).verify(msg, signature).is_ok() } /// Three different two-digit numbers in a random order, and the one the /// browser shows. pub fn numbers() -> (i64, [i64; 3]) { let mut rng = rand::thread_rng(); let mut all: Vec = (10..100).collect(); all.shuffle(&mut rng); let choices = [all[0], all[1], all[2]]; (choices[rng.gen_range(0..3)], choices) } fn new_id() -> String { let mut b = [0u8; 16]; rand::rngs::OsRng.fill_bytes(&mut b); hex::encode(b) } /// Ask the account's phones to approve `what`: recorded, and sent as an /// alert where alerts are on (the app also looks when it opens). For a /// step-up, `session_id` is the browser session it makes fresh. pub async fn ask(app: &Shared, account: &Account, kind: &str, what: &str, session_id: Option) -> sqlx::Result { let id = new_id(); let (number, choices) = numbers(); let choices = choices.iter().map(|n| n.to_string()).collect::>().join(","); let what: String = what.chars().take(200).collect(); approvals::create( &app.db, approvals::NewApproval { id: &id, account_id: account.id, kind, what: &what, number, choices: &choices, session_id, expires_ms: db::now_ms() + APPROVAL_MS, }, ) .await?; crate::push::send(app, account.id, crate::push::Alert { kind: "approval".into(), title: what, url: format!("/approvals/{id}") }); Ok(approvals::get(&app.db, &id).await?.expect("just made")) } #[cfg(test)] mod tests { use super::*; use ring::rand::SystemRandom; use ring::signature::{EcdsaKeyPair, KeyPair, ECDSA_P256_SHA256_ASN1_SIGNING}; #[test] fn a_phone_signature_verifies_and_nothing_else_does() { let rng = SystemRandom::new(); let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng).unwrap(); let pair = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8.as_ref(), &rng).unwrap(); let point = hex::encode(pair.public_key().as_ref()); let spki = format!("{}{point}", hex::encode(P256_SPKI)); assert_eq!(public_point(&spki).as_deref(), Some(point.as_str()), "the Keystore's encoding"); assert_eq!(public_point(&point).as_deref(), Some(point.as_str()), "a bare point"); assert_eq!(public_point("00ff"), None); let m = message("ab", 47, true); let sig = pair.sign(&rng, &m).unwrap(); assert!(verify(&point, &m, sig.as_ref())); assert!(!verify(&point, &message("ab", 48, true), sig.as_ref()), "another number"); assert!(!verify(&point, &message("ab", 47, false), sig.as_ref()), "a denial is not an approval"); assert!(!verify(&point, &message("ac", 47, true), sig.as_ref()), "another approval"); } #[test] fn three_different_numbers_one_shown() { for _ in 0..200 { let (n, c) = numbers(); assert!(c.contains(&n)); assert!(c[0] != c[1] && c[1] != c[2] && c[0] != c[2]); assert!(c.iter().all(|x| (10..100).contains(x))); } } }