feat: v1.8.5, support simple PBKDF encryption
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -1867,7 +1867,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tiny-encrypt"
|
name = "tiny-encrypt"
|
||||||
version = "1.8.4"
|
version = "1.8.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm-stream",
|
"aes-gcm-stream",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "tiny-encrypt"
|
name = "tiny-encrypt"
|
||||||
version = "1.8.4"
|
version = "1.8.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "A simple and tiny file encrypt tool"
|
description = "A simple and tiny file encrypt tool"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use serde::Serialize;
|
|||||||
use std::io;
|
use std::io;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
|
use crate::util_simple_pbe::SimplePbkdfEncryptionV1;
|
||||||
|
|
||||||
// Reference: https://git.hatter.ink/hatter/tiny-encrypt-rs/issues/3
|
// Reference: https://git.hatter.ink/hatter/tiny-encrypt-rs/issues/3
|
||||||
const SIMPLE_ENCRYPTION_HEADER: &str = "tinyencrypt-dir";
|
const SIMPLE_ENCRYPTION_HEADER: &str = "tinyencrypt-dir";
|
||||||
@@ -40,6 +41,14 @@ pub struct CmdSimpleEncrypt {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub value_hex: Option<String>,
|
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
|
/// Direct output result value
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub direct_output: bool,
|
pub direct_output: bool,
|
||||||
@@ -71,6 +80,10 @@ pub struct CmdSimpleDecrypt {
|
|||||||
#[arg(long, short = 'o')]
|
#[arg(long, short = 'o')]
|
||||||
pub output_format: Option<String>,
|
pub output_format: Option<String>,
|
||||||
|
|
||||||
|
/// PBKDF encryption password
|
||||||
|
#[arg(long, short = 'A')]
|
||||||
|
pub password: Option<String>,
|
||||||
|
|
||||||
/// Direct output result value
|
/// Direct output result value
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub direct_output: bool,
|
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 = cmd_encrypt::encrypt_envelops(cryptor, &value, &envelops)?;
|
||||||
|
|
||||||
let envelops_json = serde_json::to_string(&envelops)?;
|
let envelops_json = serde_json::to_string(&envelops)?;
|
||||||
let simple_encrypt_result = format!("{}.{}",
|
let mut simple_encrypt_result = format!("{}.{}",
|
||||||
SIMPLE_ENCRYPTION_HEADER,
|
SIMPLE_ENCRYPTION_HEADER,
|
||||||
URL_SAFE_NO_PAD.encode(envelops_json.as_bytes())
|
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);
|
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),
|
_ => 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"),
|
None => return simple_error!("--value-stdin/value must assign one"),
|
||||||
Some(value) => value,
|
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<_>>();
|
let value_parts = value.trim().split(SIMPLE_ENCRYPTION_DOT).collect::<Vec<_>>();
|
||||||
if value_parts.len() != 2 {
|
if value_parts.len() != 2 {
|
||||||
return simple_error!("bad value format: {}", value);
|
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);
|
return simple_error!("no envelops found: {:?}", cmd_simple_decrypt.key_id);
|
||||||
}
|
}
|
||||||
if filter_envelops.len() > 1 {
|
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)?;
|
let value = crate::cmd_decrypt::try_decrypt_key(&config, filter_envelops[0], &pin, &slot, false)?;
|
||||||
if cmd_simple_decrypt.direct_output && output_format == "plain" {
|
if cmd_simple_decrypt.direct_output && output_format == "plain" {
|
||||||
|
|||||||
@@ -75,4 +75,5 @@ mod util_keychainstatic;
|
|||||||
mod cmd_execenv;
|
mod cmd_execenv;
|
||||||
#[cfg(feature = "secure-enclave")]
|
#[cfg(feature = "secure-enclave")]
|
||||||
mod util_keychainkey;
|
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)
|
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 {
|
pub fn is_use_default_pin() -> bool {
|
||||||
if util_env::get_no_default_pin_hint() {
|
if util_env::get_no_default_pin_hint() {
|
||||||
return false;
|
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