anvilsign in

collin/anvil

1//! SSH public keys: parsing, registration, and lookup for git-over-SSH
2//! authentication.
3
4use ssh_key::{
5 HashAlg,
6 PublicKey,
7};
8
9use crate::error::{
10 Error,
11 Result,
12};
13use crate::models::SshKey;
14
15/// Parse an OpenSSH public-key line (`ssh-ed25519 AAAA… comment`) into its
16/// canonical SHA256 fingerprint and normalized key line, for registration.
17pub fn parse_public_key(openssh: &str) -> Result<(String, String)> {
18 let key = PublicKey::from_openssh(openssh.trim())
19 .map_err(|e| Error::Invalid(format!("invalid ssh public key: {e}")))?;
20 let fingerprint = key.fingerprint(HashAlg::Sha256).to_string();
21 let normalized = key
22 .to_openssh()
23 .map_err(|e| Error::Invalid(format!("encoding ssh public key: {e}")))?;
24 Ok((fingerprint, normalized))
25}
26
27/// Register an SSH public key for a user.
28///
29/// `fingerprint` must be the canonical SHA256 fingerprint and `content` the
30/// normalized OpenSSH key line. Returns [`Error::AlreadyExists`] if the
31/// fingerprint is already registered.
32pub async fn add(
33 db: &toasty::Db,
34 user_id: i64,
35 title: &str,
36 fingerprint: &str,
37 content: &str,
38) -> Result<SshKey> {
39 if find_by_fingerprint(db, fingerprint).await?.is_some() {
40 return Err(Error::AlreadyExists(format!("ssh key {fingerprint}")));
41 }
42 let mut db = db.clone();
43 let key = toasty::create!(SshKey {
44 user_id: user_id,
45 title: title,
46 fingerprint: fingerprint,
47 content: content,
48 created_at: crate::now(),
49 })
50 .exec(&mut db)
51 .await?;
52 Ok(key)
53}
54
55/// Look up a key by its fingerprint.
56pub async fn find_by_fingerprint(db: &toasty::Db, fingerprint: &str) -> Result<Option<SshKey>> {
57 let mut db = db.clone();
58 let key = SshKey::filter(SshKey::fields().fingerprint().eq(fingerprint))
59 .first()
60 .exec(&mut db)
61 .await?;
62 Ok(key)
63}
64
65/// Find the user id that owns the key with this fingerprint, if any.
66pub async fn find_user_id_by_fingerprint(
67 db: &toasty::Db,
68 fingerprint: &str,
69) -> Result<Option<i64>> {
70 Ok(find_by_fingerprint(db, fingerprint)
71 .await?
72 .map(|k| k.user_id))
73}
74
75/// Delete one of `user_id`'s keys by id. No-op if the key is missing or owned
76/// by someone else.
77pub async fn delete(db: &toasty::Db, id: i64, user_id: i64) -> Result<()> {
78 let mut conn = db.clone();
79 if let Some(key) = SshKey::filter(SshKey::fields().id().eq(id))
80 .first()
81 .exec(&mut conn)
82 .await?
83 && key.user_id == user_id
84 {
85 let mut conn = db.clone();
86 key.delete().exec(&mut conn).await?;
87 }
88 Ok(())
89}
90
91/// List a user's registered SSH keys.
92pub async fn list_by_user(db: &toasty::Db, user_id: i64) -> Result<Vec<SshKey>> {
93 let mut db = db.clone();
94 let keys = SshKey::filter(SshKey::fields().user_id().eq(user_id))
95 .exec(&mut db)
96 .await?;
97 Ok(keys)
98}