feat: v1.8.5, support simple PBKDF encryption
This commit is contained in:
@@ -9,6 +9,7 @@ use serde::Serialize;
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::process::exit;
|
||||
use crate::util_simple_pbe::SimplePbkdfEncryptionV1;
|
||||
|
||||
// Reference: https://git.hatter.ink/hatter/tiny-encrypt-rs/issues/3
|
||||
const SIMPLE_ENCRYPTION_HEADER: &str = "tinyencrypt-dir";
|
||||
@@ -40,6 +41,14 @@ pub struct CmdSimpleEncrypt {
|
||||
#[arg(long)]
|
||||
pub value_hex: Option<String>,
|
||||
|
||||
/// With PBKDF encryption
|
||||
#[arg(long, short = 'P')]
|
||||
pub with_pbkdf_encryption: bool,
|
||||
|
||||
/// PBKDF encryption password
|
||||
#[arg(long, short = 'A')]
|
||||
pub password: Option<String>,
|
||||
|
||||
/// Direct output result value
|
||||
#[arg(long)]
|
||||
pub direct_output: bool,
|
||||
@@ -71,6 +80,10 @@ pub struct CmdSimpleDecrypt {
|
||||
#[arg(long, short = 'o')]
|
||||
pub output_format: Option<String>,
|
||||
|
||||
/// PBKDF encryption password
|
||||
#[arg(long, short = 'A')]
|
||||
pub password: Option<String>,
|
||||
|
||||
/// Direct output result value
|
||||
#[arg(long)]
|
||||
pub direct_output: bool,
|
||||
@@ -186,11 +199,17 @@ pub fn inner_simple_encrypt(cmd_simple_encrypt: CmdSimpleEncrypt) -> XResult<()>
|
||||
let envelops = cmd_encrypt::encrypt_envelops(cryptor, &value, &envelops)?;
|
||||
|
||||
let envelops_json = serde_json::to_string(&envelops)?;
|
||||
let simple_encrypt_result = format!("{}.{}",
|
||||
let mut simple_encrypt_result = format!("{}.{}",
|
||||
SIMPLE_ENCRYPTION_HEADER,
|
||||
URL_SAFE_NO_PAD.encode(envelops_json.as_bytes())
|
||||
);
|
||||
|
||||
let with_pbkdf_encryption = cmd_simple_encrypt.with_pbkdf_encryption || cmd_simple_encrypt.password.is_some();
|
||||
if with_pbkdf_encryption {
|
||||
let password = util::read_password(&cmd_simple_encrypt.password)?;
|
||||
simple_encrypt_result = SimplePbkdfEncryptionV1::encrypt(&password, simple_encrypt_result.as_bytes())?.to_string();
|
||||
}
|
||||
|
||||
CmdResult::success(&simple_encrypt_result).print_exit(cmd_simple_encrypt.direct_output);
|
||||
}
|
||||
|
||||
@@ -207,10 +226,18 @@ pub fn inner_simple_decrypt(cmd_simple_decrypt: CmdSimpleDecrypt) -> XResult<()>
|
||||
_ => return simple_error!("not supported output format: {}", output_format),
|
||||
};
|
||||
|
||||
let value = match cmd_simple_decrypt.get_value()? {
|
||||
let mut value = match cmd_simple_decrypt.get_value()? {
|
||||
None => return simple_error!("--value-stdin/value must assign one"),
|
||||
Some(value) => value,
|
||||
};
|
||||
|
||||
if SimplePbkdfEncryptionV1::matches(&value) {
|
||||
let simple_pbkdf_encryption_v1: SimplePbkdfEncryptionV1 = value.as_str().try_into()?;
|
||||
let password = util::read_password(&cmd_simple_decrypt.password)?;
|
||||
let plaintext_bytes = simple_pbkdf_encryption_v1.decrypt(&password)?;
|
||||
value = opt_result!(String::from_utf8(plaintext_bytes), "Decrypt PBKDF encryption failed: {}");
|
||||
}
|
||||
|
||||
let value_parts = value.trim().split(SIMPLE_ENCRYPTION_DOT).collect::<Vec<_>>();
|
||||
if value_parts.len() != 2 {
|
||||
return simple_error!("bad value format: {}", value);
|
||||
@@ -234,7 +261,13 @@ pub fn inner_simple_decrypt(cmd_simple_decrypt: CmdSimpleDecrypt) -> XResult<()>
|
||||
return simple_error!("no envelops found: {:?}", cmd_simple_decrypt.key_id);
|
||||
}
|
||||
if filter_envelops.len() > 1 {
|
||||
return simple_error!("too many envelops: {:?}, len: {}", cmd_simple_decrypt.key_id, filter_envelops.len());
|
||||
let mut kids = vec![];
|
||||
debugging!("Found {} envelopes", filter_envelops.len());
|
||||
for envelop in &filter_envelops {
|
||||
kids.push(envelop.kid.clone());
|
||||
debugging!("- {} {}", envelop.kid, envelop.r#type.get_name());
|
||||
}
|
||||
return simple_error!("too many envelops: {:?}, len: {}, matched kids: [{}]", cmd_simple_decrypt.key_id, filter_envelops.len(), kids.join(","));
|
||||
}
|
||||
let value = crate::cmd_decrypt::try_decrypt_key(&config, filter_envelops[0], &pin, &slot, false)?;
|
||||
if cmd_simple_decrypt.direct_output && output_format == "plain" {
|
||||
|
||||
@@ -75,4 +75,5 @@ mod util_keychainstatic;
|
||||
mod cmd_execenv;
|
||||
#[cfg(feature = "secure-enclave")]
|
||||
mod util_keychainkey;
|
||||
mod util_simple_pbe;
|
||||
|
||||
|
||||
21
src/util.rs
21
src/util.rs
@@ -62,6 +62,27 @@ pub fn read_pin(pin: &Option<String>) -> XResult<String> {
|
||||
Ok(rpin)
|
||||
}
|
||||
|
||||
pub fn read_password(password: &Option<String>) -> XResult<String> {
|
||||
let rpassword = match password {
|
||||
Some(pin) => pin.to_string(),
|
||||
None => {
|
||||
let pin_entry = util_env::get_pin_entry().unwrap_or_else(|| "pinentry".to_string());
|
||||
if let Some(mut input) = PassphraseInput::with_binary(pin_entry) {
|
||||
let secret = input
|
||||
.with_description("Please input your password.")
|
||||
.with_prompt("Password:")
|
||||
.interact();
|
||||
opt_result!(secret, "Read password from pinentry failed: {}")
|
||||
.expose_secret()
|
||||
.to_string()
|
||||
} else {
|
||||
opt_result!(rpassword::prompt_password("Please input password: "), "Read password failed: {}")
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(rpassword)
|
||||
}
|
||||
|
||||
pub fn is_use_default_pin() -> bool {
|
||||
if util_env::get_no_default_pin_hint() {
|
||||
return false;
|
||||
|
||||
175
src/util_simple_pbe.rs
Normal file
175
src/util_simple_pbe.rs
Normal file
@@ -0,0 +1,175 @@
|
||||
use crate::util_digest;
|
||||
use aes_gcm_stream::{Aes256GcmStreamDecryptor, Aes256GcmStreamEncryptor};
|
||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||
use base64::Engine;
|
||||
use rand::random;
|
||||
use rust_util::{opt_result, simple_error, SimpleError, XResult};
|
||||
use std::fmt::Display;
|
||||
|
||||
const SIMPLE_PBKDF_ENCRYPTION_PREFIX: &str = "tinyencrypt-pbkdf-encryption-v1";
|
||||
// FORMAT
|
||||
// <PREFIX>.<repeation>.<iterations>.<base64_uri(salt)>.<base64_uri(nonce)>.<base64_uri(ciphertext)>.<base64_uri(tag)>
|
||||
|
||||
pub struct SimplePbkdfEncryptionV1 {
|
||||
pub repetition: u32,
|
||||
pub iterations: u32,
|
||||
pub salt: Vec<u8>,
|
||||
pub nonce: Vec<u8>,
|
||||
pub ciphertext: Vec<u8>,
|
||||
pub tag: Vec<u8>,
|
||||
}
|
||||
|
||||
impl SimplePbkdfEncryptionV1 {
|
||||
pub fn matches(enc: &str) -> bool {
|
||||
enc.starts_with(&format!("{SIMPLE_PBKDF_ENCRYPTION_PREFIX}."))
|
||||
}
|
||||
|
||||
pub fn encrypt(password: &str, plaintext: &[u8]) -> XResult<SimplePbkdfEncryptionV1> {
|
||||
let salt: [u8; 12] = random();
|
||||
let repetition = 1000;
|
||||
let iterations = 10000;
|
||||
let key = simple_pbkdf(password.as_bytes(), &salt, repetition, iterations);
|
||||
|
||||
let key_bytes: [u8; 32] = opt_result!(key.try_into(), "Bad AES 256 key: {:?}");
|
||||
let nonce: [u8; 12] = random();
|
||||
let mut ciphertext = vec![];
|
||||
|
||||
let mut aes256_gcm_stream_encryptor = Aes256GcmStreamEncryptor::new(key_bytes, &nonce);
|
||||
ciphertext.extend_from_slice(&aes256_gcm_stream_encryptor.update(plaintext));
|
||||
let (last_ciphertext, tag) = aes256_gcm_stream_encryptor.finalize();
|
||||
ciphertext.extend_from_slice(&last_ciphertext);
|
||||
|
||||
Ok(SimplePbkdfEncryptionV1 {
|
||||
repetition,
|
||||
iterations,
|
||||
salt: salt.to_vec(),
|
||||
nonce: nonce.to_vec(),
|
||||
ciphertext,
|
||||
tag,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn decrypt(&self, password: &str) -> XResult<Vec<u8>> {
|
||||
let key = simple_pbkdf(
|
||||
password.as_bytes(),
|
||||
&self.salt,
|
||||
self.repetition,
|
||||
self.iterations,
|
||||
);
|
||||
let key_bytes: [u8; 32] = opt_result!(key.try_into(), "Bad AES 256 key: {:?}");
|
||||
let mut plaintext = vec![];
|
||||
|
||||
let mut aes256_gcm_stream_decryptor = Aes256GcmStreamDecryptor::new(key_bytes, &self.nonce);
|
||||
plaintext.extend_from_slice(&aes256_gcm_stream_decryptor.update(&self.ciphertext));
|
||||
plaintext.extend_from_slice(&aes256_gcm_stream_decryptor.update(&self.tag));
|
||||
plaintext.extend_from_slice(&opt_result!(
|
||||
aes256_gcm_stream_decryptor.finalize(),
|
||||
"Decrypt failed: {}"
|
||||
));
|
||||
|
||||
Ok(plaintext)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for SimplePbkdfEncryptionV1 {
|
||||
type Error = SimpleError;
|
||||
|
||||
fn try_from(enc: String) -> Result<Self, Self::Error> {
|
||||
TryFrom::<&str>::try_from(enc.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for SimplePbkdfEncryptionV1 {
|
||||
type Error = SimpleError;
|
||||
|
||||
fn try_from(enc: &str) -> Result<Self, Self::Error> {
|
||||
if !Self::matches(enc) {
|
||||
return simple_error!("Not simple PBKDF encryption: {enc}");
|
||||
}
|
||||
let parts = enc.split(".").collect::<Vec<_>>();
|
||||
|
||||
let repetition: u32 = opt_result!(
|
||||
parts[1].parse(),
|
||||
"Parse simple PBKDF failed, invalid repetition: {}, error: {}",
|
||||
parts[1]
|
||||
);
|
||||
let iterations: u32 = opt_result!(
|
||||
parts[2].parse(),
|
||||
"Parse simple PBKDF failed, invalid iterations: {}, error: {}",
|
||||
parts[2]
|
||||
);
|
||||
let salt = opt_result!(
|
||||
URL_SAFE_NO_PAD.decode(parts[3]),
|
||||
"Parse simple PBKDF failed, invalid salt: {}, error: {}",
|
||||
parts[3]
|
||||
);
|
||||
let nonce = opt_result!(
|
||||
URL_SAFE_NO_PAD.decode(parts[4]),
|
||||
"Parse simple PBKDF failed, invalid nonce: {}, error: {}",
|
||||
parts[4]
|
||||
);
|
||||
let ciphertext = opt_result!(
|
||||
URL_SAFE_NO_PAD.decode(parts[5]),
|
||||
"Parse simple PBKDF failed, invalid ciphertext: {}, error: {}",
|
||||
parts[5]
|
||||
);
|
||||
let tag = opt_result!(
|
||||
URL_SAFE_NO_PAD.decode(parts[6]),
|
||||
"Parse simple PBKDF failed, invalid tag: {}, error: {}",
|
||||
parts[6]
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
repetition,
|
||||
iterations,
|
||||
salt,
|
||||
nonce,
|
||||
ciphertext,
|
||||
tag,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for SimplePbkdfEncryptionV1 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut enc = String::with_capacity(1024);
|
||||
enc.push_str(SIMPLE_PBKDF_ENCRYPTION_PREFIX);
|
||||
enc.push('.');
|
||||
enc.push_str(&self.repetition.to_string());
|
||||
enc.push('.');
|
||||
enc.push_str(&self.iterations.to_string());
|
||||
enc.push('.');
|
||||
enc.push_str(&URL_SAFE_NO_PAD.encode(&self.salt));
|
||||
enc.push('.');
|
||||
enc.push_str(&URL_SAFE_NO_PAD.encode(&self.nonce));
|
||||
enc.push('.');
|
||||
enc.push_str(&URL_SAFE_NO_PAD.encode(&self.ciphertext));
|
||||
enc.push('.');
|
||||
enc.push_str(&URL_SAFE_NO_PAD.encode(&self.tag));
|
||||
write!(f, "{}", enc)
|
||||
}
|
||||
}
|
||||
|
||||
fn simple_pbkdf(password: &[u8], salt: &[u8], repetition: u32, iterations: u32) -> Vec<u8> {
|
||||
let mut input = password.to_vec();
|
||||
for it in 0..iterations {
|
||||
let mut message = Vec::with_capacity((input.len() + salt.len() + 4) * repetition as usize);
|
||||
for _ in 0..repetition {
|
||||
message.extend_from_slice(&it.to_be_bytes());
|
||||
message.extend_from_slice(&input);
|
||||
message.extend_from_slice(&salt);
|
||||
}
|
||||
input = util_digest::sha256_digest(&message);
|
||||
}
|
||||
input
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
let enc = SimplePbkdfEncryptionV1::encrypt("helloworld", "test".as_bytes()).unwrap();
|
||||
let enc_str = enc.to_string();
|
||||
let enc2: SimplePbkdfEncryptionV1 = enc_str.try_into().unwrap();
|
||||
assert_eq!(enc.to_string(), enc2.to_string());
|
||||
let plain = enc2.decrypt("helloworld").unwrap();
|
||||
assert_eq!(b"test", plain.as_slice());
|
||||
}
|
||||
Reference in New Issue
Block a user