Commit 1a0408dd authored by SamwiseFilmore's avatar SamwiseFilmore
Browse files

pkgar-keys: Update seckey + refactor passwd; Docs and cleanup

- SecKey took a lower-level approach for its 0.11 API, so I refactored
  passwords to include a new type to make sure everything gets zeroed.
- Bunch of documentation work, I'm happy with the state of the rustdoc
  and error handling, at least enough to release 1.0 soon.
parent faaf1c88
......@@ -9,7 +9,7 @@ clap = "2.33.1"
dirs = "3.0.1"
hex = { version = "0.4.2", features = ["serde"] }
lazy_static = "1.4.0"
seckey = "0.9.3"
seckey = "0.11.2"
serde = { version = "1.0.114", default_features = false, features = ["derive"] }
sodiumoxide = { version = "0.2.5", default_features = false }
termion = "1.5.5"
......
......@@ -4,6 +4,7 @@ use std::path::PathBuf;
use user_error::UFE;
use thiserror::Error;
/// An error which includes path context and implements `UFE` for easy display.
#[derive(Debug, Error)]
#[error("File: {path}")]
pub struct Error {
......@@ -14,6 +15,9 @@ pub struct Error {
impl UFE for Error {}
/// The main error type that is used by this library internally. For additional
/// contextual information, most public routines use [`Error`](struct.Error.html).
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum ErrorKind {
#[error("Io")]
......@@ -25,9 +29,6 @@ pub enum ErrorKind {
#[error("Public and secret keys do not match")]
KeyMismatch,
#[error("Unable to allocate locked/zeroed memory")]
SecureMem,
#[error("Invalid nonce length")]
NonceInvalid,
......
......@@ -8,7 +8,7 @@ use std::path::{Path, PathBuf};
use hex::FromHex;
use lazy_static::lazy_static;
use seckey::SecKey;
use seckey::SecBytes;
use serde::{Deserialize, Serialize};
use sodiumoxide::crypto::{
pwhash,
......@@ -24,9 +24,19 @@ lazy_static! {
dirs::home_dir()
.unwrap_or("./".into())
};
/// The default location for pkgar to look for the user's public key.
///
/// Defaults to `$HOME/.pkgar/keys/id_ed25519.pub.toml`. If `$HOME` is
/// unset, `./.pkgar/keys/id_ed25519.pub.toml`.
pub static ref DEFAULT_PUBKEY: PathBuf = {
Path::join(&HOMEDIR, ".pkgar/keys/id_ed25519.pub.toml")
};
/// The default location for pkgar to look for the user's secret key.
///
/// Defaults to `$HOME/.pkgar/keys/id_ed25519.toml`. If `$HOME` is unset,
/// `./.pkgar/keys/id_ed25519.toml`.
pub static ref DEFAULT_SECKEY: PathBuf = {
Path::join(&HOMEDIR, ".pkgar/keys/id_ed25519.toml")
};
......@@ -71,7 +81,7 @@ pub struct PublicKeyFile {
}
impl PublicKeyFile {
/// Helper function to deserialize.
/// Parse a `PublicKeyFile` from `file` (in toml format).
pub fn open(file: &Path) -> Result<PublicKeyFile, Error> {
let content = fs::read_to_string(file)
.map_err(|src| Error {
......@@ -86,13 +96,13 @@ impl PublicKeyFile {
})
}
/// Write `self` serialized as toml to `w`
/// Write `self` serialized as toml to `w`.
pub fn write(&self, mut w: impl Write) -> Result<(), ErrorKind> {
w.write_all(toml::to_string(self)?.as_bytes())?;
Ok(())
}
/// Shortcut function to write serialized to `file`
/// Shortcut to write the public key to `file`
pub fn save(&self, file: &Path) -> Result<(), Error> {
self.write(
File::create(file)
......@@ -113,9 +123,9 @@ enum SKey {
}
impl SKey {
fn encrypt(&mut self, passwd: SecKey<str>, salt: pwhash::Salt, nonce: secretbox::Nonce) {
fn encrypt(&mut self, passwd: Passwd, salt: pwhash::Salt, nonce: secretbox::Nonce) {
if let SKey::Plain(skey) = self {
if let Some(passwd_key) = gen_key(passwd, salt) {
if let Some(passwd_key) = passwd.gen_key(salt) {
let mut buf = [0; 80];
buf.copy_from_slice(&secretbox::seal(skey.as_ref(), &nonce, &passwd_key));
*self = SKey::Cipher(buf);
......@@ -123,9 +133,9 @@ impl SKey {
}
}
fn decrypt(&mut self, passwd: SecKey<str>, salt: pwhash::Salt, nonce: secretbox::Nonce) -> Result<(), ErrorKind> {
fn decrypt(&mut self, passwd: Passwd, salt: pwhash::Salt, nonce: secretbox::Nonce) -> Result<(), ErrorKind> {
if let SKey::Cipher(ciphertext) = self {
if let Some(passwd_key) = gen_key(passwd, salt) {
if let Some(passwd_key) = passwd.gen_key(salt) {
let skey_plain = secretbox::open(ciphertext.as_ref(), &nonce, &passwd_key)
.map_err(|_| ErrorKind::PassphraseIncorrect )?;
......@@ -189,8 +199,8 @@ pub struct SecretKeyFile {
}
impl SecretKeyFile {
/// Generate a keypair with all the nessesary info to save both
/// keys. You must call `save()` on each object to persist to disk.
/// Generate a keypair with all the nessesary info to save both keys. You
/// must call `save()` on each object to persist them to disk.
pub fn new() -> (PublicKeyFile, SecretKeyFile) {
let (pkey, skey) = sign::gen_keypair();
......@@ -204,7 +214,7 @@ impl SecretKeyFile {
(pkey_file, skey_file)
}
/// Parse a SecretKeyFile from `file`.
/// Parse a `SecretKeyFile` from `file` (in toml format).
pub fn open(file: &Path) -> Result<SecretKeyFile, Error> {
let content = fs::read_to_string(file)
.map_err(|src| Error {
......@@ -219,13 +229,14 @@ impl SecretKeyFile {
})
}
/// Write `self` serialized as toml to `w`
/// Write `self` serialized as toml to `w`.
pub fn write(&self, mut w: impl Write) -> Result<(), ErrorKind> {
w.write_all(toml::to_string(&self)?.as_bytes())?;
Ok(())
}
/// Shortcut to write the secret key to `file`.
///
/// Make sure to call `encrypt()` in order to encrypt
/// the private key, otherwise it will be stored as plain text.
pub fn save(&self, file: &Path) -> Result<(), Error> {
......@@ -247,13 +258,13 @@ impl SecretKeyFile {
/// Ensure that the internal state of this struct is encrypted.
/// Note that if passwd is empty, this function is a no-op.
pub fn encrypt(&mut self, passwd: SecKey<str>) {
pub fn encrypt(&mut self, passwd: Passwd) {
self.skey.encrypt(passwd, self.salt, self.nonce)
}
/// Ensure that the internal state of this struct is decrypted.
/// If the internal state is already decrypted, this function is a no-op.
pub fn decrypt(&mut self, passwd: SecKey<str>) -> Result<(), ErrorKind> {
pub fn decrypt(&mut self, passwd: Passwd) -> Result<(), ErrorKind> {
self.skey.decrypt(passwd, self.salt, self.nonce)
}
......@@ -281,61 +292,96 @@ impl SecretKeyFile {
}
}
/// Get a key for symmetric key encryption from a password.
fn gen_key(passwd: SecKey<str>, salt: pwhash::Salt) -> Option<secretbox::Key> {
if passwd.read().deref() == "" {
None
} else {
let mut key = secretbox::Key([0; secretbox::KEYBYTES]);
let secretbox::Key(ref mut binary_key) = key;
pwhash::derive_key(binary_key, passwd.read().as_bytes(), &salt,
pwhash::OPSLIMIT_INTERACTIVE,
pwhash::MEMLIMIT_INTERACTIVE)
.expect("Failed to get key from password");
Some(key)
}
/// Secure in-memory representation of a password.
pub struct Passwd {
bytes: SecBytes,
}
/// Prompt the user for a password on stdin.
fn get_passwd(prompt: &str) -> Result<SecKey<str>, ErrorKind> {
let stdout = stdout();
let mut stdout = stdout.lock();
let stdin = stdin();
let mut stdin = stdin.lock();
stdout.write_all(prompt.as_bytes())?;
stdout.flush()?;
impl Passwd {
/// Create a new `Passwd` and zero the old string.
pub fn new(passwd: &mut String) -> Passwd {
let pwd = Passwd {
bytes :SecBytes::with(
passwd.len(),
|buf| buf.copy_from_slice(passwd.as_bytes())
),
};
unsafe {
seckey::zero(passwd.as_bytes_mut());
}
pwd
}
let mut passwd = stdin.read_passwd(&mut stdout)?
.ok_or(ErrorKind::Io(io::Error::new(io::ErrorKind::UnexpectedEof, "Invalid Password Input")))?;
/// Prompt the user for a `Passwd` on stdin.
pub fn prompt(prompt: impl AsRef<str>) -> Result<Passwd, ErrorKind> {
let stdout = stdout();
let mut stdout = stdout.lock();
let stdin = stdin();
let mut stdin = stdin.lock();
stdout.write_all(prompt.as_ref().as_bytes())?;
stdout.flush()?;
let mut passwd = stdin.read_passwd(&mut stdout)?
.ok_or(ErrorKind::Io(
io::Error::new(
io::ErrorKind::UnexpectedEof,
"Invalid Password Input",
)
))?;
println!();
Ok(Passwd::new(&mut passwd))
}
let passwd = SecKey::from_str(&mut passwd)
.ok_or(ErrorKind::SecureMem)?;
/// Prompt for a password on stdin and confirm it. For configurable
/// prompts, use [`Passwd::prompt`](struct.Passwd.html#method.prompt).
pub fn prompt_new() -> Result<Passwd, ErrorKind> {
let passwd = Passwd::prompt(
"Please enter a new passphrase (leave empty to store the key in plaintext): "
)?;
let confirm = Passwd::prompt("Please re-enter the passphrase: ")?;
println!();
if passwd != confirm {
Err(ErrorKind::PassphraseMismatch)
} else {
Ok(passwd)
}
}
Ok(passwd)
/// Get a key for symmetric key encryption from a password.
fn gen_key(&self, salt: pwhash::Salt) -> Option<secretbox::Key> {
if self.bytes.read().len() > 0 {
let mut key = secretbox::Key([0; secretbox::KEYBYTES]);
let secretbox::Key(ref mut binary_key) = key;
pwhash::derive_key(
binary_key,
&self.bytes.read(),
&salt,
pwhash::OPSLIMIT_INTERACTIVE,
pwhash::MEMLIMIT_INTERACTIVE,
).expect("Failed to get key from password");
Some(key)
} else {
None
}
}
}
/// Prompt for a password and confirm it.
fn get_new_passwd() -> Result<SecKey<str>, ErrorKind> {
let passwd = get_passwd("Please enter a new passphrase (leave empty to store the key in plaintext): ")?;
let confirm = get_passwd("Please re-enter the passphrase: ")?;
if passwd.read().deref() != confirm.read().deref() {
Err(ErrorKind::PassphraseMismatch)
} else {
Ok(passwd)
impl PartialEq for Passwd {
fn eq(&self, other: &Passwd) -> bool {
self.bytes.read().deref() == other.bytes.read().deref()
}
}
impl Eq for Passwd {}
/// Generate a new keypair. The new keys will be saved to `file`. The user
/// will be prompted on stdin for a password, empty passwords will cause the
/// secret key to be stored in plain text. Note that parent
/// directories will not be created.
pub fn gen_keypair(pkey_path: &Path, skey_path: &Path) -> Result<(PublicKeyFile, SecretKeyFile), Error> {
let passwd = get_new_passwd()
let passwd = Passwd::prompt_new()
.map_err(|src| Error {
path: skey_path.to_path_buf(),
src,
......@@ -361,7 +407,7 @@ fn prompt_skey(skey_path: &Path, prompt: impl AsRef<str>) -> Result<SecretKeyFil
let mut key_file = SecretKeyFile::open(skey_path)?;
if key_file.is_encrypted() {
let passwd = get_passwd(&format!("{} {}: ", prompt.as_ref(), skey_path.display()))
let passwd = Passwd::prompt(&format!("{} {}: ", prompt.as_ref(), skey_path.display()))
.map_err(to_file_err)?;
key_file.decrypt(passwd)
.map_err(to_file_err)?;
......@@ -380,7 +426,7 @@ pub fn get_skey(skey_path: &Path) -> Result<SecretKeyFile, Error> {
pub fn re_encrypt(skey_path: &Path) -> Result<(), Error> {
let mut skey_file = prompt_skey(skey_path, "Old passphrase for")?;
let passwd = get_new_passwd()
let passwd = Passwd::prompt_new()
.map_err(|src| Error {
path: skey_path.to_path_buf(),
src,
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment