From d021439ddeccecb57f4774419516867c25292c5c Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Sun, 26 May 2024 15:26:10 +0200 Subject: [PATCH] fix encoding of private keys --- Cargo.lock | 1 + Cargo.toml | 1 + src/bip340.rs | 27 +------- src/ed25519.rs | 27 +------- src/encrypt.rs | 26 ++++---- src/lib.rs | 2 +- src/main.rs | 8 +-- src/runtime.rs | 19 +++--- src/secret.rs | 169 +++++++++++++++++++++++++++++++++---------------- 9 files changed, 140 insertions(+), 140 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1aaf5c3..ad8fc09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -670,6 +670,7 @@ dependencies = [ "amplify", "ascii-armor", "baid64", + "base64", "chrono", "clap", "crossbeam-channel", diff --git a/Cargo.toml b/Cargo.toml index ce794cf..a5ad52d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ amplify = "4.6.0" strict_encoding = "2.7.0-beta.4" ascii-armor = "0.7.0" baid64 = "0.2.2" +base64 = "0.22.1" secp256k1 = { version = "0.29.0", features = ["rand", "global-context", "rand-std"] } ec25519 = "0.1.0" rand = "0.8.5" diff --git a/src/bip340.rs b/src/bip340.rs index 064f5dd..b136278 100644 --- a/src/bip340.rs +++ b/src/bip340.rs @@ -20,18 +20,14 @@ // limitations under the License. use std::cmp::Ordering; -use std::fmt; -use std::fmt::{Display, Formatter}; use std::hash::{Hash, Hasher}; -use std::str::FromStr; -use baid64::{Baid64ParseError, DisplayBaid64, FromBaid64Str}; use secp256k1::schnorr::Signature; use secp256k1::{Keypair, Message, SecretKey, XOnlyPublicKey, SECP256K1}; use crate::{Algo, Chain, InvalidPubkey, InvalidSig, SsiPub, SsiSig}; -#[derive(Clone, Eq, PartialEq)] +#[derive(Clone, Eq, PartialEq, From)] pub struct Bip340Secret(pub(crate) SecretKey); impl Ord for Bip340Secret { @@ -46,18 +42,6 @@ impl Hash for Bip340Secret { fn hash(&self, state: &mut H) { self.0.secret_bytes().hash(state) } } -impl DisplayBaid64 for Bip340Secret { - const HRI: &'static str = "bip340-priv"; - const CHUNKING: bool = false; - const PREFIX: bool = true; - const EMBED_CHECKSUM: bool = true; - const MNEMONIC: bool = false; - - fn to_baid64_payload(&self) -> [u8; 32] { <[u8; 32]>::from(self.clone()) } -} - -impl FromBaid64Str for Bip340Secret {} - impl From for [u8; 32] { fn from(ssi: Bip340Secret) -> Self { ssi.0.secret_bytes() } } @@ -68,15 +52,6 @@ impl From<[u8; 32]> for Bip340Secret { } } -impl Display for Bip340Secret { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_baid64(f) } -} - -impl FromStr for Bip340Secret { - type Err = Baid64ParseError; - fn from_str(s: &str) -> Result { Self::from_baid64_str(s) } -} - impl Bip340Secret { pub fn new(chain: Chain) -> Self { use rand::thread_rng; diff --git a/src/ed25519.rs b/src/ed25519.rs index d3d0611..7713d13 100644 --- a/src/ed25519.rs +++ b/src/ed25519.rs @@ -20,18 +20,14 @@ // limitations under the License. use std::cmp::Ordering; -use std::fmt; -use std::fmt::{Display, Formatter}; use std::hash::{Hash, Hasher}; use std::ops::Deref; -use std::str::FromStr; -use baid64::{Baid64ParseError, DisplayBaid64, FromBaid64Str}; use ec25519::{KeyPair, Noise, PublicKey, SecretKey, Seed, Signature}; use crate::{Algo, Chain, InvalidPubkey, InvalidSig, SsiPub, SsiSig}; -#[derive(Clone, Eq, PartialEq)] +#[derive(Clone, Eq, PartialEq, From)] pub struct Ed25519Secret(pub(crate) SecretKey); impl Ord for Ed25519Secret { @@ -46,18 +42,6 @@ impl Hash for Ed25519Secret { fn hash(&self, state: &mut H) { self.0.as_slice().hash(state) } } -impl DisplayBaid64<64> for Ed25519Secret { - const HRI: &'static str = "ed25519-priv"; - const CHUNKING: bool = false; - const PREFIX: bool = true; - const EMBED_CHECKSUM: bool = true; - const MNEMONIC: bool = false; - - fn to_baid64_payload(&self) -> [u8; 64] { <[u8; 64]>::from(self.clone()) } -} - -impl FromBaid64Str<64> for Ed25519Secret {} - impl From for [u8; 64] { fn from(ssi: Ed25519Secret) -> Self { *ssi.0.deref() } } @@ -68,15 +52,6 @@ impl From<[u8; 64]> for Ed25519Secret { } } -impl Display for Ed25519Secret { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_baid64(f) } -} - -impl FromStr for Ed25519Secret { - type Err = Baid64ParseError; - fn from_str(s: &str) -> Result { Self::from_baid64_str(s) } -} - impl Ed25519Secret { pub fn new(chain: Chain) -> Self { loop { diff --git a/src/encrypt.rs b/src/encrypt.rs index 7353f0c..70dcde4 100644 --- a/src/encrypt.rs +++ b/src/encrypt.rs @@ -21,8 +21,8 @@ use std::str::FromStr; -use aes_gcm::aead::{Aead, OsRng}; -use aes_gcm::{AeadCore, Aes256Gcm, KeyInit, Nonce}; +use aes_gcm::aead::{Aead, Nonce, OsRng}; +use aes_gcm::{AeadCore, Aes256Gcm, KeyInit}; use amplify::confinement::{Confined, SmallOrdMap, U64 as U64MAX}; use amplify::{Bytes32, Wrapper}; use armor::{ArmorHeader, ArmorParseError, AsciiArmor}; @@ -75,6 +75,7 @@ impl SymmetricKey { #[strict_type(lib = LIB_NAME_SSI)] pub struct Encrypted { pub keys: SmallOrdMap, + pub nonce: [u8; 12], pub data: Confined, 0, U64MAX>, } @@ -91,7 +92,7 @@ impl AsciiArmor for Encrypted { fn to_ascii_armored_data(&self) -> Vec { self.to_strict_serialized::() - .expect("64 bits will not error") + .expect("64 bits will never error") .into_inner() } @@ -122,9 +123,10 @@ impl Encrypted { .map_err(|_| EncryptionError::InvalidPubkey(pk))?, ); } - let msg = encrypt(source, key); + let (nonce, msg) = encrypt(source, key); Ok(Self { keys: Confined::try_from(keys).map_err(|_| EncryptionError::TooManyReceivers)?, + nonce: nonce.into(), data: Confined::from_collection_unsafe(msg), }) } @@ -140,7 +142,7 @@ impl Encrypted { let key = pair .decrypt_key(key) .map_err(|_| DecryptionError::InvalidPubkey(pair.pk))?; - Ok(decrypt(self.data.to_inner(), key)) + Ok(decrypt(self.data.as_slice(), self.nonce.into(), key)) } } @@ -179,7 +181,7 @@ impl SsiPair { } } -pub fn encrypt(source: Vec, key: impl AsRef<[u8]>) -> Vec { +pub fn encrypt(source: Vec, key: impl AsRef<[u8]>) -> (Nonce, Vec) { let key = Sha256::digest(key.as_ref()); let key = aes_gcm::Key::::from_slice(key.as_slice()); @@ -189,23 +191,17 @@ pub fn encrypt(source: Vec, key: impl AsRef<[u8]>) -> Vec { let ciphered_data = cipher .encrypt(&nonce, source.as_ref()) .expect("failed to encrypt"); - // combining nonce and encrypted data together - // for storage purpose - let mut encrypted_data: Vec = nonce.to_vec(); - encrypted_data.extend_from_slice(&ciphered_data); - encrypted_data + (nonce, ciphered_data) } -pub fn decrypt(encrypted: Vec, key: impl AsRef<[u8]>) -> Vec { +pub fn decrypt(encrypted: &[u8], nonce: Nonce, key: impl AsRef<[u8]>) -> Vec { let key = Sha256::digest(key.as_ref()); let key = aes_gcm::Key::::from_slice(key.as_slice()); - let (nonce_arr, ciphered_data) = encrypted.split_at(12); - let nonce = Nonce::from_slice(nonce_arr); let cipher = Aes256Gcm::new(key); cipher - .decrypt(nonce, ciphered_data) + .decrypt(&nonce, encrypted) .expect("failed to decrypt data") } diff --git a/src/lib.rs b/src/lib.rs index 04fa892..d7b39ab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,6 +42,6 @@ pub use public::{ SsiSig, UnknownAlgo, UnknownChain, VerifyError, }; pub use runtime::{LoadError, SignerError, SsiRuntime, SSI_DIR}; -pub use secret::{SecretParseError, SsiPair, SsiSecret}; +pub use secret::{EncryptedSecret, RevealError, SecretParseError, SsiPair, SsiSecret}; pub const LIB_NAME_SSI: &str = "SSI"; diff --git a/src/main.rs b/src/main.rs index 2a2ceca..15211da 100644 --- a/src/main.rs +++ b/src/main.rs @@ -252,7 +252,7 @@ fn exec(command: Command) -> Result<(), CliError> { .map_err(CliError::Password)?; eprintln!("Generating new {algo} identity...."); - let mut secret = match prefix { + let secret = match prefix { Some(prefix) => SsiSecret::vanity(&prefix, algo, chain, threads), None => SsiSecret::new(algo, chain), }; @@ -260,11 +260,7 @@ fn exec(command: Command) -> Result<(), CliError> { let ssi = Ssi::new(uids, expiry, &secret); println!("{ssi}"); - if !passwd.is_empty() { - secret.conceal(passwd); - } - - runtime.secrets.insert(secret); + runtime.secrets.insert(secret.conceal(passwd)); runtime.identities.insert(ssi); runtime.store().map_err(CliError::Store)?; diff --git a/src/runtime.rs b/src/runtime.rs index c639c74..c1dec78 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -29,7 +29,9 @@ use std::path::PathBuf; use baid64::Baid64ParseError; -use crate::{Fingerprint, SecretParseError, Ssi, SsiPair, SsiParseError, SsiQuery, SsiSecret}; +use crate::{ + EncryptedSecret, Fingerprint, SecretParseError, Ssi, SsiPair, SsiParseError, SsiQuery, +}; #[derive(Debug, Display, Error, From)] #[display(inner)] @@ -57,7 +59,7 @@ pub enum SignerError { } pub struct SsiRuntime { - pub secrets: BTreeSet, + pub secrets: BTreeSet, pub identities: BTreeSet, } @@ -156,12 +158,9 @@ impl SsiRuntime { .secrets .iter() .find_map(|s| { - let mut s = (*s).clone(); - if !passwd.is_empty() { - s.reveal(passwd); - } - if s.to_public() == ssi.pk { - Some(s) + let sk = s.reveal(passwd).ok()?; + if sk.to_public() == ssi.pk { + Some(sk) } else { None } @@ -170,7 +169,5 @@ impl SsiRuntime { Ok(SsiPair::new(ssi, sk)) } - pub fn is_signing(&self, fp: Fingerprint) -> bool { - self.secrets.iter().any(|s| s.fingerprint() == fp) - } + pub fn is_signing(&self, fp: Fingerprint) -> bool { self.secrets.iter().any(|s| s.fp == fp) } } diff --git a/src/secret.rs b/src/secret.rs index bf3e266..9e35614 100644 --- a/src/secret.rs +++ b/src/secret.rs @@ -23,8 +23,15 @@ use std::fmt::{self, Display, Formatter}; use std::hash::Hash; use std::str::FromStr; -use amplify::Bytes32; -use baid64::Baid64ParseError; +use aes_gcm::aead::Nonce; +use aes_gcm::Aes256Gcm; +use amplify::hex::{FromHex, ToHex}; +use amplify::{hex, Bytes32}; +use baid64::{Baid64ParseError, BAID64_ALPHABET}; +use base64::alphabet::Alphabet; +use base64::engine::general_purpose::NO_PAD; +use base64::engine::GeneralPurpose; +use base64::Engine; use chrono::{DateTime, Utc}; use sha2::{Digest, Sha256}; @@ -33,10 +40,35 @@ use crate::{ SsiSig, }; +#[derive(Clone, Eq, PartialEq, Hash, Debug, Display, Error, From)] +#[display(doc_comments)] +pub enum RevealError { + #[from(ec25519::Error)] + #[from(secp256k1::Error)] + /// invalid password. + InvalidPassword, + + /// unsupported algorithm #{0}. + Unsupported(u8), +} + #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub enum SsiSecret { - Bip340(Fingerprint, Bip340Secret), - Ed25519(Fingerprint, Ed25519Secret), +pub struct EncryptedSecret { + pub fp: Fingerprint, + pub nonce: Nonce, + pub algo: Algo, + pub key: Vec, +} + +impl EncryptedSecret { + pub fn reveal(&self, passwd: impl AsRef) -> Result { + let sk = decrypt(&self.key, self.nonce, passwd.as_ref()); + match self.algo { + Algo::Ed25519 => Ok(ec25519::SecretKey::from_slice(&sk)?.into()), + Algo::Bip340 => Ok(secp256k1::SecretKey::from_slice(&sk)?.into()), + Algo::Other(algo) => Err(RevealError::Unsupported(algo)), + } + } } #[derive(Debug, Display, Error, From)] @@ -44,37 +76,81 @@ pub enum SsiSecret { pub enum SecretParseError { /// incomplete private key data. Incomplete, - /// invalid fingerprint data in private key - {0}. + + /// private key data misses key and signature scheme information. + NoAlgo, + + /// private key signature scheme {0} is not supported yet. + UnsupportedAlgo(String), + + #[from] + /// private key has invalid nonce value - {0} + InvalidNonce(hex::Error), + + /// invalid fingerprint data in private key - {0} InvalidFingerprint(Baid64ParseError), + #[from] /// invalid secret key data - {0} - InvalidSecret(Baid64ParseError), + Decode(base64::DecodeError), } -impl FromStr for SsiSecret { +impl FromStr for EncryptedSecret { type Err = SecretParseError; fn from_str(mut s: &str) -> Result { - s = s.trim_start_matches("ssi:"); - let (fp, sk) = s.split_once('/').ok_or(SecretParseError::Incomplete)?; + s = s.trim_start_matches("ssi://"); + let (prefix, sk) = s.split_once('/').ok_or(SecretParseError::Incomplete)?; + let (fp, nonce) = prefix.split_once(':').ok_or(SecretParseError::Incomplete)?; let fp = Fingerprint::from_str(fp).map_err(SecretParseError::InvalidFingerprint)?; - if sk.starts_with("bip340-priv") { - Ok(Self::Bip340(fp, Bip340Secret::from_str(sk)?)) - } else { - Ok(Self::Ed25519(fp, Ed25519Secret::from_str(sk)?)) - } + + let alphabet = Alphabet::new(BAID64_ALPHABET).expect("invalid Baid64 alphabet"); + let engine = GeneralPurpose::new(&alphabet, NO_PAD); + + let (schema, key) = sk.split_once(':').ok_or(SecretParseError::NoAlgo)?; + let nonce = <[u8; 12]>::from_hex(nonce)?.into(); + let algo = match schema { + "bip340-priv" => Algo::Bip340, + "ed25519-priv" => Algo::Ed25519, + other => return Err(SecretParseError::UnsupportedAlgo(other.to_owned())), + }; + + let key = engine.decode(key)?; + + Ok(Self { + fp, + nonce, + algo, + key, + }) } } -impl Display for SsiSecret { +impl Display for EncryptedSecret { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match self { - SsiSecret::Bip340(fp, sk) => write!(f, "{fp}/{sk}"), - SsiSecret::Ed25519(fp, sk) => write!(f, "{fp}/{sk}"), - } + let alphabet = Alphabet::new(BAID64_ALPHABET).expect("invalid Baid64 alphabet"); + let engine = GeneralPurpose::new(&alphabet, NO_PAD); + write!( + f, + "ssi://{}:{}/{}-priv:{}", + self.fp, + self.nonce.to_hex(), + self.algo, + engine.encode(&self.key) + ) } } +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, From)] +pub enum SsiSecret { + #[from] + #[from(secp256k1::SecretKey)] + Bip340(Bip340Secret), + #[from] + #[from(ec25519::SecretKey)] + Ed25519(Ed25519Secret), +} + impl SsiSecret { pub fn new(algo: Algo, chain: Chain) -> Self { match algo { @@ -86,14 +162,12 @@ impl SsiSecret { pub fn new_ed25519(chain: Chain) -> Self { let sk = Ed25519Secret::new(chain); - let fp = sk.to_public().fingerprint(); - Self::Ed25519(fp, sk) + Self::Ed25519(sk) } pub fn new_bip340(chain: Chain) -> Self { let sk = Bip340Secret::new(chain); - let fp = sk.to_public().fingerprint(); - Self::Bip340(fp, sk) + Self::Bip340(sk) } pub fn vanity(prefix: &str, algo: Algo, chain: Chain, threads: u8) -> Self { @@ -117,54 +191,39 @@ impl SsiSecret { pub fn algorithm(&self) -> Algo { match self { - SsiSecret::Bip340(_, _) => Algo::Bip340, - SsiSecret::Ed25519(_, _) => Algo::Ed25519, - } - } - - pub fn fingerprint(&self) -> Fingerprint { - match self { - SsiSecret::Bip340(fp, _) | SsiSecret::Ed25519(fp, _) => *fp, + SsiSecret::Bip340(_) => Algo::Bip340, + SsiSecret::Ed25519(_) => Algo::Ed25519, } } pub fn to_public(&self) -> SsiPub { match self { - SsiSecret::Bip340(_, sk) => sk.to_public(), - SsiSecret::Ed25519(_, sk) => sk.to_public(), + SsiSecret::Bip340(sk) => sk.to_public(), + SsiSecret::Ed25519(sk) => sk.to_public(), } } pub fn sign(&self, msg: [u8; 32]) -> SsiSig { match self { - SsiSecret::Bip340(_, sk) => sk.sign(msg), - SsiSecret::Ed25519(_, sk) => sk.sign(msg), + SsiSecret::Bip340(sk) => sk.sign(msg), + SsiSecret::Ed25519(sk) => sk.sign(msg), } } - pub fn conceal(&mut self, passwd: impl AsRef) { - self.replace(&encrypt(self.to_vec(), passwd.as_ref())); - } - - pub fn reveal(&mut self, passwd: impl AsRef) { - self.replace(&decrypt(self.to_vec(), passwd.as_ref())); - } - - pub fn to_vec(&self) -> Vec { - match self { - SsiSecret::Bip340(_, sk) => sk.0.secret_bytes().to_vec(), - SsiSecret::Ed25519(_, sk) => sk.0.to_vec(), + pub fn conceal(&self, passwd: impl AsRef) -> EncryptedSecret { + let (nonce, key) = encrypt(self.to_vec(), passwd.as_ref()); + EncryptedSecret { + fp: self.to_public().fingerprint(), + nonce, + algo: self.algorithm(), + key, } } - fn replace(&mut self, secret: &[u8]) { + pub fn to_vec(&self) -> Vec { match self { - SsiSecret::Bip340(_, sk) => { - sk.0 = secp256k1::SecretKey::from_slice(secret).expect("same size") - } - SsiSecret::Ed25519(_, sk) => { - sk.0 = ec25519::SecretKey::from_slice(secret).expect("same size") - } + SsiSecret::Bip340(sk) => sk.0.secret_bytes().to_vec(), + SsiSecret::Ed25519(sk) => sk.0.to_vec(), } } }