anvilsign in

collin/anvil

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