feat: v0.5.0, supports ChaCha20/Poly1305

This commit is contained in:
2023-10-22 19:05:38 +08:00
parent 83464dfb28
commit bb07aec896
10 changed files with 105 additions and 125 deletions

2
Cargo.lock generated
View File

@@ -1729,7 +1729,7 @@ dependencies = [
[[package]]
name = "tiny-encrypt"
version = "0.4.4"
version = "0.5.0"
dependencies = [
"aes-gcm-stream",
"base64",

View File

@@ -1,6 +1,6 @@
[package]
name = "tiny-encrypt"
version = "0.4.4"
version = "0.5.0"
edition = "2021"
license = "MIT"
description = "A simple and tiny file encrypt tool"
@@ -39,7 +39,7 @@ yubikey = { version = "0.8", features = ["untested"] }
zeroize = "1.6"
[patch.crates-io]
rust-crypto = { git="https://github.com/jht5945/rust-crypto.git" }
rust-crypto = { git = "https://github.com/jht5945/rust-crypto.git" }
[profile.release]
codegen-units = 1

View File

@@ -16,14 +16,14 @@ use yubikey::piv::{AlgorithmId, decrypt_data};
use yubikey::YubiKey;
use zeroize::Zeroize;
use crate::{util, util_enc_file, util_envelop, util_file, util_pgp, util_piv};
use crate::{consts, crypto_simple, util, util_enc_file, util_envelop, util_file, util_pgp, util_piv};
use crate::compress::GzStreamDecoder;
use crate::config::TinyEncryptConfig;
use crate::consts::{
DATE_TIME_FORMAT, ENC_AES256_GCM_P256, ENC_AES256_GCM_P384, ENC_AES256_GCM_X25519,
SALT_COMMENT, TINY_ENC_CONFIG_FILE, TINY_ENC_FILE_EXT,
};
use crate::crypto_aes::{aes_gcm_decrypt, try_aes_gcm_decrypt_with_salt};
use crate::crypto_cryptor::Cryptor;
use crate::spec::{EncEncryptedMeta, TinyEncryptEnvelop, TinyEncryptEnvelopType, TinyEncryptMeta};
use crate::util::SecVec;
use crate::util_digest::DigestWrite;
@@ -109,6 +109,10 @@ pub fn decrypt_single(config: &Option<TinyEncryptConfig>,
util_enc_file::read_tiny_encrypt_meta_and_normalize(&mut file_in), "Read file: {}, failed: {}", &path_display);
debugging!("Found meta: {}", serde_json::to_string_pretty(&meta).unwrap());
let encryption_algorithm = meta.encryption_algorithm
.as_ref().map(String::as_str).unwrap_or(consts::TINY_ENC_AES_GCM);
let cryptor = Cryptor::from(encryption_algorithm)?;
let do_skip_file_out = cmd_decrypt.skip_decrypt_file || cmd_decrypt.direct_print || cmd_decrypt.digest_file;
let path_out = &path_display[0..path_display.len() - TINY_ENC_FILE_EXT.len()];
if !do_skip_file_out { util::require_file_not_exists(path_out)?; }
@@ -127,8 +131,8 @@ pub fn decrypt_single(config: &Option<TinyEncryptConfig>,
// debugging!("Decrypt key: {}", hex::encode(&key.0));
debugging!("Decrypt nonce: {}", hex::encode(&nonce.0));
let enc_meta = parse_encrypted_meta(&meta, &key.0, &nonce.0)?;
parse_encrypted_comment(&meta, &key.0, &nonce.0)?;
let enc_meta = parse_encrypted_meta(&meta, cryptor, &key.0, &nonce.0)?;
parse_encrypted_comment(&meta, cryptor, &key.0, &nonce.0)?;
// Decrypt to output
if cmd_decrypt.direct_print {
@@ -223,11 +227,11 @@ fn decrypt_file(file_in: &mut File, file_len: u64, file_out: &mut impl Write,
Ok(total_len)
}
fn parse_encrypted_comment(meta: &TinyEncryptMeta, key: &[u8], nonce: &[u8]) -> XResult<()> {
fn parse_encrypted_comment(meta: &TinyEncryptMeta, crypto: Cryptor, key: &[u8], nonce: &[u8]) -> XResult<()> {
if let Some(encrypted_comment) = &meta.encrypted_comment {
match util::decode_base64(encrypted_comment) {
Err(e) => warning!("Decode encrypted comment failed: {}", e),
Ok(ec_bytes) => match try_aes_gcm_decrypt_with_salt(key, nonce, SALT_COMMENT, &ec_bytes) {
Ok(ec_bytes) => match crypto_simple::try_decrypt_with_salt(crypto, key, nonce, SALT_COMMENT, &ec_bytes) {
Err(e) => warning!("Decrypt encrypted comment failed: {}", e),
Ok(decrypted_comment_bytes) => match String::from_utf8(decrypted_comment_bytes.clone()) {
Err(_) => success!("Encrypted message hex: {}", hex::encode(&decrypted_comment_bytes)),
@@ -239,14 +243,14 @@ fn parse_encrypted_comment(meta: &TinyEncryptMeta, key: &[u8], nonce: &[u8]) ->
Ok(())
}
fn parse_encrypted_meta(meta: &TinyEncryptMeta, key: &[u8], nonce: &[u8]) -> XResult<Option<EncEncryptedMeta>> {
fn parse_encrypted_meta(meta: &TinyEncryptMeta, cryptor: Cryptor, key: &[u8], nonce: &[u8]) -> XResult<Option<EncEncryptedMeta>> {
Ok(match &meta.encrypted_meta {
None => None,
Some(enc_encrypted_meta) => {
let enc_encrypted_meta_bytes = opt_result!(
util::decode_base64(enc_encrypted_meta), "Decode enc-encrypted-meta failed: {}");
let enc_meta = opt_result!(
EncEncryptedMeta::unseal(key, nonce, &enc_encrypted_meta_bytes), "Unseal enc-encrypted-meta failed: {}");
EncEncryptedMeta::unseal(cryptor, key, nonce, &enc_encrypted_meta_bytes), "Unseal enc-encrypted-meta failed: {}");
debugging!("Encrypted meta: {:?}", enc_meta);
if let Some(filename) = &enc_meta.filename {
information!("Source filename: {}", filename);
@@ -307,7 +311,8 @@ fn try_decrypt_key_ecdh(config: &Option<TinyEncryptConfig>,
slot_id,
), "Decrypt via PIV card failed: {}");
let key = util::simple_kdf(shared_secret.as_slice());
let decrypted_key = aes_gcm_decrypt(&key, &wrap_key.nonce, &wrap_key.encrypted_data)?;
let decrypted_key = crypto_simple::decrypt(
Cryptor::Aes256Gcm, &key, &wrap_key.nonce, &wrap_key.encrypted_data)?;
util::zeroize(key);
util::zeroize(shared_secret);
Ok(decrypted_key)
@@ -329,7 +334,8 @@ fn try_decrypt_key_ecdh_pgp_x25519(envelop: &TinyEncryptEnvelop, pin: &Option<St
let shared_secret = trans.decipher(Cryptogram::ECDH(&epk_bytes))?;
let key = util::simple_kdf(shared_secret.as_slice());
let decrypted_key = aes_gcm_decrypt(&key, &wrap_key.nonce, &wrap_key.encrypted_data)?;
let decrypted_key = crypto_simple::decrypt(
Cryptor::Aes256Gcm, &key, &wrap_key.nonce, &wrap_key.encrypted_data)?;
util::zeroize(key);
util::zeroize(shared_secret);
Ok(decrypted_key)

View File

@@ -11,14 +11,14 @@ use rust_util::{debugging, failure, iff, information, opt_result, simple_error,
use rust_util::util_time::UnixEpochTime;
use zeroize::Zeroize;
use crate::{util, util_enc_file, util_p256, util_p384, util_x25519};
use crate::{consts, crypto_simple, util, util_enc_file, util_p256, util_p384, util_x25519};
use crate::compress::GzStreamEncoder;
use crate::config::{TinyEncryptConfig, TinyEncryptConfigEnvelop};
use crate::consts::{
ENC_AES256_GCM_P256, ENC_AES256_GCM_P384, ENC_AES256_GCM_X25519,
SALT_COMMENT, TINY_ENC_CONFIG_FILE, TINY_ENC_FILE_EXT,
};
use crate::crypto_aes::{aes_gcm_encrypt, aes_gcm_encrypt_with_salt};
use crate::crypto_cryptor::Cryptor;
use crate::crypto_rsa::parse_spki;
use crate::spec::{
EncEncryptedMeta, EncMetadata, TINY_ENCRYPT_VERSION_10,
@@ -58,6 +58,9 @@ pub struct CmdEncrypt {
/// Disable compress meta
#[arg(long)]
pub disable_compress_meta: bool,
/// Encryption algorithm (AES/GCM, CHACHA20/POLY1305 or AES, CHACHA20, default AES/GCM)
#[arg(long, short = 'A')]
pub encryption_algorithm: Option<String>,
}
pub fn encrypt(cmd_encrypt: CmdEncrypt) -> XResult<()> {
@@ -116,6 +119,16 @@ fn encrypt_single(path: &PathBuf, envelops: &[&TinyEncryptConfigEnvelop], cmd_en
return Ok(0);
}
let encryption_algorithm = cmd_encrypt.encryption_algorithm.as_ref()
.map(String::as_str).unwrap_or(consts::TINY_ENC_AES_GCM)
.to_lowercase();
let cryptor = match encryption_algorithm.as_str() {
"aes" | "aes/gcm" => Cryptor::Aes256Gcm,
"chacha20" | "chacha20/poly1305" => Cryptor::ChaCha20Poly1305,
_ => return simple_error!("Unknown encryption algorithm: {}, should be AES or CHACHA20", encryption_algorithm),
};
information!("Using encryption algorithm: {}", cryptor.get_name());
util::require_file_exists(path)?;
let mut file_in = opt_result!(File::open(path), "Open file: {} failed: {}", &path_display);
@@ -129,7 +142,8 @@ fn encrypt_single(path: &PathBuf, envelops: &[&TinyEncryptConfigEnvelop], cmd_en
let encrypted_comment = match &cmd_encrypt.encrypted_comment {
None => None,
Some(encrypted_comment) => Some(util::encode_base64(
&aes_gcm_encrypt_with_salt(&key.0, &nonce.0, SALT_COMMENT, encrypted_comment.as_bytes())?))
&crypto_simple::encrypt_with_salt(
cryptor, &key.0, &nonce.0, SALT_COMMENT, encrypted_comment.as_bytes())?))
};
let file_metadata = opt_result!(fs::metadata(path), "Read file: {} meta failed: {}", path.display());
@@ -138,7 +152,8 @@ fn encrypt_single(path: &PathBuf, envelops: &[&TinyEncryptConfigEnvelop], cmd_en
c_time: file_metadata.created().ok().and_then(|t| t.to_millis()),
m_time: file_metadata.modified().ok().and_then(|t| t.to_millis()),
};
let enc_encrypted_meta_bytes = opt_result!(enc_encrypted_meta.seal(&key.0, &nonce.0), "Seal enc-encrypted-meta failed: {}");
let enc_encrypted_meta_bytes = opt_result!(enc_encrypted_meta.seal(
cryptor, &key.0, &nonce.0), "Seal enc-encrypted-meta failed: {}");
let enc_metadata = EncMetadata {
comment: cmd_encrypt.comment.clone(),
@@ -147,11 +162,21 @@ fn encrypt_single(path: &PathBuf, envelops: &[&TinyEncryptConfigEnvelop], cmd_en
compress: cmd_encrypt.compress,
};
let mut encrypt_meta = TinyEncryptMeta::new(&file_metadata, &enc_metadata, &nonce.0, envelops);
let mut encrypt_meta = TinyEncryptMeta::new(
&file_metadata, &enc_metadata, cryptor, &nonce.0, envelops);
debugging!("Encrypted meta: {:?}", encrypt_meta);
if cmd_encrypt.compatible_with_1_0 {
encrypt_meta = process_compatible_with_1_0(cmd_encrypt, encrypt_meta)?;
if !cmd_encrypt.disable_compress_meta {
return simple_error!("Compatible with 1.0 mode must turns --disable-compress-meta on.");
}
if let Cryptor::Aes256Gcm = Cryptor::Aes256Gcm {} else {
return simple_error!("Compatible with 1.0 mode must use AES/GCM.");
}
encrypt_meta = process_compatible_with_1_0(encrypt_meta)?;
if encrypt_meta.pgp_envelop.is_none() && encrypt_meta.ecdh_envelop.is_none() {
return simple_error!("Compatible with 1.0 mode must contains PGP or ECDH Envelop.");
}
}
let mut file_out = File::create(&path_out)?;
@@ -172,10 +197,7 @@ fn encrypt_single(path: &PathBuf, envelops: &[&TinyEncryptConfigEnvelop], cmd_en
Ok(file_metadata.len())
}
fn process_compatible_with_1_0(cmd_encrypt: &CmdEncrypt, mut encrypt_meta: TinyEncryptMeta) -> XResult<TinyEncryptMeta> {
if !cmd_encrypt.disable_compress_meta {
return simple_error!("Compatible with 1.0 mode must turns --disable-compress-meta on.");
}
fn process_compatible_with_1_0(mut encrypt_meta: TinyEncryptMeta) -> XResult<TinyEncryptMeta> {
if let Some(envelops) = encrypt_meta.envelops {
let mut filter_envelops = vec![];
for envelop in envelops {
@@ -309,7 +331,8 @@ fn encrypt_envelop_shared_secret(key: &[u8],
let shared_key = util::simple_kdf(shared_secret);
let (_, nonce) = util::make_key256_and_nonce();
let encrypted_key = aes_gcm_encrypt(&shared_key, &nonce.0, key)?;
let encrypted_key = crypto_simple::encrypt(
Cryptor::Aes256Gcm, &shared_key, &nonce.0, key)?;
let wrap_key = WrapKey {
header: WrapKeyHeader {

View File

@@ -17,7 +17,7 @@ pub struct CmdInfo {
/// File
pub paths: Vec<PathBuf>,
/// Show raw meta
#[arg(long, default_value_t = false)]
#[arg(long, short = 'R', default_value_t = false)]
pub raw_meta: bool,
}

View File

@@ -1,89 +0,0 @@
use rust_util::{opt_result, XResult};
pub fn try_aes_gcm_decrypt_with_salt(key: &[u8], nonce: &[u8], salt: &[u8], message: &[u8]) -> XResult<Vec<u8>> {
let new_nonce = build_salted_nonce(nonce, salt);
if let Ok(decrypted) = aes_gcm_decrypt(key, &new_nonce, message) {
return Ok(decrypted);
}
aes_gcm_decrypt(key, nonce, message)
}
pub fn aes_gcm_decrypt(key: &[u8], nonce: &[u8], message: &[u8]) -> XResult<Vec<u8>> {
Ok(opt_result!(aes_gcm_stream::aes_256_gcm_decrypt(key, nonce, message), "Bad key or cipher text: {}"))
}
pub fn aes_gcm_encrypt_with_salt(key: &[u8], nonce: &[u8], salt: &[u8], message: &[u8]) -> XResult<Vec<u8>> {
let new_nonce = build_salted_nonce(nonce, salt);
aes_gcm_encrypt(key, &new_nonce, message)
}
pub fn aes_gcm_encrypt(key: &[u8], nonce: &[u8], message: &[u8]) -> XResult<Vec<u8>> {
Ok(opt_result!(aes_gcm_stream::aes_256_gcm_encrypt(key, nonce, message), "Bad key length: {}"))
}
fn build_salted_nonce(nonce: &[u8], salt: &[u8]) -> Vec<u8> {
let mut nonce_with_salt = nonce.to_vec();
nonce_with_salt.extend_from_slice(salt);
let input = hex::decode(sha256::digest(nonce_with_salt)).unwrap();
input[0..12].to_vec()
}
#[test]
fn test_aes_gcm_01() {
use aes_gcm_stream::Aes256GcmStreamEncryptor;
let data_key = hex::decode("0001020304050607080910111213141516171819202122232425262728293031").unwrap();
let nonce = hex::decode("000102030405060708091011").unwrap();
let plain_text1 = "Hello world!".as_bytes();
let encrypted_text1 = "dce9511866417cff5123fa08c9e92cf156c5fc8bf6108ff28816fb58";
let plain_text2 = "This is a test message.".as_bytes();
let encrypted_text2 = "c0e45407290878b0426fea4c09597ce323b056f975c63cce6c8da516c2a78c7d71b590c869cf92";
let key256: [u8; 32] = data_key.as_slice().try_into().unwrap();
{
let mut encryptor = Aes256GcmStreamEncryptor::new(key256.clone(), &nonce);
let mut encrypted = encryptor.update(plain_text1);
let (last_block, tag) = encryptor.finalize();
encrypted.extend_from_slice(&last_block);
encrypted.extend_from_slice(&tag);
assert_eq!(encrypted_text1, hex::encode(&encrypted));
}
{
let mut encryptor = Aes256GcmStreamEncryptor::new(key256.clone(), &nonce);
let mut encrypted = encryptor.update(plain_text2);
let (last_block, tag) = encryptor.finalize();
encrypted.extend_from_slice(&last_block);
encrypted.extend_from_slice(&tag);
assert_eq!(encrypted_text2, hex::encode(&encrypted));
}
}
#[test]
fn test_aes_gcm_02() {
use aes_gcm_stream::Aes256GcmStreamDecryptor;
let data_key = hex::decode("aa01020304050607080910111213141516171819202122232425262728293031").unwrap();
let nonce = hex::decode("aa0102030405060708091011").unwrap();
let plain_text1 = hex::encode("Hello world!".as_bytes());
let encrypted_text1 = hex::decode("42b625d2bacb8a514076f14002f02770e9ccd98c90e556dc267aca30").unwrap();
let plain_text2 = hex::encode("This is a test message.".as_bytes());
let encrypted_text2 = hex::decode("5ebb20cdf5828e1e533ae1043ce6703cfa51574a83a069700aedefdbe2c735b01b74da214cba4a").unwrap();
let key256: [u8; 32] = data_key.as_slice().try_into().unwrap();
{
let mut decryptor = Aes256GcmStreamDecryptor::new(key256.clone(), &nonce);
let mut plain_text = decryptor.update(encrypted_text1.as_slice());
let last_block = decryptor.finalize().unwrap();
plain_text.extend_from_slice(&last_block);
assert_eq!(plain_text1, hex::encode(&plain_text));
}
{
let mut decryptor = Aes256GcmStreamDecryptor::new(key256.clone(), &nonce);
let mut plain_text = decryptor.update(encrypted_text2.as_slice());
let last_block = decryptor.finalize().unwrap();
plain_text.extend_from_slice(&last_block);
assert_eq!(plain_text2, hex::encode(&plain_text));
}
}

View File

@@ -20,6 +20,14 @@ impl Cryptor {
}
}
pub fn get_name(&self) -> String {
let name = match self {
Cryptor::Aes256Gcm => consts::TINY_ENC_AES_GCM,
Cryptor::ChaCha20Poly1305 => consts::TINY_ENC_CHACHA20_POLY1305,
};
name.to_string()
}
pub fn encryptor(self, key: &[u8], nonce: &[u8]) -> XResult<Box<dyn Encryptor>> {
get_encryptor(self, key, nonce)
}

31
src/crypto_simple.rs Normal file
View File

@@ -0,0 +1,31 @@
use rust_util::XResult;
use crate::crypto_cryptor::Cryptor;
pub fn try_decrypt_with_salt(crypto: Cryptor, key: &[u8], nonce: &[u8], salt: &[u8], message: &[u8]) -> XResult<Vec<u8>> {
let new_nonce = build_salted_nonce(nonce, salt);
if let Ok(decrypted) = decrypt(crypto, key, &new_nonce, message) {
return Ok(decrypted);
}
decrypt(crypto, key, nonce, message)
}
pub fn decrypt(crypto: Cryptor, key: &[u8], nonce: &[u8], message: &[u8]) -> XResult<Vec<u8>> {
crypto.decryptor(key, nonce)?.decrypt(message)
}
pub fn encrypt_with_salt(crypto: Cryptor, key: &[u8], nonce: &[u8], salt: &[u8], message: &[u8]) -> XResult<Vec<u8>> {
let new_nonce = build_salted_nonce(nonce, salt);
encrypt(crypto, key, &new_nonce, message)
}
pub fn encrypt(crypto: Cryptor, key: &[u8], nonce: &[u8], message: &[u8]) -> XResult<Vec<u8>> {
Ok(crypto.encryptor(key, nonce)?.encrypt(message))
}
fn build_salted_nonce(nonce: &[u8], salt: &[u8]) -> Vec<u8> {
let mut nonce_with_salt = nonce.to_vec();
nonce_with_salt.extend_from_slice(salt);
let input = hex::decode(sha256::digest(nonce_with_salt)).unwrap();
input[0..12].to_vec()
}

View File

@@ -21,7 +21,7 @@ mod util_x25519;
mod compress;
mod config;
mod spec;
mod crypto_aes;
mod crypto_simple;
mod crypto_rsa;
mod crypto_cryptor;
mod wrap_key;

View File

@@ -5,8 +5,9 @@ use rust_util::{opt_result, util_time, XResult};
use rust_util::util_time::get_millis;
use serde::{Deserialize, Serialize};
use crate::{compress, crypto_aes};
use crate::consts::{SALT_META, TINY_ENC_AES_GCM};
use crate::{compress, crypto_simple};
use crate::consts::SALT_META;
use crate::crypto_cryptor::Cryptor;
use crate::util::{encode_base64, get_user_agent};
pub const TINY_ENCRYPT_VERSION_10: &str = "1.0";
@@ -104,21 +105,21 @@ pub struct EncEncryptedMeta {
}
impl EncEncryptedMeta {
pub fn unseal(key: &[u8], nonce: &[u8], message: &[u8]) -> XResult<Self> {
let mut decrypted = opt_result!(crypto_aes::try_aes_gcm_decrypt_with_salt(
key, nonce, SALT_META, message), "Decrypt failed: {}");
pub fn unseal(crypto: Cryptor, key: &[u8], nonce: &[u8], message: &[u8]) -> XResult<Self> {
let mut decrypted = opt_result!(crypto_simple::try_decrypt_with_salt(
crypto, key, nonce, SALT_META, message), "Decrypt failed: {}");
decrypted = opt_result!(compress::decompress(&decrypted), "Decode faield: {}");
let meta = opt_result!(
serde_json::from_slice::<Self>(&decrypted), "Parse failed: {}");
Ok(meta)
}
pub fn seal(&self, key: &[u8], nonce: &[u8]) -> XResult<Vec<u8>> {
pub fn seal(&self, crypto: Cryptor, key: &[u8], nonce: &[u8]) -> XResult<Vec<u8>> {
let mut encrypted_meta_json = serde_json::to_vec(self).unwrap();
encrypted_meta_json = opt_result!(
compress::compress(Compression::default(), &encrypted_meta_json), "Compress failed: {}");
let encrypted = opt_result!(crypto_aes::aes_gcm_encrypt_with_salt(
key, nonce, SALT_META, encrypted_meta_json.as_slice()), "Encrypt failed: {}");
let encrypted = opt_result!(crypto_simple::encrypt_with_salt(
crypto, key, nonce, SALT_META, encrypted_meta_json.as_slice()), "Encrypt failed: {}");
Ok(encrypted)
}
}
@@ -131,7 +132,7 @@ pub struct EncMetadata {
}
impl TinyEncryptMeta {
pub fn new(metadata: &Metadata, enc_metadata: &EncMetadata, nonce: &[u8], envelops: Vec<TinyEncryptEnvelop>) -> Self {
pub fn new(metadata: &Metadata, enc_metadata: &EncMetadata, cryptor: Cryptor, nonce: &[u8], envelops: Vec<TinyEncryptEnvelop>) -> Self {
TinyEncryptMeta {
version: TINY_ENCRYPT_VERSION_11.to_string(),
created: util_time::get_current_millis() as u64,
@@ -147,7 +148,7 @@ impl TinyEncryptMeta {
ecdh_point: None,
envelop: None,
envelops: Some(envelops),
encryption_algorithm: Some(TINY_ENC_AES_GCM.to_string()),
encryption_algorithm: Some(cryptor.get_name()),
nonce: encode_base64(nonce),
file_length: metadata.len(),
file_last_modified: match metadata.modified() {