Skip to content

Commit 2fc965a

Browse files
authored
Merge pull request #7 from stevenlei/restore-key
Add Key Restoration and Timestamp Commands
2 parents 02499fb + 879f997 commit 2fc965a

1 file changed

Lines changed: 157 additions & 4 deletions

File tree

bpb/src/main.rs

Lines changed: 157 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,11 @@ mod keychain;
99
mod legacy_config;
1010
mod tests;
1111

12-
use std::time::SystemTime;
13-
1412
use ed25519_dalek as ed25519;
1513
use failure::Error;
1614
use keychain::{add_keychain_item, get_keychain_item};
1715
use rand::RngCore;
16+
use std::time::SystemTime;
1817

1918
use crate::config::Config;
2019
use crate::key_data::KeyData;
@@ -33,6 +32,44 @@ fn main() -> Result<(), Error> {
3332
Some("import") => import(),
3433
Some("upgrade") => upgrade(),
3534
Some("print") => print_public_key(),
35+
Some("timestamp") => print_timestamp(),
36+
Some("restore") => {
37+
// Check for force flag
38+
let mut force = false;
39+
let mut remaining_args = Vec::new();
40+
41+
for arg in args {
42+
if arg == "-f" || arg == "--force" {
43+
force = true;
44+
} else {
45+
remaining_args.push(arg);
46+
}
47+
}
48+
49+
let mut remaining_iter = remaining_args.into_iter();
50+
51+
if let Some(private_key) = remaining_iter.next() {
52+
if let Some(user_id) = remaining_iter.next() {
53+
let timestamp_str = remaining_iter.next();
54+
let timestamp = if let Some(ts_str) = timestamp_str {
55+
match ts_str.parse::<u64>() {
56+
Ok(ts) => Some(ts),
57+
Err(_) => {
58+
eprintln!("Warning: Invalid timestamp format. Using current time instead.");
59+
None
60+
}
61+
}
62+
} else {
63+
None
64+
};
65+
restore_from_private_key(private_key, user_id, timestamp, force)
66+
} else {
67+
bail!("Must specify both a 64-character private key AND a user ID, e.g.: `bpb restore [-f] YOUR_PRIVATE_KEY \"Name <email@example.com>\" [TIMESTAMP]`")
68+
}
69+
} else {
70+
bail!("Must specify a 64-character private key and a user ID, e.g.: `bpb restore [-f] YOUR_PRIVATE_KEY \"Name <email@example.com>\" [TIMESTAMP]`")
71+
}
72+
},
3673
Some("fingerprint") => print_fingerprint(),
3774
Some("key-id") => print_key_id(),
3875
Some("sign-hex") => {
@@ -68,6 +105,8 @@ fn print_help_message() -> Result<(), Error> {
68105
println!(" fingerprint: Print the fingerprint of the public key.");
69106
println!(" key-id: Print the key ID of the public key.");
70107
println!(" sign-hex <hex>: Sign a hex string and print the signature and public key.\n");
108+
println!(" timestamp: Print the timestamp of the current key.");
109+
println!(" restore [-f] <key> <userid> [timestamp]: Restore a key from a 64-character private key.\n The -f flag will override any existing key.\n The timestamp is optional and will be used to generate the same public key format.");
71110
println!("See https://github.com/pkgxdev/bpb for more information.");
72111
Ok(())
73112
}
@@ -200,8 +239,11 @@ fn import() -> Result<(), Error> {
200239
let service = config.service();
201240
let account = config.user_id();
202241

203-
let key = std::env::args().nth(2).unwrap();
204-
add_keychain_item(service, account, &key)
242+
if let Some(key) = std::env::args().nth(2) {
243+
add_keychain_item(service, account, &key)
244+
} else {
245+
bail!("Must specify a key to import, e.g.: `bpb import YOUR_PRIVATE_KEY`")
246+
}
205247
}
206248

207249
fn legacy_keys_file() -> String {
@@ -217,6 +259,117 @@ fn to_32_bytes(slice: &String) -> Result<[u8; 32], Error> {
217259
Ok(array)
218260
}
219261

262+
fn restore_from_private_key(private_key: String, user_id: String, timestamp_opt: Option<u64>, force: bool) -> Result<(), Error> {
263+
// Check for existing configuration and handle force flag
264+
let existing_config = Config::load().ok();
265+
266+
if let Some(config) = &existing_config {
267+
if !force {
268+
let config_path = config::keys_file();
269+
eprintln!(
270+
"A keypair already exists. Use -f flag to override or manually perform these steps:\n\n1. Run `security delete-generic-password -s {}`\n2. Delete the config file at `{}`",
271+
config.service(),
272+
config_path.display()
273+
);
274+
return Ok(());
275+
} else {
276+
// Force flag is set, we'll remove existing config and keychain entry
277+
println!("Force flag set: overriding existing key");
278+
279+
// 1. Remove keychain entry
280+
let service = config.service();
281+
let account = config.user_id();
282+
println!("Removing existing keychain entry for service: {}, account: {}", service, account);
283+
284+
let _ = std::process::Command::new("security")
285+
.args(["delete-generic-password", "-s", service])
286+
.output();
287+
288+
// 2. Delete config file
289+
let config_path = config::keys_file();
290+
if config_path.exists() {
291+
println!("Removing existing config file: {}", config_path.display());
292+
let _ = std::fs::remove_file(config_path);
293+
}
294+
}
295+
}
296+
297+
// Trim whitespace and newlines from the private key
298+
let private_key = private_key.trim();
299+
300+
// Validate the private key format
301+
if private_key.len() != 64 {
302+
bail!("Invalid private key length: expected 64 characters, got {}", private_key.len());
303+
}
304+
305+
// Try to decode the hex string to get the private key bytes
306+
let secret_bytes = match hex::decode(private_key) {
307+
Ok(bytes) => {
308+
if bytes.len() != 32 {
309+
bail!("Invalid private key decoded length: expected 32 bytes, got {}", bytes.len());
310+
}
311+
bytes
312+
}
313+
Err(_) => bail!("Failed to decode private key. It should be a valid hex string.")
314+
};
315+
316+
// Convert to 32-byte array
317+
let mut secret = [0u8; 32];
318+
secret.copy_from_slice(&secret_bytes);
319+
320+
// Create keypair from private key
321+
let keypair = ed25519::SigningKey::from_bytes(&secret);
322+
323+
// Get or use provided timestamp
324+
let timestamp = if let Some(ts) = timestamp_opt {
325+
println!("Using provided timestamp: {}", ts);
326+
ts
327+
} else {
328+
let current_ts = SystemTime::now()
329+
.duration_since(SystemTime::UNIX_EPOCH)?
330+
.as_secs();
331+
println!("Using current timestamp: {}", current_ts);
332+
current_ts
333+
};
334+
335+
// Validate user ID
336+
if user_id.is_empty() {
337+
bail!("User ID cannot be empty");
338+
}
339+
340+
// Get public key from keypair
341+
let public_key = hex::encode(keypair.verifying_key().as_bytes());
342+
343+
// Create and save config
344+
let config = Config::create(public_key, user_id, timestamp)?;
345+
config.write()?;
346+
347+
// Store private key in keychain
348+
let service = config.service();
349+
let account = config.user_id();
350+
let hex = hex::encode(keypair.to_bytes());
351+
add_keychain_item(service, account, &hex)?;
352+
353+
// Print the public key
354+
let keydata = KeyData::load(&config, keypair.to_bytes())?;
355+
println!("Key has been successfully restored.");
356+
println!("{}", keydata.public());
357+
358+
Ok(())
359+
}
360+
361+
fn print_timestamp() -> Result<(), Error> {
362+
// Load the configuration file
363+
let config = Config::load()?;
364+
365+
// Get the timestamp
366+
let timestamp = config.timestamp();
367+
368+
println!("{}", timestamp);
369+
370+
Ok(())
371+
}
372+
220373
// iterates over a hex array and prints space-separated groups of four characters
221374
fn pretty_print_hex_string(hex: &[u8]) -> String {
222375
hex.chunks(2)

0 commit comments

Comments
 (0)