diff --git a/Cargo.lock b/Cargo.lock index e36b5db..2b33181 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,18 +105,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "arrayref" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" - -[[package]] -name = "arrayvec" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" - [[package]] name = "ascii" version = "1.1.0" @@ -124,22 +112,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" [[package]] -name = "baid58" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0585242d87ed976e05db6ae86a0f771f140104a4b6c91b4c3e43b9b2357486" -dependencies = [ - "base58", - "blake3", - "mnemonic", - "sha2", -] - -[[package]] -name = "base58" -version = "0.2.0" +name = "base64" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6107fe1be6682a68940da878d9e9f5e90ca5745b3dec9fd1bb393c8777d4f581" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" [[package]] name = "bitflags" @@ -147,19 +123,6 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" -[[package]] -name = "blake3" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cca6d3674597c30ddf2c587bf8d9d65c9a84d2326d941cc79c9842dfe0ef52" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if", - "constant_time_eq", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -243,12 +206,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" -[[package]] -name = "constant_time_eq" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" - [[package]] name = "cpufeatures" version = "0.2.12" @@ -482,9 +439,10 @@ version = "0.1.0" dependencies = [ "aes", "amplify", - "baid58", + "base64", "clap", "crossbeam-channel", + "mnemonic", "rand", "rpassword", "secp256k1", diff --git a/Cargo.toml b/Cargo.toml index 400a2c1..b70ac87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,6 @@ required-features = ["cli"] [dependencies] amplify = "4.6.0" -baid58 = "0.4.4" secp256k1 = { version = "0.29.0", features = ["rand", "global-context"] } rand = "0.8.5" clap = { version = "4.5.4", features = ["derive"], optional = true } @@ -23,6 +22,9 @@ rpassword = { version = "7.3.1", optional = true } aes = { version = "0.8.4", optional = true } crossbeam-channel = { version = "0.5.12", optional = true } +mnemonic = "1.0.1" +base64 = "0.22.0" + [features] default = ["cli"] cli = ["clap", "crossbeam-channel", "shellexpand", "rpassword", "aes"] diff --git a/src/baid64.rs b/src/baid64.rs new file mode 100644 index 0000000..ed0e4e1 --- /dev/null +++ b/src/baid64.rs @@ -0,0 +1,139 @@ +// Base64-encoded identifiers +// +// SPDX-License-Identifier: Apache-2.0 +// +// Written in 2024 by +// Dr Maxim Orlovsky +// +// Copyright (C) 2024 Cyphernet. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fmt::{self, Display, Formatter}; + +use base64::Engine; +use sha2::Digest; + +pub const HRI_MAX_LEN: usize = 16; +const LEN: usize = 32; + +pub const BAID64_ALPHABET: &str = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@$"; + +pub trait ToBaid64 { + const HRI: &'static str; + const CHUNKING: bool; + const PREFIX: bool; + const MNEMONIC: bool; + + fn to_baid64_payload(&self) -> [u8; 32]; + fn to_baid64(&self) -> Baid64 { + Baid64::with( + Self::HRI, + self.to_baid64_payload(), + Self::CHUNKING, + Self::PREFIX, + Self::MNEMONIC, + ) + } + fn to_baid64_string(&self) -> String { self.to_baid64().to_string() } + fn fmt_baid64(&self, f: &mut Formatter) -> fmt::Result { Display::fmt(&self.to_baid64(), f) } +} + +#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)] +pub struct Baid64 { + hri: &'static str, + chunking: bool, + mnemonic: String, + prefix: bool, + suffix: bool, + checksum: u32, + payload: [u8; LEN], +} + +impl Baid64 { + pub fn with( + hri: &'static str, + payload: [u8; LEN], + chunking: bool, + prefix: bool, + suffix: bool, + ) -> Self { + debug_assert!(hri.len() <= HRI_MAX_LEN, "HRI is too long"); + debug_assert!(LEN > HRI_MAX_LEN, "Baid64 id must be at least 9 bytes"); + + let key = sha2::Sha256::digest(hri.as_bytes()); + let mut sha = sha2::Sha256::new_with_prefix(key); + sha.update(&payload); + let sha = sha.finalize(); + let checksum = u32::from_le_bytes([sha[0], sha[1], sha[1], sha[2]]); + let mnemonic = mnemonic::to_string(checksum.to_le_bytes()); + + Self { + hri, + chunking, + mnemonic, + prefix, + suffix, + checksum, + payload, + } + } + + pub fn plain(hri: &'static str, payload: [u8; LEN]) -> Self { + Self::with(hri, payload, false, false, false) + } + pub fn chunked(hri: &'static str, payload: [u8; LEN]) -> Self { + Self::with(hri, payload, true, false, false) + } + pub fn full(hri: &'static str, payload: [u8; LEN]) -> Self { + Self::with(hri, payload, true, true, true) + } + + pub const fn human_identifier(&self) -> &'static str { self.hri } + + pub fn mnemonic(&self) -> &str { &self.mnemonic } + pub const fn checksum(&self) -> u32 { self.checksum } +} + +impl Display for Baid64 { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + use base64::alphabet::Alphabet; + use base64::engine::general_purpose::NO_PAD; + use base64::engine::GeneralPurpose; + + if (self.prefix && !f.sign_minus()) || (!self.prefix && f.sign_minus()) { + write!(f, "{}:", self.hri)?; + } + + let alphabet = Alphabet::new(BAID64_ALPHABET).expect("invalid Baid64 alphabet"); + let engine = GeneralPurpose::new(&alphabet, NO_PAD); + let s = engine.encode(self.payload); + + if self.chunking { + let bytes = s.as_bytes(); + f.write_str(&String::from_utf8_lossy(&bytes[..6]))?; + for chunk in bytes[6..].chunks(8) { + write!(f, "-{}", &String::from_utf8_lossy(chunk))?; + } + } else { + f.write_str(&s)?; + } + + if (self.suffix && !f.alternate()) || (!self.suffix && f.alternate()) { + write!(f, "#{}", self.mnemonic)?; + } + + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index f22f0ed..4fa72cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,8 @@ #[macro_use] extern crate amplify; +pub mod baid64; + use std::fmt; use std::fmt::{Display, Formatter}; use std::hash::Hash; @@ -31,10 +33,11 @@ use aes::cipher::generic_array::GenericArray; use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit}; use aes::{Aes256, Block}; use amplify::{Bytes, Display}; -use baid58::{Baid58ParseError, Chunking, FromBaid58, ToBaid58, CHUNKING_32}; use secp256k1::SECP256K1; use sha2::{Digest, Sha256}; +use crate::baid64::ToBaid64; + #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Display, Default)] #[non_exhaustive] pub enum Algo { @@ -133,12 +136,13 @@ pub struct Ssi { key: Bytes<30>, } -impl ToBaid58<32> for Ssi { +impl ToBaid64 for Ssi { const HRI: &'static str = "ssi"; - const CHUNKING: Option = CHUNKING_32; + const CHUNKING: bool = true; + const PREFIX: bool = true; + const MNEMONIC: bool = false; - fn to_baid58_payload(&self) -> [u8; 32] { <[u8; 32]>::from(*self) } - fn to_baid58_string(&self) -> String { self.to_string() } + fn to_baid64_payload(&self) -> [u8; 32] { <[u8; 32]>::from(*self) } } impl From for [u8; 32] { @@ -160,17 +164,17 @@ impl From<[u8; 32]> for Ssi { } } -impl FromBaid58<32> for Ssi {} - impl Display for Ssi { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{::<.2}", self.to_baid58()) } + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_baid64(f) } } +/* impl FromStr for Ssi { type Err = Baid58ParseError; fn from_str(s: &str) -> Result { Self::from_baid58_maybe_chunked_str(s, ':', '#') } } + */ #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display, Error)] #[display("invalid public key")] @@ -194,11 +198,13 @@ impl Ssi { #[derive(Clone, Eq, PartialEq)] pub struct SsiSecret(secp256k1::SecretKey); -impl ToBaid58<32> for SsiSecret { - const HRI: &'static str = "ssi"; +impl ToBaid64 for SsiSecret { + const HRI: &'static str = "ssi:priv"; + const CHUNKING: bool = false; + const PREFIX: bool = true; + const MNEMONIC: bool = false; - fn to_baid58_payload(&self) -> [u8; 32] { <[u8; 32]>::from(self.clone()) } - fn to_baid58_string(&self) -> String { self.to_string() } + fn to_baid64_payload(&self) -> [u8; 32] { <[u8; 32]>::from(self.clone()) } } impl From for [u8; 32] { @@ -211,17 +217,17 @@ impl From<[u8; 32]> for SsiSecret { } } -impl FromBaid58<32> for SsiSecret {} - impl Display for SsiSecret { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{:!<.2}", self.to_baid58()) } + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_baid64(f) } } +/* impl FromStr for SsiSecret { type Err = Baid58ParseError; fn from_str(s: &str) -> Result { Self::from_baid58_maybe_chunked_str(s, '!', '#') } } + */ impl SsiSecret { pub fn new(chain: Chain) -> Self { diff --git a/src/main.rs b/src/main.rs index db95bf2..feb603d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -83,7 +83,9 @@ fn main() { let ssi = secret.to_public(); println!("{ssi}"); - secret.encrypt(passwd); + if !passwd.is_empty() { + secret.encrypt(passwd); + } let mut path = data_dir.clone(); path.push(name);