11use crate :: config:: { AliasConfig , Config , ConfigError } ;
2+ use base64:: Engine ;
3+ use base64:: engine:: general_purpose:: STANDARD as BASE64 ;
24use clap:: Subcommand ;
35use colored:: Colorize ;
6+ use pbkdf2:: hmac:: Hmac ;
7+ use rand:: RngCore ;
8+ use sha2:: Sha256 ;
49use sqlx:: postgres:: PgPoolOptions ;
510use sqlx:: { PgPool , Row } ;
611
@@ -23,6 +28,12 @@ pub enum UsersError {
2328
2429 #[ error( "User '{0}' already exists" ) ]
2530 UserExists ( String ) ,
31+
32+ #[ error( "Passwords do not match" ) ]
33+ PasswordMismatch ,
34+
35+ #[ error( "Password error: {0}" ) ]
36+ Password ( String ) ,
2637}
2738
2839#[ derive( Subcommand ) ]
@@ -41,14 +52,19 @@ pub enum UsersCommand {
4152 /// Create a new user (bootstrap mode - no user required)
4253 #[ command( after_help = "Examples:\n \
4354 ow local users create max\n \
44- ow local users create max --system") ]
55+ ow local users create max --system\n \
56+ ow local users create max --password") ]
4557 Create {
4658 /// Username for the new user
4759 username : String ,
4860
4961 /// Claim the system user (rename __system__ to this username)
5062 #[ arg( long) ]
5163 system : bool ,
64+
65+ /// Set a password for the user (prompts interactively)
66+ #[ arg( long) ]
67+ password : bool ,
5268 } ,
5369
5470 /// Delete a user
@@ -70,7 +86,11 @@ impl UsersCommand {
7086 match self {
7187 Self :: List => cmd_list ( & pool) . await ,
7288 Self :: Get { username } => cmd_get ( & pool, & username) . await ,
73- Self :: Create { username, system } => cmd_create ( & pool, username, system) . await ,
89+ Self :: Create {
90+ username,
91+ system,
92+ password,
93+ } => cmd_create ( & pool, username, system, password) . await ,
7494 Self :: Delete { username } => cmd_delete ( & pool, & username) . await ,
7595 }
7696 }
@@ -171,7 +191,53 @@ async fn cmd_get(pool: &PgPool, username: &str) -> Result<(), UsersError> {
171191 Ok ( ( ) )
172192}
173193
174- async fn cmd_create ( pool : & PgPool , username : String , system : bool ) -> Result < ( ) , UsersError > {
194+ fn hash_password ( password : & str ) -> String {
195+ const ITERATIONS : u32 = 100_000 ;
196+ const SALT_LEN : usize = 16 ;
197+ const KEY_LEN : usize = 32 ;
198+
199+ let mut salt = [ 0u8 ; SALT_LEN ] ;
200+ rand:: rng ( ) . fill_bytes ( & mut salt) ;
201+
202+ let mut hash = [ 0u8 ; KEY_LEN ] ;
203+ pbkdf2:: pbkdf2 :: < Hmac < Sha256 > > ( password. as_bytes ( ) , & salt, ITERATIONS , & mut hash)
204+ . expect ( "HMAC can be initialized with any key length" ) ;
205+
206+ format ! (
207+ "{}${}${}" ,
208+ ITERATIONS ,
209+ BASE64 . encode( salt) ,
210+ BASE64 . encode( hash)
211+ )
212+ }
213+
214+ fn prompt_password ( ) -> Result < String , UsersError > {
215+ let password = rpassword:: prompt_password ( "Password: " )
216+ . map_err ( |e| UsersError :: Password ( e. to_string ( ) ) ) ?;
217+
218+ let confirm =
219+ rpassword:: prompt_password ( "Confirm: " ) . map_err ( |e| UsersError :: Password ( e. to_string ( ) ) ) ?;
220+
221+ if password != confirm {
222+ return Err ( UsersError :: PasswordMismatch ) ;
223+ }
224+
225+ Ok ( password)
226+ }
227+
228+ async fn cmd_create (
229+ pool : & PgPool ,
230+ username : String ,
231+ system : bool ,
232+ password : bool ,
233+ ) -> Result < ( ) , UsersError > {
234+ let password_hash = if password {
235+ let pw = prompt_password ( ) ?;
236+ Some ( hash_password ( & pw) )
237+ } else {
238+ None
239+ } ;
240+
175241 if system {
176242 // Rename the system user to claim it
177243 let result = sqlx:: query (
@@ -224,6 +290,16 @@ async fn cmd_create(pool: &PgPool, username: String, system: bool) -> Result<(),
224290 ) ;
225291 }
226292
293+ if let Some ( hash) = password_hash {
294+ sqlx:: query ( "UPDATE users SET password_hash = $1 WHERE username = $2" )
295+ . bind ( & hash)
296+ . bind ( & username)
297+ . execute ( pool)
298+ . await ?;
299+
300+ println ! ( "{} Password set." , "Password" . green( ) . bold( ) ) ;
301+ }
302+
227303 println ! ( "\n {} Set this user as default with:" , "Next:" . cyan( ) . bold( ) ) ;
228304 println ! (
229305 " {}" ,
0 commit comments