Files
lighthouse/common/account_utils/src/lib.rs
Paul Hauner 6ea3bc5e52 Implement VC API (#1657)
## Issue Addressed

NA

## Proposed Changes

- Implements a HTTP API for the validator client.
- Creates EIP-2335 keystores with an empty `description` field, instead of a missing `description` field. Adds option to set name.
- Be more graceful with setups without any validators (yet)
    - Remove an error log when there are no validators.
    - Create the `validator` dir if it doesn't exist.
- Allow building a `ValidatorDir` without a withdrawal keystore (required for the API method where we only post a voting keystore).
- Add optional `description` field to `validator_definitions.yml`

## TODO

- [x] Signature header, as per https://github.com/sigp/lighthouse/issues/1269#issuecomment-649879855
- [x] Return validator descriptions
- [x] Return deposit data
- [x] Respect the mnemonic offset
- [x] Check that mnemonic can derive returned keys
- [x] Be strict about non-localhost
- [x] Allow graceful start without any validators (+ create validator dir)
- [x] Docs final pass
- [x] Swap to EIP-2335 description field. 
- [x] Fix Zerioze TODO in VC api types.
- [x] Zeroize secp256k1 key

## Endpoints

- [x] `GET /lighthouse/version`
- [x] `GET /lighthouse/health`
- [x] `GET /lighthouse/validators` 
- [x] `POST /lighthouse/validators/hd`
- [x] `POST /lighthouse/validators/keystore`
- [x] `PATCH /lighthouse/validators/:validator_pubkey`
- [ ] ~~`POST /lighthouse/validators/:validator_pubkey/exit/:epoch`~~ Future works


## Additional Info

TBC
2020-10-02 09:42:19 +00:00

248 lines
7.1 KiB
Rust

//! Provides functions that are used for key/account management across multiple crates in the
//! Lighthouse project.
use eth2_keystore::Keystore;
use eth2_wallet::{
bip39::{Language, Mnemonic, MnemonicType},
Wallet,
};
use rand::{distributions::Alphanumeric, Rng};
use serde_derive::{Deserialize, Serialize};
use std::fs::{self, File};
use std::io;
use std::io::prelude::*;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use zeroize::Zeroize;
pub mod validator_definitions;
pub use eth2_keystore;
pub use eth2_wallet;
pub use eth2_wallet::PlainText;
/// The minimum number of characters required for a wallet password.
pub const MINIMUM_PASSWORD_LEN: usize = 12;
/// The `Alphanumeric` crate only generates a-z, A-Z, 0-9, therefore it has a range of 62
/// characters.
///
/// 62**48 is greater than 255**32, therefore this password has more bits of entropy than a byte
/// array of length 32.
const DEFAULT_PASSWORD_LEN: usize = 48;
/// Returns the "default" path where a wallet should store its password file.
pub fn default_wallet_password_path<P: AsRef<Path>>(wallet_name: &str, secrets_dir: P) -> PathBuf {
secrets_dir.as_ref().join(format!("{}.pass", wallet_name))
}
/// Returns a password for a wallet, where that password is loaded from the "default" path.
pub fn default_wallet_password<P: AsRef<Path>>(
wallet: &Wallet,
secrets_dir: P,
) -> Result<PlainText, io::Error> {
let path = default_wallet_password_path(wallet.name(), secrets_dir);
fs::read(path).map(|bytes| PlainText::from(strip_off_newlines(bytes)))
}
/// Returns the "default" path where a keystore should store its password file.
pub fn default_keystore_password_path<P: AsRef<Path>>(
keystore: &Keystore,
secrets_dir: P,
) -> PathBuf {
secrets_dir
.as_ref()
.join(format!("0x{}", keystore.pubkey()))
}
/// Reads a password file into a Zeroize-ing `PlainText` struct, with new-lines removed.
pub fn read_password<P: AsRef<Path>>(path: P) -> Result<PlainText, io::Error> {
fs::read(path).map(strip_off_newlines).map(Into::into)
}
/// Creates a file with `600 (-rw-------)` permissions.
pub fn create_with_600_perms<P: AsRef<Path>>(path: P, bytes: &[u8]) -> Result<(), io::Error> {
let path = path.as_ref();
let mut file = File::create(&path)?;
let mut perm = file.metadata()?.permissions();
perm.set_mode(0o600);
file.set_permissions(perm)?;
file.write_all(bytes)?;
Ok(())
}
/// Generates a random alphanumeric password of length `DEFAULT_PASSWORD_LEN`.
pub fn random_password() -> PlainText {
rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(DEFAULT_PASSWORD_LEN)
.collect::<String>()
.into_bytes()
.into()
}
/// Remove any number of newline or carriage returns from the end of a vector of bytes.
pub fn strip_off_newlines(mut bytes: Vec<u8>) -> Vec<u8> {
let mut strip_off = 0;
for (i, byte) in bytes.iter().rev().enumerate() {
if *byte == b'\n' || *byte == b'\r' {
strip_off = i + 1;
} else {
break;
}
}
bytes.truncate(bytes.len() - strip_off);
bytes
}
/// Reads a password from TTY or stdin if `use_stdin == true`.
pub fn read_password_from_user(use_stdin: bool) -> Result<ZeroizeString, String> {
let result = if use_stdin {
rpassword::prompt_password_stderr("")
.map_err(|e| format!("Error reading from stdin: {}", e))
} else {
rpassword::read_password_from_tty(None)
.map_err(|e| format!("Error reading from tty: {}", e))
};
result.map(ZeroizeString::from)
}
/// Reads a mnemonic phrase from TTY or stdin if `use_stdin == true`.
pub fn read_input_from_user(use_stdin: bool) -> Result<String, String> {
let mut input = String::new();
if use_stdin {
io::stdin()
.read_line(&mut input)
.map_err(|e| format!("Error reading from stdin: {}", e))?;
} else {
let tty = File::open("/dev/tty").map_err(|e| format!("Error opening tty: {}", e))?;
let mut buf_reader = io::BufReader::new(tty);
buf_reader
.read_line(&mut input)
.map_err(|e| format!("Error reading from tty: {}", e))?;
}
trim_newline(&mut input);
Ok(input)
}
fn trim_newline(s: &mut String) {
if s.ends_with('\n') {
s.pop();
if s.ends_with('\r') {
s.pop();
}
}
}
/// Takes a string password and checks that it meets minimum requirements.
///
/// The current minimum password requirement is a 12 character length character length.
pub fn is_password_sufficiently_complex(password: &[u8]) -> Result<(), String> {
if password.len() >= MINIMUM_PASSWORD_LEN {
Ok(())
} else {
Err(format!(
"Please use at least {} characters for your password.",
MINIMUM_PASSWORD_LEN
))
}
}
/// Returns a random 24-word english mnemonic.
pub fn random_mnemonic() -> Mnemonic {
Mnemonic::new(MnemonicType::Words24, Language::English)
}
/// Attempts to parse a mnemonic phrase.
pub fn mnemonic_from_phrase(phrase: &str) -> Result<Mnemonic, String> {
Mnemonic::from_phrase(phrase, Language::English).map_err(|e| e.to_string())
}
/// Provides a new-type wrapper around `String` that is zeroized on `Drop`.
///
/// Useful for ensuring that password memory is zeroed-out on drop.
#[derive(Clone, PartialEq, Serialize, Deserialize, Zeroize)]
#[zeroize(drop)]
#[serde(transparent)]
pub struct ZeroizeString(String);
impl From<String> for ZeroizeString {
fn from(s: String) -> Self {
Self(s)
}
}
impl ZeroizeString {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<[u8]> for ZeroizeString {
fn as_ref(&self) -> &[u8] {
self.0.as_bytes()
}
}
#[cfg(test)]
mod test {
use super::is_password_sufficiently_complex;
use super::strip_off_newlines;
#[test]
fn test_strip_off() {
let expected = "hello world".as_bytes().to_vec();
assert_eq!(
strip_off_newlines("hello world\n".as_bytes().to_vec()),
expected
);
assert_eq!(
strip_off_newlines("hello world\n\n\n\n".as_bytes().to_vec()),
expected
);
assert_eq!(
strip_off_newlines("hello world\r".as_bytes().to_vec()),
expected
);
assert_eq!(
strip_off_newlines("hello world\r\r\r\r\r".as_bytes().to_vec()),
expected
);
assert_eq!(
strip_off_newlines("hello world\r\n".as_bytes().to_vec()),
expected
);
assert_eq!(
strip_off_newlines("hello world\r\n\r\n".as_bytes().to_vec()),
expected
);
assert_eq!(
strip_off_newlines("hello world".as_bytes().to_vec()),
expected
);
}
#[test]
fn test_password_over_min_length() {
is_password_sufficiently_complex(b"TestPasswordLong").unwrap();
}
#[test]
fn test_password_exactly_min_length() {
is_password_sufficiently_complex(b"TestPassword").unwrap();
}
#[test]
#[should_panic]
fn test_password_too_short() {
is_password_sufficiently_complex(b"TestPass").unwrap();
}
}