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}