| a22e4aec Andy McCall ↶ |
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 |
pub fn public_point(hex_key: &str) -> Option<String> { |
| 27 |
let bytes = hex::decode(hex_key.trim()).ok()?; |
| 28 |
let point = match bytes.len() { |
| 29 |
91 if bytes[..26] == P256_SPKI => &bytes[26..], |
| 30 |
65 => &bytes[..], |
| 31 |
_ => return None, |
| 32 |
}; |
| 33 |
if point[0] != 0x04 { |
| 34 |
return None; |
| 35 |
} |
| 36 |
|
| 37 |
|
| 38 |
Some(hex::encode(point)) |
| 39 |
} |
| 40 |
|
| 41 |
|
| 42 |
pub fn message(id: &str, number: i64, approve: bool) -> Vec<u8> { |
| 43 |
format!("oxygenhub approval v1\n{id}\n{number}\n{}", if approve { "approve" } else { "deny" }).into_bytes() |
| 44 |
} |
| 45 |
|
| 46 |
|
| 47 |
pub fn verify(point_hex: &str, msg: &[u8], signature: &[u8]) -> bool { |
| 48 |
let Ok(point) = hex::decode(point_hex) else { return false }; |
| 49 |
UnparsedPublicKey::new(&ECDSA_P256_SHA256_ASN1, point).verify(msg, signature).is_ok() |
| 50 |
} |
| 51 |
|
| 52 |
|
| 53 |
|
| 54 |
pub fn numbers() -> (i64, [i64; 3]) { |
| 55 |
let mut rng = rand::thread_rng(); |
| 56 |
let mut all: Vec<i64> = (10..100).collect(); |
| 57 |
all.shuffle(&mut rng); |
| 58 |
let choices = [all[0], all[1], all[2]]; |
| 59 |
(choices[rng.gen_range(0..3)], choices) |
| 60 |
} |
| 61 |
|
| 62 |
fn new_id() -> String { |
| 63 |
let mut b = [0u8; 16]; |
| 64 |
rand::rngs::OsRng.fill_bytes(&mut b); |
| 65 |
hex::encode(b) |
| 66 |
} |
| 67 |
|
| 68 |
|
| 69 |
|
| 70 |
|
| 71 |
pub async fn ask(app: &Shared, account: &Account, kind: &str, what: &str, session_id: Option<i64>) -> sqlx::Result<approvals::Approval> { |
| 72 |
let id = new_id(); |
| 73 |
let (number, choices) = numbers(); |
| 74 |
let choices = choices.iter().map(|n| n.to_string()).collect::<Vec<_>>().join(","); |
| 75 |
let what: String = what.chars().take(200).collect(); |
| 76 |
approvals::create( |
| 77 |
&app.db, |
| 78 |
approvals::NewApproval { |
| 79 |
id: &id, |
| 80 |
account_id: account.id, |
| 81 |
kind, |
| 82 |
what: &what, |
| 83 |
number, |
| 84 |
choices: &choices, |
| 85 |
session_id, |
| 86 |
expires_ms: db::now_ms() + APPROVAL_MS, |
| 87 |
}, |
| 88 |
) |
| 89 |
.await?; |
| 90 |
crate::push::send(app, account.id, crate::push::Alert { kind: "approval".into(), title: what, url: format!("/approvals/{id}") }); |
| 91 |
Ok(approvals::get(&app.db, &id).await?.expect("just made")) |
| 92 |
} |
| 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 |
fn a_phone_signature_verifies_and_nothing_else_does() { |
| 102 |
let rng = SystemRandom::new(); |
| 103 |
let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng).unwrap(); |
| 104 |
let pair = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8.as_ref(), &rng).unwrap(); |
| 105 |
let point = hex::encode(pair.public_key().as_ref()); |
| 106 |
let spki = format!("{}{point}", hex::encode(P256_SPKI)); |
| 107 |
assert_eq!(public_point(&spki).as_deref(), Some(point.as_str()), "the Keystore's encoding"); |
| 108 |
assert_eq!(public_point(&point).as_deref(), Some(point.as_str()), "a bare point"); |
| 109 |
assert_eq!(public_point("00ff"), None); |
| 110 |
let m = message("ab", 47, true); |
| 111 |
let sig = pair.sign(&rng, &m).unwrap(); |
| 112 |
assert!(verify(&point, &m, sig.as_ref())); |
| 113 |
assert!(!verify(&point, &message("ab", 48, true), sig.as_ref()), "another number"); |
| 114 |
assert!(!verify(&point, &message("ab", 47, false), sig.as_ref()), "a denial is not an approval"); |
| 115 |
assert!(!verify(&point, &message("ac", 47, true), sig.as_ref()), "another approval"); |
| 116 |
} |
| 117 |
|
| 118 |
#[test] |
| 119 |
fn three_different_numbers_one_shown() { |
| 120 |
for _ in 0..200 { |
| 121 |
let (n, c) = numbers(); |
| 122 |
assert!(c.contains(&n)); |
| 123 |
assert!(c[0] != c[1] && c[1] != c[2] && c[0] != c[2]); |
| 124 |
assert!(c.iter().all(|x| (10..100).contains(x))); |
| 125 |
} |
| 126 |
} |
| 127 |
} |