feat: v1.8.5, support simple PBKDF encryption

This commit is contained in:
2025-01-26 00:22:16 +08:00
parent cc5178d1c1
commit a7802a3750
6 changed files with 235 additions and 5 deletions

2
Cargo.lock generated
View File

@@ -1867,7 +1867,7 @@ dependencies = [
[[package]]
name = "tiny-encrypt"
version = "1.8.4"
version = "1.8.5"
dependencies = [
"aes-gcm-stream",
"base64 0.22.1",

View File

@@ -1,6 +1,6 @@
[package]
name = "tiny-encrypt"
version = "1.8.4"
version = "1.8.5"
edition = "2021"
license = "MIT"
description = "A simple and tiny file encrypt tool"

View File

@@ -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" {

View File

@@ -75,4 +75,5 @@ mod util_keychainstatic;
mod cmd_execenv;
#[cfg(feature = "secure-enclave")]
mod util_keychainkey;
mod util_simple_pbe;

View File

@@ -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
View 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());
}