Skip to main content

pray_core/
auth_store_keys.rs

1use super::support::*;
2use super::RegistryAuthStore;
3use crate::auth::{
4    AuthPasskeyChallengeResponse, AuthPasskeyEnrollmentResponse, AuthPasskeyLoginResponse,
5    AuthSessionKind, AuthSshKeyChallengeResponse, AuthSshKeyEnrollmentResponse,
6    AuthSshKeyLoginResponse,
7};
8use crate::{PrayError, PrayResult};
9use rusqlite::OptionalExtension;
10
11impl RegistryAuthStore {
12    pub fn request_passkey_challenge(
13        &self,
14        credential_id: &str,
15    ) -> PrayResult<AuthPasskeyChallengeResponse> {
16        validate_identifier(credential_id, "credential id")?;
17        let connection = self.connection()?;
18        let email: String = connection.query_row(
19            "SELECT email FROM passkeys WHERE credential_id = ?1",
20            rusqlite::params![credential_id],
21            |row| row.get(0),
22        )?;
23        let challenge = generate_auth_challenge("passkey", credential_id)?;
24        let challenge_id = generate_challenge_id(&email, credential_id, "passkey", &challenge)?;
25        store_challenge(&connection, &challenge_id, &email, &challenge, "passkey")?;
26        Ok(AuthPasskeyChallengeResponse {
27            credential_id: credential_id.to_string(),
28            challenge_id,
29            challenge,
30        })
31    }
32    pub fn respond_passkey_challenge(
33        &self,
34        credential_id: &str,
35        challenge_id: &str,
36        signature: &str,
37    ) -> PrayResult<AuthPasskeyLoginResponse> {
38        validate_identifier(credential_id, "credential id")?;
39        validate_identifier(challenge_id, "challenge id")?;
40        validate_signature(signature)?;
41        let connection = self.connection()?;
42        let email: String = connection.query_row(
43            "SELECT email FROM passkeys WHERE credential_id = ?1",
44            rusqlite::params![credential_id],
45            |row| row.get(0),
46        )?;
47        let challenge = load_challenge(&connection, challenge_id, &email, "passkey")?;
48        let public_key = load_passkey_public_key(&connection, credential_id)?;
49        verify_signature(&public_key, challenge.challenge.as_bytes(), signature)?;
50        mark_challenge_used(&connection, challenge_id)?;
51        let session = self.issue_session(&email, AuthSessionKind::Passkey)?;
52        Ok(AuthPasskeyLoginResponse {
53            email,
54            token: session.token,
55        })
56    }
57    pub fn request_ssh_key_challenge(
58        &self,
59        public_key: &str,
60    ) -> PrayResult<AuthSshKeyChallengeResponse> {
61        validate_public_key(public_key)?;
62        let connection = self.connection()?;
63        let (public_key, _) = parse_ssh_ed25519_public_key(public_key)?;
64        let fingerprint = ssh_key_fingerprint(&public_key);
65        let email: String = connection.query_row(
66            "SELECT email FROM ssh_keys WHERE fingerprint = ?1",
67            rusqlite::params![fingerprint],
68            |row| row.get(0),
69        )?;
70        let challenge = generate_auth_challenge("ssh_key", &public_key)?;
71        let challenge_id = generate_challenge_id(&email, &fingerprint, "ssh_key", &challenge)?;
72        store_challenge(&connection, &challenge_id, &email, &challenge, "ssh_key")?;
73        Ok(AuthSshKeyChallengeResponse {
74            fingerprint,
75            challenge_id,
76            challenge,
77        })
78    }
79    pub fn respond_ssh_key_challenge(
80        &self,
81        public_key: &str,
82        challenge_id: &str,
83        signature: &str,
84    ) -> PrayResult<AuthSshKeyLoginResponse> {
85        validate_public_key(public_key)?;
86        validate_identifier(challenge_id, "challenge id")?;
87        validate_signature(signature)?;
88        let connection = self.connection()?;
89        let (public_key, _) = parse_ssh_ed25519_public_key(public_key)?;
90        let fingerprint = ssh_key_fingerprint(&public_key);
91        let email: String = connection.query_row(
92            "SELECT email FROM ssh_keys WHERE fingerprint = ?1",
93            rusqlite::params![fingerprint],
94            |row| row.get(0),
95        )?;
96        let challenge = load_challenge(&connection, challenge_id, &email, "ssh_key")?;
97        verify_signature(&public_key, challenge.challenge.as_bytes(), signature)?;
98        mark_challenge_used(&connection, challenge_id)?;
99        let session = self.issue_session(&email, AuthSessionKind::SshKey)?;
100        Ok(AuthSshKeyLoginResponse {
101            email,
102            token: session.token,
103        })
104    }
105    pub fn enroll_passkey(
106        &self,
107        email: &str,
108        credential_id: &str,
109        public_key: &str,
110        label: Option<&str>,
111    ) -> PrayResult<AuthPasskeyEnrollmentResponse> {
112        validate_email(email)?;
113        validate_identifier(credential_id, "credential id")?;
114        validate_public_key(public_key)?;
115        let connection = self.connection()?;
116        ensure_user_can_authenticate(&connection, email)?;
117        let timestamp = current_unix_timestamp()?;
118        connection.execute(
119        "INSERT INTO passkeys (credential_id, email, public_key, label, created_at, last_used_at)
120         VALUES (?1, ?2, ?3, ?4, ?5, NULL)
121         ON CONFLICT(credential_id) DO UPDATE SET email = excluded.email, public_key = excluded.public_key, label = excluded.label",
122        rusqlite::params![credential_id, email, public_key, label.unwrap_or(""), timestamp],
123    )?;
124        Ok(AuthPasskeyEnrollmentResponse {
125            email: email.to_string(),
126            credential_id: credential_id.to_string(),
127            enrolled: true,
128        })
129    }
130    pub fn login_with_passkey(
131        &self,
132        credential_id: &str,
133    ) -> PrayResult<AuthPasskeyLoginResponse> {
134        validate_identifier(credential_id, "credential id")?;
135        let connection = self.connection()?;
136        let email: Option<String> = connection
137            .query_row(
138                "SELECT email FROM passkeys WHERE credential_id = ?1",
139                rusqlite::params![credential_id],
140                |row| row.get(0),
141            )
142            .optional()?;
143        let Some(email) = email else {
144            return Err(PrayError::Resolution(format!(
145                "unknown passkey credential: {credential_id}"
146            )));
147        };
148        let session = self.issue_session(&email, AuthSessionKind::Passkey)?;
149        connection.execute(
150            "UPDATE passkeys SET last_used_at = ?2 WHERE credential_id = ?1",
151            rusqlite::params![credential_id, current_unix_timestamp()?],
152        )?;
153        Ok(AuthPasskeyLoginResponse {
154            email,
155            token: session.token,
156        })
157    }
158    pub fn enroll_ssh_key(
159        &self,
160        email: &str,
161        public_key: &str,
162        label: Option<&str>,
163    ) -> PrayResult<AuthSshKeyEnrollmentResponse> {
164        validate_email(email)?;
165        validate_public_key(public_key)?;
166        let connection = self.connection()?;
167        ensure_user_can_authenticate(&connection, email)?;
168        let (public_key, _) = parse_ssh_ed25519_public_key(public_key)?;
169        let fingerprint = ssh_key_fingerprint(&public_key);
170        let timestamp = current_unix_timestamp()?;
171        connection.execute(
172        "INSERT INTO ssh_keys (fingerprint, email, public_key, label, created_at, last_used_at)
173         VALUES (?1, ?2, ?3, ?4, ?5, NULL)
174         ON CONFLICT(fingerprint) DO UPDATE SET email = excluded.email, public_key = excluded.public_key, label = excluded.label",
175        rusqlite::params![fingerprint, email, public_key, label.unwrap_or(""), timestamp],
176    )?;
177        Ok(AuthSshKeyEnrollmentResponse {
178            email: email.to_string(),
179            fingerprint,
180            enrolled: true,
181        })
182    }
183    pub fn login_with_ssh_key(&self, public_key: &str) -> PrayResult<AuthSshKeyLoginResponse> {
184        validate_public_key(public_key)?;
185        let connection = self.connection()?;
186        let (public_key, _) = parse_ssh_ed25519_public_key(public_key)?;
187        let fingerprint = ssh_key_fingerprint(&public_key);
188        let email: Option<String> = connection
189            .query_row(
190                "SELECT email FROM ssh_keys WHERE fingerprint = ?1",
191                rusqlite::params![fingerprint],
192                |row| row.get(0),
193            )
194            .optional()?;
195        let Some(email) = email else {
196            return Err(PrayError::Resolution(format!(
197                "unknown ssh key fingerprint: {fingerprint}"
198            )));
199        };
200        let session = self.issue_session(&email, AuthSessionKind::SshKey)?;
201        connection.execute(
202            "UPDATE ssh_keys SET last_used_at = ?2 WHERE fingerprint = ?1",
203            rusqlite::params![fingerprint, current_unix_timestamp()?],
204        )?;
205        Ok(AuthSshKeyLoginResponse {
206            email,
207            token: session.token,
208        })
209    }
210}