Skip to content

Commit

Permalink
Merge pull request #3425 from eigerco/nijo/add-accounts-new-save-to-file
Browse files Browse the repository at this point in the history
Add support for account new --save-to-file
  • Loading branch information
zosorock authored Dec 11, 2024
2 parents 3e5d310 + 302cb55 commit 9ba3710
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 17 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ workspace = true
[dependencies.sys-info]
version = "0.9"

[dependencies.tempfile]
version = "3"

[dependencies.time]
version = "0.3"

Expand Down
188 changes: 180 additions & 8 deletions cli/src/commands/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@ use rand::SeedableRng;
use rand_chacha::ChaChaRng;
use rayon::prelude::*;
use std::{
fs::File,
io::{Read, Write},
path::PathBuf,
};

use zeroize::Zeroize;

/// Commands to manage Aleo accounts.
Expand All @@ -52,6 +54,9 @@ pub enum Account {
/// Print sensitive information (such as the private key) discreetly in an alternate screen
#[clap(long)]
discreet: bool,
/// Specify the path to a file where to save the account in addition to printing it
#[clap(long = "save-to-file")]
save_to_file: Option<String>,
},
Sign {
/// Specify the network of the private key to sign with
Expand Down Expand Up @@ -97,12 +102,20 @@ fn aleo_literal_to_fields<N: Network>(input: &str) -> Result<Vec<Field<N>>> {
impl Account {
pub fn parse(self) -> Result<String> {
match self {
Self::New { network, seed, vanity, discreet } => {
Self::New { network, seed, vanity, discreet, save_to_file } => {
// Ensure only the seed or the vanity string is specified.
if seed.is_some() && vanity.is_some() {
bail!("Cannot specify both the '--seed' and '--vanity' flags");
}

if save_to_file.is_some() && vanity.is_some() {
bail!("Cannot specify both the '--save-to-file' and '--vanity' flags");
}

if save_to_file.is_some() && discreet {
bail!("Cannot specify both the '--save-to-file' and '--discreet' flags");
}

match vanity {
// Generate a vanity account for the specified network.
Some(vanity) => match network {
Expand All @@ -113,9 +126,9 @@ impl Account {
},
// Generate a seeded account for the specified network.
None => match network {
MainnetV0::ID => Self::new_seeded::<MainnetV0>(seed, discreet),
TestnetV0::ID => Self::new_seeded::<TestnetV0>(seed, discreet),
CanaryV0::ID => Self::new_seeded::<CanaryV0>(seed, discreet),
MainnetV0::ID => Self::new_seeded::<MainnetV0>(seed, discreet, save_to_file),
TestnetV0::ID => Self::new_seeded::<TestnetV0>(seed, discreet, save_to_file),
CanaryV0::ID => Self::new_seeded::<CanaryV0>(seed, discreet, save_to_file),
unknown_id => bail!("Unknown network ID ({unknown_id})"),
},
}
Expand Down Expand Up @@ -227,7 +240,7 @@ impl Account {
}

/// Generates a new Aleo account with an optional seed.
fn new_seeded<N: Network>(seed: Option<String>, discreet: bool) -> Result<String> {
fn new_seeded<N: Network>(seed: Option<String>, discreet: bool, save_to_file: Option<String>) -> Result<String> {
// Recover the seed.
let seed = match seed {
// Recover the field element deterministically.
Expand All @@ -242,6 +255,13 @@ impl Account {
PrivateKey::try_from(seed).map_err(|_| anyhow!("Failed to convert the seed into a valid private key"))?;
// Construct the account.
let account = snarkos_account::Account::<N>::try_from(private_key)?;
// Save to file in addition to printing it back to the user
if let Some(path) = save_to_file {
crate::check_parent_permissions(&path)?;
let mut file = File::create_new(path)?;
file.write_all(account.private_key().to_string().as_bytes())?;
crate::set_user_read_only(&file)?;
}
// Print the new Aleo account.
if !discreet {
return Ok(account.to_string());
Expand Down Expand Up @@ -331,13 +351,15 @@ fn wait_for_keypress() {
#[cfg(test)]
mod tests {
use crate::commands::Account;
use std::{fs, fs::Permissions, io::Write};
use tempfile::{NamedTempFile, TempDir};

use colored::Colorize;

#[test]
fn test_new() {
for _ in 0..3 {
let account = Account::New { network: 0, seed: None, vanity: None, discreet: false };
let account = Account::New { network: 0, seed: None, vanity: None, discreet: false, save_to_file: None };
assert!(account.parse().is_ok());
}
}
Expand All @@ -363,7 +385,7 @@ mod tests {
);

let vanity = None;
let account = Account::New { network: 0, seed, vanity, discreet: false };
let account = Account::New { network: 0, seed, vanity, discreet: false, save_to_file: None };
let actual = account.parse().unwrap();
assert_eq!(expected, actual);
}
Expand All @@ -389,11 +411,123 @@ mod tests {
);

let vanity = None;
let account = Account::New { network: 0, seed, vanity, discreet: false };
let account = Account::New { network: 0, seed, vanity, discreet: false, save_to_file: None };
let actual = account.parse().unwrap();
assert_eq!(expected, actual);
}

#[cfg(unix)]
#[test]
fn test_new_save_to_file() {
use std::os::unix::fs::PermissionsExt;

let dir = TempDir::new().expect("Failed to create temp folder");
let dir_path = dir.path();
fs::set_permissions(dir_path, Permissions::from_mode(0o700)).expect("Failed to set permissions");

let mut file = dir.path().to_owned();
file.push("my-private-key-file");
let file = file.display().to_string();

let seed = Some(1231275789u64.to_string());
let vanity = None;
let discreet = false;
let save_to_file = Some(file.clone());
let account = Account::New { network: 0, seed, vanity, discreet, save_to_file };
let actual = account.parse().unwrap();

let expected = "APrivateKey1zkp2n22c19hNdGF8wuEoQcuiyuWbquY6up4CtG5DYKqPX2X";
assert!(actual.contains(expected));

let content = fs::read_to_string(&file).expect("Failed to read private-key-file");
assert_eq!(expected, content);

// check the permissions - to read-only for the owner
let metadata = fs::metadata(file).unwrap();
let permissions = metadata.permissions();
assert_eq!(permissions.mode() & 0o777, 0o400, "File permissions are not 0o400");
}

#[cfg(unix)]
#[test]
fn test_new_prevent_save_to_file_in_non_protected_folder() {
use std::os::unix::fs::PermissionsExt;

let dir = TempDir::new().expect("Failed to create temp folder");
let dir_path = dir.path();
fs::set_permissions(dir_path, Permissions::from_mode(0o444)).expect("Failed to set permissions");

let mut file = dir.path().to_owned();
file.push("my-private-key-file");
let file = file.display().to_string();

let seed = None;
let vanity = None;
let discreet = false;
let save_to_file = Some(file);
let account = Account::New { network: 0, seed, vanity, discreet, save_to_file };
let res = account.parse();
assert!(res.is_err());
}

#[test]
fn test_new_prevent_save_to_file_in_non_existing_folder() {
let dir = TempDir::new().expect("Failed to create temp folder");

let mut file = dir.path().to_owned();
file.push("missing-folder");
file.push("my-private-key-file");
let file = file.display().to_string();

let seed = None;
let vanity = None;
let discreet = false;
let save_to_file = Some(file);
let account = Account::New { network: 0, seed, vanity, discreet, save_to_file };
let res = account.parse();
assert!(res.is_err());
}

#[test]
fn test_new_prevent_overwrite_existing_file() {
let mut file = NamedTempFile::new().expect("Failed to create temp file");
write!(file, "don't overwrite me").expect("Failed to write secret to file");

let seed = None;
let vanity = None;
let discreet = false;
let path = file.path().display().to_string();
let account = Account::New { network: 0, seed, vanity, discreet, save_to_file: Some(path) };
let res = account.parse();
assert!(res.is_err());

let expected = "don't overwrite me";
let content = fs::read_to_string(file).expect("Failed to read private-key-file");
assert_eq!(expected, content);
}

#[test]
fn test_new_disallow_save_to_file_with_discreet() {
let seed = None;
let vanity = None;
let discreet = true;
let save_to_file = Some("/tmp/not-important".to_string());
let account = Account::New { network: 0, seed, vanity, discreet, save_to_file };
let res = account.parse();
assert!(res.is_err());
}

#[test]
fn test_new_disallow_save_to_file_with_vanity() {
let seed = None;
let vanity = Some("foo".to_string());
let discreet = false;
let save_to_file = Some("/tmp/not-important".to_string());
let account = Account::New { network: 0, seed, vanity, discreet, save_to_file };
let res = account.parse();
assert!(res.is_err());
}

#[test]
fn test_signature_raw() {
let key = "APrivateKey1zkp61PAYmrYEKLtRWeWhUoDpFnGLNuHrCciSqN49T86dw3p".to_string();
Expand All @@ -402,6 +536,44 @@ mod tests {
assert!(account.parse().is_ok());
}

#[test]
fn test_signature_raw_using_private_key_file() {
let key = "APrivateKey1zkp61PAYmrYEKLtRWeWhUoDpFnGLNuHrCciSqN49T86dw3p".to_string();
let message = "Hello, world!".to_string();

let mut file = NamedTempFile::new().expect("Failed to create temp file");
writeln!(file, "{}", key).expect("Failed to write key to temp file");

let path = file.path().display().to_string();
let account = Account::Sign { network: 0, private_key: None, private_key_file: Some(path), message, raw: true };
assert!(account.parse().is_ok());
}

#[cfg(unix)]
#[test]
fn test_signature_raw_using_private_key_file_from_account_new() {
use std::os::unix::fs::PermissionsExt;

let message = "Hello, world!".to_string();

let dir = TempDir::new().expect("Failed to create temp folder");
let dir_path = dir.path();
fs::set_permissions(dir_path, Permissions::from_mode(0o700)).expect("Failed to set permissions");

let mut file = dir.path().to_owned();
file.push("my-private-key-file");
let file = file.display().to_string();

let seed = None;
let vanity = None;
let discreet = false;
let account = Account::New { network: 0, seed, vanity, discreet, save_to_file: Some(file.clone()) };
assert!(account.parse().is_ok());

let account = Account::Sign { network: 0, private_key: None, private_key_file: Some(file), message, raw: true };
assert!(account.parse().is_ok());
}

#[test]
fn test_signature() {
let key = "APrivateKey1zkp61PAYmrYEKLtRWeWhUoDpFnGLNuHrCciSqN49T86dw3p".to_string();
Expand Down
10 changes: 1 addition & 9 deletions cli/src/commands/start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -651,15 +651,7 @@ fn check_permissions(path: &PathBuf) -> Result<(), snarkvm::prelude::Error> {
{
use std::os::unix::fs::PermissionsExt;
ensure!(path.exists(), "The file '{:?}' does not exist", path);
let parent = path.parent();
if let Some(parent) = parent {
let parent_permissions = parent.metadata()?.permissions().mode();
ensure!(
parent_permissions & 0o777 == 0o700,
"The folder {:?} must be readable only by the owner (0700)",
parent
);
}
crate::check_parent_permissions(path)?;
let permissions = path.metadata()?.permissions().mode();
ensure!(permissions & 0o777 == 0o600, "The file {:?} must be readable only by the owner (0600)", path);
}
Expand Down
44 changes: 44 additions & 0 deletions cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,47 @@ extern crate thiserror;

pub mod commands;
pub mod helpers;

use anyhow::Result;
use std::{
fs::{File, Permissions},
path::Path,
};

#[cfg(unix)]
pub fn check_parent_permissions<T: AsRef<Path>>(path: T) -> Result<()> {
use anyhow::{bail, ensure};
use std::os::unix::fs::PermissionsExt;

if let Some(parent) = path.as_ref().parent() {
let permissions = parent.metadata()?.permissions().mode();
ensure!(permissions & 0o777 == 0o700, "The folder {:?} must be readable only by the owner (0700)", parent);
} else {
let path = path.as_ref();
bail!("Parent does not exist for path={}", path.display());
}

Ok(())
}

#[cfg(windows)]
pub fn check_parent_permissions<T: AsRef<Path>>(_path: T) -> Result<()> {
Ok(())
}

#[cfg(unix)]
fn set_user_read_only(file: &File) -> Result<()> {
use std::os::unix::fs::PermissionsExt;

let permissions = Permissions::from_mode(0o400);
file.set_permissions(permissions)?;
Ok(())
}

#[cfg(windows)]
fn set_user_read_only(file: &File) -> Result<()> {
let mut permissions = file.metadata()?.permissions();
permissions.set_readonly(true);
file.set_permissions(permissions)?;
Ok(())
}

0 comments on commit 9ba3710

Please sign in to comment.