Alternative ATProto PDS implementation
1//! Based on https://github.com/blacksky-algorithms/rsky/blob/main/rsky-pds/src/account_manager/helpers/password.rs
2//! blacksky-algorithms/rsky is licensed under the Apache License 2.0
3//!
4//! Modified for SQLite backend
5use crate::models::pds as models;
6use crate::models::pds::AppPassword;
7use anyhow::{Result, bail};
8use diesel::*;
9use rsky_common::{get_random_str, now};
10use rsky_lexicon::com::atproto::server::CreateAppPasswordOutput;
11#[expect(unused_imports)]
12pub(crate) use rsky_pds::account_manager::helpers::password::{
13 UpdateUserPasswordOpts, gen_salt_and_hash, hash_app_password, hash_with_salt, verify,
14};
15
16pub async fn verify_account_password(
17 did: &str,
18 password: &String,
19 db: &deadpool_diesel::Pool<
20 deadpool_diesel::Manager<SqliteConnection>,
21 deadpool_diesel::sqlite::Object,
22 >,
23) -> Result<bool> {
24 use crate::schema::pds::account::dsl as AccountSchema;
25
26 let did = did.to_owned();
27 let found = db
28 .get()
29 .await?
30 .interact(move |conn| {
31 AccountSchema::account
32 .filter(AccountSchema::did.eq(did))
33 .select(models::Account::as_select())
34 .first(conn)
35 .optional()
36 })
37 .await
38 .expect("Failed to get account")?;
39 if let Some(found) = found {
40 verify(password, &found.password)
41 } else {
42 Ok(false)
43 }
44}
45
46pub async fn verify_app_password(
47 did: &str,
48 password: &str,
49 db: &deadpool_diesel::Pool<
50 deadpool_diesel::Manager<SqliteConnection>,
51 deadpool_diesel::sqlite::Object,
52 >,
53) -> Result<Option<String>> {
54 use crate::schema::pds::app_password::dsl as AppPasswordSchema;
55
56 let did = did.to_owned();
57 let password = password.to_owned();
58 let password_encrypted = hash_app_password(&did, &password).await?;
59 let found = db
60 .get()
61 .await?
62 .interact(move |conn| {
63 AppPasswordSchema::app_password
64 .filter(AppPasswordSchema::did.eq(did))
65 .filter(AppPasswordSchema::password.eq(password_encrypted))
66 .select(AppPassword::as_select())
67 .first(conn)
68 .optional()
69 })
70 .await
71 .expect("Failed to get app password")?;
72 if let Some(found) = found {
73 Ok(Some(found.name))
74 } else {
75 Ok(None)
76 }
77}
78
79/// create an app password with format:
80/// 1234-abcd-5678-efgh
81pub async fn create_app_password(
82 did: String,
83 name: String,
84 db: &deadpool_diesel::Pool<
85 deadpool_diesel::Manager<SqliteConnection>,
86 deadpool_diesel::sqlite::Object,
87 >,
88) -> Result<CreateAppPasswordOutput> {
89 let str = &get_random_str()[0..16].to_lowercase();
90 let chunks = [&str[0..4], &str[4..8], &str[8..12], &str[12..16]];
91 let password = chunks.join("-");
92 let password_encrypted = hash_app_password(&did, &password).await?;
93
94 use crate::schema::pds::app_password::dsl as AppPasswordSchema;
95
96 let created_at = now();
97
98 db.get()
99 .await?
100 .interact(move |conn| {
101 let got: Option<AppPassword> = insert_into(AppPasswordSchema::app_password)
102 .values((
103 AppPasswordSchema::did.eq(did),
104 AppPasswordSchema::name.eq(&name),
105 AppPasswordSchema::password.eq(password_encrypted),
106 AppPasswordSchema::createdAt.eq(&created_at),
107 ))
108 .returning(AppPassword::as_select())
109 .get_result(conn)
110 .optional()?;
111 if got.is_some() {
112 Ok(CreateAppPasswordOutput {
113 name,
114 password,
115 created_at,
116 })
117 } else {
118 bail!("could not create app-specific password")
119 }
120 })
121 .await
122 .expect("Failed to create app password")
123}
124
125pub async fn list_app_passwords(
126 did: &str,
127 db: &deadpool_diesel::Pool<
128 deadpool_diesel::Manager<SqliteConnection>,
129 deadpool_diesel::sqlite::Object,
130 >,
131) -> Result<Vec<(String, String)>> {
132 use crate::schema::pds::app_password::dsl as AppPasswordSchema;
133
134 let did = did.to_owned();
135 db.get()
136 .await?
137 .interact(move |conn| {
138 Ok(AppPasswordSchema::app_password
139 .filter(AppPasswordSchema::did.eq(did))
140 .select((AppPasswordSchema::name, AppPasswordSchema::createdAt))
141 .get_results(conn)?)
142 })
143 .await
144 .expect("Failed to list app passwords")
145}
146
147pub async fn update_user_password(
148 opts: UpdateUserPasswordOpts,
149 db: &deadpool_diesel::Pool<
150 deadpool_diesel::Manager<SqliteConnection>,
151 deadpool_diesel::sqlite::Object,
152 >,
153) -> Result<()> {
154 use crate::schema::pds::account::dsl as AccountSchema;
155
156 db.get()
157 .await?
158 .interact(move |conn| {
159 _ = update(AccountSchema::account)
160 .filter(AccountSchema::did.eq(opts.did))
161 .set(AccountSchema::password.eq(opts.password_encrypted))
162 .execute(conn)?;
163 Ok(())
164 })
165 .await
166 .expect("Failed to update user password")
167}
168
169pub async fn delete_app_password(
170 did: &str,
171 name: &str,
172 db: &deadpool_diesel::Pool<
173 deadpool_diesel::Manager<SqliteConnection>,
174 deadpool_diesel::sqlite::Object,
175 >,
176) -> Result<()> {
177 use crate::schema::pds::app_password::dsl as AppPasswordSchema;
178
179 let did = did.to_owned();
180 let name = name.to_owned();
181 db.get()
182 .await?
183 .interact(move |conn| {
184 _ = delete(AppPasswordSchema::app_password)
185 .filter(AppPasswordSchema::did.eq(did))
186 .filter(AppPasswordSchema::name.eq(name))
187 .execute(conn)?;
188 Ok(())
189 })
190 .await
191 .expect("Failed to delete app password")
192}