1
+ use argon2:: { password_hash, Argon2 , PasswordHasher , PasswordVerifier } ;
1
2
use std:: error:: Error ;
2
- use argon2 :: { password_hash , Argon2 , PasswordHash , PasswordHasher , PasswordVerifier } ;
3
+ use std :: sync :: Arc ;
3
4
4
5
use password_hash:: PasswordHashString ;
5
6
@@ -10,21 +11,24 @@ use uuid::Uuid;
10
11
11
12
use tokio:: sync:: Semaphore ;
12
13
13
- #[ derive( sqlx:: Type ) ]
14
+ #[ derive( sqlx:: Type , Debug ) ]
14
15
#[ sqlx( transparent) ]
15
16
pub struct AccountId ( pub Uuid ) ;
16
17
17
-
18
18
pub struct AccountsManager {
19
- hashing_semaphore : Semaphore ,
19
+ hashing_semaphore : Arc < Semaphore > ,
20
20
}
21
21
22
22
#[ derive( Debug , thiserror:: Error ) ]
23
23
pub enum CreateError {
24
- #[ error( "email in-use" ) ]
24
+ #[ error( "error creating account: email in-use" ) ]
25
25
EmailInUse ,
26
- General ( #[ source]
27
- #[ from] GeneralError ) ,
26
+ #[ error( "error creating account" ) ]
27
+ General (
28
+ #[ source]
29
+ #[ from]
30
+ GeneralError ,
31
+ ) ,
28
32
}
29
33
30
34
#[ derive( Debug , thiserror:: Error ) ]
@@ -33,50 +37,95 @@ pub enum AuthenticateError {
33
37
UnknownEmail ,
34
38
#[ error( "invalid password" ) ]
35
39
InvalidPassword ,
36
- General ( #[ source]
37
- #[ from] GeneralError ) ,
40
+ #[ error( "authentication error" ) ]
41
+ General (
42
+ #[ source]
43
+ #[ from]
44
+ GeneralError ,
45
+ ) ,
38
46
}
39
47
40
48
#[ derive( Debug , thiserror:: Error ) ]
41
49
pub enum GeneralError {
42
- Sqlx ( #[ source]
43
- #[ from] sqlx:: Error ) ,
44
- PasswordHash ( #[ source] #[ from] argon2:: password_hash:: Error ) ,
45
- Task ( #[ source]
46
- #[ from] tokio:: task:: JoinError ) ,
50
+ #[ error( "database error" ) ]
51
+ Sqlx (
52
+ #[ source]
53
+ #[ from]
54
+ sqlx:: Error ,
55
+ ) ,
56
+ #[ error( "error hashing password" ) ]
57
+ PasswordHash (
58
+ #[ source]
59
+ #[ from]
60
+ argon2:: password_hash:: Error ,
61
+ ) ,
62
+ #[ error( "task panicked" ) ]
63
+ Task (
64
+ #[ source]
65
+ #[ from]
66
+ tokio:: task:: JoinError ,
67
+ ) ,
47
68
}
48
69
49
70
impl AccountsManager {
50
- pub async fn new ( conn : & mut PgConnection , max_hashing_threads : usize ) -> Result < Self , GeneralError > {
51
- sqlx:: migrate!( ) . run ( conn) . await ?;
71
+ pub async fn new (
72
+ conn : & mut PgConnection ,
73
+ max_hashing_threads : usize ,
74
+ ) -> Result < Self , GeneralError > {
75
+ sqlx:: migrate!( )
76
+ . run ( conn)
77
+ . await
78
+ . map_err ( sqlx:: Error :: from) ?;
52
79
53
- AccountsManager {
54
- hashing_semaphore : Semaphore :: new ( max_hashing_threads)
55
- }
80
+ Ok ( AccountsManager {
81
+ hashing_semaphore : Semaphore :: new ( max_hashing_threads) . into ( ) ,
82
+ } )
56
83
}
57
84
58
- async fn hash_password ( & self , password : String ) -> Result < PasswordHash , GeneralError > {
59
- let guard = self . hashing_semaphore . acquire ( ) . await
85
+ async fn hash_password ( & self , password : String ) -> Result < PasswordHashString , GeneralError > {
86
+ let guard = self
87
+ . hashing_semaphore
88
+ . clone ( )
89
+ . acquire_owned ( )
90
+ . await
60
91
. expect ( "BUG: this semaphore should not be closed" ) ;
61
92
62
93
// We transfer ownership to the blocking task and back to ensure Tokio doesn't spawn
63
94
// excess threads.
64
95
let ( _guard, res) = tokio:: task:: spawn_blocking ( move || {
65
96
let salt = argon2:: password_hash:: SaltString :: generate ( rand:: thread_rng ( ) ) ;
66
- ( guard, Argon2 :: default ( ) . hash_password ( password. as_bytes ( ) , & salt) )
97
+ (
98
+ guard,
99
+ Argon2 :: default ( )
100
+ . hash_password ( password. as_bytes ( ) , & salt)
101
+ . map ( |hash| hash. serialize ( ) ) ,
102
+ )
67
103
} )
68
- . await ?;
104
+ . await ?;
69
105
70
106
Ok ( res?)
71
107
}
72
108
73
- async fn verify_password ( & self , password : String , hash : PasswordHashString ) -> Result < ( ) , AuthenticateError > {
74
- let guard = self . hashing_semaphore . acquire ( ) . await
109
+ async fn verify_password (
110
+ & self ,
111
+ password : String ,
112
+ hash : PasswordHashString ,
113
+ ) -> Result < ( ) , AuthenticateError > {
114
+ let guard = self
115
+ . hashing_semaphore
116
+ . clone ( )
117
+ . acquire_owned ( )
118
+ . await
75
119
. expect ( "BUG: this semaphore should not be closed" ) ;
76
120
77
121
let ( _guard, res) = tokio:: task:: spawn_blocking ( move || {
78
- ( guard, Argon2 :: default ( ) . verify_password ( password. as_bytes ( ) , & hash. password_hash ( ) ) )
79
- } ) . await . map_err ( GeneralError :: from) ?;
122
+ (
123
+ guard,
124
+ Argon2 :: default ( ) . verify_password ( password. as_bytes ( ) , & hash. password_hash ( ) ) ,
125
+ )
126
+ } )
127
+ . await
128
+ . map_err ( GeneralError :: from) ?;
80
129
81
130
if let Err ( password_hash:: Error :: Password ) = res {
82
131
return Err ( AuthenticateError :: InvalidPassword ) ;
@@ -87,46 +136,64 @@ impl AccountsManager {
87
136
Ok ( ( ) )
88
137
}
89
138
90
- pub async fn create ( & self , txn : & mut PgTransaction , email : & str , password : String ) -> Result < AccountId , CreateError > {
139
+ pub async fn create (
140
+ & self ,
141
+ txn : & mut PgTransaction < ' _ > ,
142
+ email : & str ,
143
+ password : String ,
144
+ ) -> Result < AccountId , CreateError > {
91
145
// Hash password whether the account exists or not to make it harder
92
146
// to tell the difference in the timing.
93
147
let hash = self . hash_password ( password) . await ?;
94
148
149
+ // Thanks to `sqlx.toml`, `account_id` maps to `AccountId`
95
150
// language=PostgreSQL
96
- sqlx:: query !(
151
+ sqlx:: query_scalar !(
97
152
"insert into accounts.account(email, password_hash) \
98
153
values ($1, $2) \
99
154
returning account_id",
100
155
email,
101
- Text ( hash) as Text < PasswordHash < ' static >> ,
156
+ hash. as_str ( ) ,
102
157
)
103
- . fetch_one ( & mut * txn)
104
- . await
105
- . map_err ( |e| if e. constraint ( ) == Some ( "account_account_id_key" ) {
158
+ . fetch_one ( & mut * * txn)
159
+ . await
160
+ . map_err ( |e| {
161
+ if e. as_database_error ( ) . and_then ( |dbe| dbe. constraint ( ) ) == Some ( "account_account_id_key" ) {
106
162
CreateError :: EmailInUse
107
163
} else {
108
164
GeneralError :: from ( e) . into ( )
109
- } )
165
+ }
166
+ } )
110
167
}
111
168
112
- pub async fn authenticate ( & self , conn : & mut PgConnection , email : & str , password : String ) -> Result < AccountId , AuthenticateError > {
169
+ pub async fn authenticate (
170
+ & self ,
171
+ conn : & mut PgConnection ,
172
+ email : & str ,
173
+ password : String ,
174
+ ) -> Result < AccountId , AuthenticateError > {
175
+ // Thanks to `sqlx.toml`:
176
+ // * `account_id` maps to `AccountId`
177
+ // * `password_hash` maps to `Text<PasswordHashString>`
113
178
let maybe_account = sqlx:: query!(
114
- "select account_id, password_hash as \" password_hash: Text<PasswordHashString> \" \
179
+ "select account_id, password_hash \
115
180
from accounts.account \
116
- where email_id = $1",
181
+ where email = $1",
117
182
email
118
183
)
119
- . fetch_optional ( & mut * conn)
120
- . await
121
- . map_err ( GeneralError :: from) ?;
184
+ . fetch_optional ( & mut * conn)
185
+ . await
186
+ . map_err ( GeneralError :: from) ?;
122
187
123
188
let Some ( account) = maybe_account else {
124
189
// Hash the password whether the account exists or not to hide the difference in timing.
125
- self . hash_password ( password) . await . map_err ( GeneralError :: from) ?;
190
+ self . hash_password ( password)
191
+ . await
192
+ . map_err ( GeneralError :: from) ?;
126
193
return Err ( AuthenticateError :: UnknownEmail ) ;
127
194
} ;
128
195
129
- self . verify_password ( password, account. password_hash . into ( ) ) ?;
196
+ self . verify_password ( password, account. password_hash . into_inner ( ) ) . await ?;
130
197
131
198
Ok ( account. account_id )
132
199
}
0 commit comments