| 1 | |
|
| 2 | |
|
| 3 | |
|
| 4 | |
|
| 5 | |
|
| 6 | | use crate::db::{self, accounts::Account, approvals}; |
| 7 | | use crate::Shared; |
| 8 | | use rand::{seq::SliceRandom, Rng, RngCore}; |
| 9 | | use ring::signature::{UnparsedPublicKey, ECDSA_P256_SHA256_ASN1}; |
| 10 | |
|
| 11 | |
|
| 12 | | pub const APPROVAL_MS: i64 = 2 * 60_000; |
| 13 | |
|
| 14 | | pub const FRESH_MS: i64 = 15 * 60_000; |
| 15 | |
|
| 16 | |
|
| 17 | |
|
| 18 | | const P256_SPKI: [u8; 26] = [ |
| 19 | | 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, |
| 20 | | 0x07, 0x03, 0x42, 0x00, |
| 21 | | ]; |
| 22 | |
|
| 23 | |
|
| 24 | |
|
| 25 | |
|
| 26 | 4× | pub fn public_point(hex_key: &str) -> Option<String> { |
| 27 | 4× | let bytes = hex::decode(hex_key.trim()).ok()?; |
| 28 | 4× | let point = match bytes.len() { |
| 29 | 2× | 91 if bytes[..26] == P256_SPKI => &bytes[26..], |
| 30 | 1× | 65 => &bytes[..], |
| 31 | 1× | _ => return None, |
| 32 | | }; |
| 33 | 3× | if point[0] != 0x04 { |
| 34 | 0× | return None; |
| 35 | 3× | } |
| 36 | | |
| 37 | | |
| 38 | 3× | Some(hex::encode(point)) |
| 39 | 4× | } |
| 40 | |
|
| 41 | |
|
| 42 | 12× | pub fn message(id: &str, number: i64, approve: bool) -> Vec<u8> { |
| 43 | 12× | format!("oxygenhub approval v1\n{id}\n{number}\n{}", if approve { "approve" } else { "deny" }).into_bytes() |
| 44 | 12× | } |
| 45 | |
|
| 46 | |
|
| 47 | 8× | pub fn verify(point_hex: &str, msg: &[u8], signature: &[u8]) -> bool { |
| 48 | 8× | let Ok(point) = hex::decode(point_hex) else { return false }; |
| 49 | 8× | UnparsedPublicKey::new(&ECDSA_P256_SHA256_ASN1, point).verify(msg, signature).is_ok() |
| 50 | 8× | } |
| 51 | |
|
| 52 | |
|
| 53 | |
|
| 54 | 203× | pub fn numbers() -> (i64, [i64; 3]) { |
| 55 | 203× | let mut rng = rand::thread_rng(); |
| 56 | 203× | let mut all: Vec<i64> = (10..100).collect(); |
| 57 | 203× | all.shuffle(&mut rng); |
| 58 | 203× | let choices = [all[0], all[1], all[2]]; |
| 59 | 203× | (choices[rng.gen_range(0..3)], choices) |
| 60 | 203× | } |
| 61 | |
|
| 62 | 3× | fn new_id() -> String { |
| 63 | 3× | let mut b = [0u8; 16]; |
| 64 | 3× | rand::rngs::OsRng.fill_bytes(&mut b); |
| 65 | 3× | hex::encode(b) |
| 66 | 3× | } |
| 67 | |
|
| 68 | |
|
| 69 | |
|
| 70 | |
|
| 71 | 3× | pub async fn ask(app: &Shared, account: &Account, kind: &str, what: &str, session_id: Option<i64>) -> sqlx::Result<approvals::Approval> { |
| 72 | 3× | let id = new_id(); |
| 73 | 3× | let (number, choices) = numbers(); |
| 74 | 9× | let choices = choices.iter().map(|n| n.to_string()).collect::<Vec<_>>().join(","); |
| 75 | 3× | let what: String = what.chars().take(200).collect(); |
| 76 | 3× | approvals::create( |
| 77 | 3× | &app.db, |
| 78 | 3× | approvals::NewApproval { |
| 79 | 3× | id: &id, |
| 80 | 3× | account_id: account.id, |
| 81 | 3× | kind, |
| 82 | 3× | what: &what, |
| 83 | 3× | number, |
| 84 | 3× | choices: &choices, |
| 85 | 3× | session_id, |
| 86 | 3× | expires_ms: db::now_ms() + APPROVAL_MS, |
| 87 | 3× | }, |
| 88 | 3× | ) |
| 89 | 3× | .await?; |
| 90 | 3× | crate::push::send(app, account.id, crate::push::Alert { kind: "approval".into(), title: what, url: format!("/approvals/{id}") }); |
| 91 | 3× | Ok(approvals::get(&app.db, &id).await?.expect("just made")) |
| 92 | 3× | } |
| 93 | |
|
| 94 | | #[cfg(test)] |
| 95 | | mod tests { |
| 96 | | use super::*; |
| 97 | | use ring::rand::SystemRandom; |
| 98 | | use ring::signature::{EcdsaKeyPair, KeyPair, ECDSA_P256_SHA256_ASN1_SIGNING}; |
| 99 | |
|
| 100 | | #[test] |
| 101 | 1× | fn a_phone_signature_verifies_and_nothing_else_does() { |
| 102 | 1× | let rng = SystemRandom::new(); |
| 103 | 1× | let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng).unwrap(); |
| 104 | 1× | let pair = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8.as_ref(), &rng).unwrap(); |
| 105 | 1× | let point = hex::encode(pair.public_key().as_ref()); |
| 106 | 1× | let spki = format!("{}{point}", hex::encode(P256_SPKI)); |
| 107 | 1× | assert_eq!(public_point(&spki).as_deref(), Some(point.as_str()), "the Keystore's encoding"); |
| 108 | 1× | assert_eq!(public_point(&point).as_deref(), Some(point.as_str()), "a bare point"); |
| 109 | 1× | assert_eq!(public_point("00ff"), None); |
| 110 | 1× | let m = message("ab", 47, true); |
| 111 | 1× | let sig = pair.sign(&rng, &m).unwrap(); |
| 112 | 1× | assert!(verify(&point, &m, sig.as_ref())); |
| 113 | 1× | assert!(!verify(&point, &message("ab", 48, true), sig.as_ref()), "another number"); |
| 114 | 1× | assert!(!verify(&point, &message("ab", 47, false), sig.as_ref()), "a denial is not an approval"); |
| 115 | 1× | assert!(!verify(&point, &message("ac", 47, true), sig.as_ref()), "another approval"); |
| 116 | 1× | } |
| 117 | |
|
| 118 | | #[test] |
| 119 | 1× | fn three_different_numbers_one_shown() { |
| 120 | 1× | for _ in 0..200 { |
| 121 | 200× | let (n, c) = numbers(); |
| 122 | 200× | assert!(c.contains(&n)); |
| 123 | 200× | assert!(c[0] != c[1] && c[1] != c[2] && c[0] != c[2]); |
| 124 | 600× | assert!(c.iter().all(|x| (10..100).contains(x))); |
| 125 | | } |
| 126 | 1× | } |
| 127 | | } |