feat: 0.3.0, supports meta compress, encrypted meta

This commit is contained in:
2023-10-12 22:38:50 +08:00
parent 097cde6b9a
commit 9cae4e987a
9 changed files with 180 additions and 61 deletions

37
Cargo.lock generated
View File

@@ -572,25 +572,14 @@ dependencies = [
[[package]]
name = "errno"
version = "0.3.4"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "add4f07d43996f76ef320709726a556a9d4f965d9410d8d0271132d2f8293480"
checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860"
dependencies = [
"errno-dragonfly",
"libc",
"windows-sys",
]
[[package]]
name = "errno-dragonfly"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "fastrand"
version = "2.0.1"
@@ -1025,9 +1014,9 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
[[package]]
name = "linux-raw-sys"
version = "0.4.9"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45786cec4d5e54a224b15cb9f06751883103a27c19c93eda09b0b4f5f08fefac"
checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f"
[[package]]
name = "log"
@@ -1448,9 +1437,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.68"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b1106fec09662ec6dd98ccac0f81cef56984d0b49f75c92d8cbad76e20c005c"
checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da"
dependencies = [
"unicode-ident",
]
@@ -1679,9 +1668,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.17"
version = "0.38.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f25469e9ae0f3d0047ca8b93fc56843f38e6774f0914a107ff8b41be8be8e0b7"
checksum = "5a74ee2d7c2581cd139b42447d7d9389b889bdaad3a73f1ebb16f2a3237bb19c"
dependencies = [
"bitflags 2.4.0",
"errno",
@@ -1800,9 +1789,9 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.19"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0"
checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090"
[[package]]
name = "serde"
@@ -2128,7 +2117,7 @@ dependencies = [
[[package]]
name = "tiny-encrypt"
version = "0.2.5"
version = "0.3.0"
dependencies = [
"aes-gcm-stream",
"base64",
@@ -2173,9 +2162,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.32.0"
version = "1.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653"
dependencies = [
"backtrace",
"bytes",

View File

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

View File

@@ -20,12 +20,9 @@ use zeroize::Zeroize;
use crate::{card, file, util, util_piv};
use crate::compress::GzStreamDecoder;
use crate::config::TinyEncryptConfig;
use crate::crypto_aes::aes_gcm_decrypt;
use crate::spec::{TinyEncryptEnvelop, TinyEncryptEnvelopType, TinyEncryptMeta};
use crate::util::{
ENC_AES256_GCM_P256, ENC_AES256_GCM_P384, ENC_AES256_GCM_X25519,
TINY_ENC_CONFIG_FILE, TINY_ENC_FILE_EXT,
};
use crate::crypto_aes::{aes_gcm_decrypt, try_aes_gcm_decrypt_with_salt};
use crate::spec::{EncEncryptedMeta, TinyEncryptEnvelop, TinyEncryptEnvelopType, TinyEncryptMeta};
use crate::util::{ENC_AES256_GCM_P256, ENC_AES256_GCM_P384, ENC_AES256_GCM_X25519, SALT_COMMENT, TINY_ENC_CONFIG_FILE, TINY_ENC_FILE_EXT};
use crate::wrap_key::WrapKey;
#[derive(Debug, Args)]
@@ -41,6 +38,9 @@ pub struct CmdDecrypt {
/// Remove source file
#[arg(long, short = 'R')]
pub remove_file: bool,
/// Skip decrypt file
#[arg(long)]
pub skip_decrypt_file: bool,
}
pub fn decrypt(cmd_decrypt: CmdDecrypt) -> XResult<()> {
@@ -105,34 +105,48 @@ pub fn decrypt_single(config: &Option<TinyEncryptConfig>,
debugging!("Decrypt key: {}", hex::encode(&key));
debugging!("Decrypt nonce: {}", hex::encode(&nonce));
if let Some(enc_encrypted_meta) = &meta.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: {}");
if let Some(filename) = &enc_meta.filename {
information!("Source filename: {}", filename);
}
}
if let Some(encrypted_comment) = &meta.encrypted_comment {
match util::decode_base64(encrypted_comment) {
Err(e) => warning!("Decode encrypted comment failed: {}", e),
Ok(encrypted_comment_based_bytes) => match aes_gcm_decrypt(&key, &nonce, &encrypted_comment_based_bytes) {
Ok(ec_bytes) => match try_aes_gcm_decrypt_with_salt(&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)),
Ok(message) => success!("Encrypted message: {}", message),
Ok(message) => success!("Encrypted comment: {}", message),
}
}
}
}
let mut file_out = File::create(path_out)?;
if cmd_decrypt.skip_decrypt_file {
information!("Decrypt file is skipped.");
} else {
let mut file_out = File::create(path_out)?;
let start = Instant::now();
util_msg::print_lastline(
&format!("Decrypting file: {}{} ...", path_display, iff!(meta.compress, " [compressed]", ""))
);
let _ = decrypt_file(&mut file_in, &mut file_out, &key, &nonce, meta.compress)?;
util_msg::clear_lastline();
let encrypt_duration = start.elapsed();
debugging!("Encrypt file: {} elapsed: {} ms", path_display, encrypt_duration.as_millis());
let start = Instant::now();
util_msg::print_lastline(
&format!("Decrypting file: {}{} ...", path_display, iff!(meta.compress, " [compressed]", ""))
);
let _ = decrypt_file(&mut file_in, &mut file_out, &key, &nonce, meta.compress)?;
util_msg::clear_lastline();
let encrypt_duration = start.elapsed();
debugging!("Encrypt file: {} elapsed: {} ms", path_display, encrypt_duration.as_millis());
drop(file_out);
}
util::zeroize(key);
util::zeroize(nonce);
drop(file_in);
drop(file_out);
if cmd_decrypt.remove_file {
match fs::remove_file(path) {
Err(e) => warning!("Remove file: {} failed: {}", path_display, e),

View File

@@ -7,16 +7,16 @@ use std::time::Instant;
use clap::Args;
use flate2::Compression;
use rsa::Pkcs1v15Encrypt;
use rust_util::{debugging, failure, information, opt_result, simple_error, success, util_msg, warning, XResult};
use rust_util::{debugging, failure, iff, information, opt_result, simple_error, success, util_msg, warning, XResult};
use zeroize::Zeroize;
use crate::{util, util_ecdh, util_p384, util_x25519};
use crate::{compress, util, util_ecdh, util_p384, util_x25519};
use crate::compress::GzStreamEncoder;
use crate::config::{TinyEncryptConfig, TinyEncryptConfigEnvelop};
use crate::crypto_aes::aes_gcm_encrypt;
use crate::crypto_aes::{aes_gcm_encrypt, aes_gcm_encrypt_with_salt};
use crate::crypto_rsa::parse_spki;
use crate::spec::{EncMetadata, TINY_ENCRYPT_VERSION_10, TinyEncryptEnvelop, TinyEncryptEnvelopType, TinyEncryptMeta};
use crate::util::{ENC_AES256_GCM_P256, ENC_AES256_GCM_P384, ENC_AES256_GCM_X25519, TINY_ENC_CONFIG_FILE};
use crate::spec::{EncEncryptedMeta, EncMetadata, TINY_ENCRYPT_VERSION_10, TinyEncryptEnvelop, TinyEncryptEnvelopType, TinyEncryptMeta};
use crate::util::{ENC_AES256_GCM_P256, ENC_AES256_GCM_P384, ENC_AES256_GCM_X25519, SALT_COMMENT, TINY_ENC_CONFIG_FILE};
use crate::wrap_key::{WrapKey, WrapKeyHeader};
#[derive(Debug, Args)]
@@ -44,6 +44,9 @@ pub struct CmdEncrypt {
/// Remove source file
#[arg(long, short = 'R')]
pub remove_file: bool,
/// Disable compress meta
#[arg(long)]
pub disable_compress_meta: bool,
}
pub fn encrypt(cmd_encrypt: CmdEncrypt) -> XResult<()> {
@@ -115,14 +118,19 @@ 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(&key, &nonce, encrypted_comment.as_bytes())?))
&aes_gcm_encrypt_with_salt(&key, &nonce, SALT_COMMENT, encrypted_comment.as_bytes())?))
};
let enc_encrypted_meta = EncEncryptedMeta {
filename: Some(util::get_file_name(path)),
};
let enc_encrypted_meta_bytes = opt_result!(enc_encrypted_meta.seal(&key, &nonce), "Seal enc-encrypted-meta failed: {}");
let file_metadata = opt_result!(fs::metadata(path), "Read file: {} meta failed: {}", path.display());
let enc_metadata = EncMetadata {
comment: cmd_encrypt.comment.clone(),
encrypted_comment,
encrypted_meta: None,
encrypted_meta: Some(util::encode_base64(&enc_encrypted_meta_bytes)),
compress: cmd_encrypt.compress,
};
@@ -150,10 +158,18 @@ fn encrypt_single(path: &PathBuf, envelops: &[&TinyEncryptConfigEnvelop], cmd_en
}
}
let compress_meta = !cmd_encrypt.disable_compress_meta;
let mut file_out = File::create(&path_out)?;
opt_result!(file_out.write_all(&util::TINY_ENC_MAGIC_TAG.to_be_bytes()), "Write tag failed: {}");
let encrypted_meta_bytes = opt_result!(serde_json::to_vec(&encrypt_meta), "Generate meta json bytes failed: {}");
let tag = iff!(compress_meta, util::TINY_ENC_COMPRESSED_MAGIC_TAG, util::TINY_ENC_MAGIC_TAG);
opt_result!(file_out.write_all(&tag.to_be_bytes()), "Write tag failed: {}");
let mut encrypted_meta_bytes = opt_result!(serde_json::to_vec(&encrypt_meta), "Generate meta json bytes failed: {}");
if compress_meta {
encrypted_meta_bytes = opt_result!(
compress::compress(Compression::default(), &encrypted_meta_bytes), "Compress encrypted meta failed: {}");
}
let encrypted_meta_bytes_len = encrypted_meta_bytes.len() as u32;
debugging!("Encrypted meta len: {}", encrypted_meta_bytes_len);
opt_result!(file_out.write_all(&encrypted_meta_bytes_len.to_be_bytes()), "Write meta len failed: {}");
opt_result!(file_out.write_all(&encrypted_meta_bytes), "Write meta failed: {}");
@@ -281,7 +297,7 @@ fn encrypt_envelop_shared_secret(key: &[u8],
let wrap_key = WrapKey {
header: WrapKeyHeader {
kid: Some(envelop.kid.clone()),
kid: None, // Some(envelop.kid.clone()),
enc: enc_type.to_string(),
e_pub_key: util::encode_base64_url_no_pad(&ephemeral_spki),
},

View File

@@ -4,6 +4,20 @@ use flate2::Compression;
use flate2::write::{GzDecoder, GzEncoder};
use rust_util::{simple_error, XResult};
pub fn compress(compression: Compression, message: &[u8]) -> XResult<Vec<u8>> {
let mut encoder = GzStreamEncoder::new(compression);
let mut buff = encoder.update(message)?;
buff.extend_from_slice(&encoder.finalize()?);
Ok(buff)
}
pub fn decompress(message: &[u8]) -> XResult<Vec<u8>> {
let mut decoder = GzStreamDecoder::new();
let mut buff = decoder.update(message)?;
buff.extend_from_slice(&decoder.finalize()?);
Ok(buff)
}
pub struct GzStreamEncoder {
gz_encoder: GzEncoder<Vec<u8>>,
}

View File

@@ -1,25 +1,48 @@
use aes_gcm_stream::{Aes256GcmStreamDecryptor, Aes256GcmStreamEncryptor};
use rust_util::{opt_result, XResult};
use zeroize::Zeroize;
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>> {
let key: [u8; 32] = opt_result!(key.try_into(), "Invalid envelop: {}");
let mut key: [u8; 32] = opt_result!(key.try_into(), "Invalid envelop: {}");
let mut aes256_gcm = Aes256GcmStreamDecryptor::new(key, nonce);
let mut b1 = aes256_gcm.update(message);
let b2 = opt_result!(aes256_gcm.finalize(), "Invalid envelop: {}");
b1.extend_from_slice(&b2);
key.zeroize();
Ok(b1)
}
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>> {
let key: [u8; 32] = opt_result!(key.try_into(), "Invalid envelop: {}");
let mut key: [u8; 32] = opt_result!(key.try_into(), "Invalid envelop: {}");
let mut aes256_gcm = Aes256GcmStreamEncryptor::new(key, nonce);
let mut b1 = aes256_gcm.update(message);
let (b2, tag) = aes256_gcm.finalize();
b1.extend_from_slice(&b2);
b1.extend_from_slice(&tag);
key.zeroize();
Ok(b1)
}
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() {
let data_key = hex::decode("0001020304050607080910111213141516171819202122232425262728293031").unwrap();

View File

@@ -1,9 +1,9 @@
use std::io::{Read, Write};
use rust_util::{opt_result, simple_error, XResult};
use rust_util::{debugging, opt_result, simple_error, XResult};
use crate::spec::TinyEncryptMeta;
use crate::util;
use crate::{compress, util};
pub fn _write_tiny_encrypt_meta<W: Write>(w: &mut W, meta: &TinyEncryptMeta) -> XResult<usize> {
let meta_json = opt_result!( serde_json::to_string(meta), "Meta to JSON failed: {}");
@@ -27,7 +27,9 @@ pub fn read_tiny_encrypt_meta<R: Read>(r: &mut R) -> XResult<TinyEncryptMeta> {
let mut tag_buff = [0_u8; 2];
opt_result!(r.read_exact(&mut tag_buff), "Read tag failed: {}");
let tag = u16::from_be_bytes(tag_buff);
if tag != util::TINY_ENC_MAGIC_TAG {
let is_normal_tiny_enc = tag == util::TINY_ENC_MAGIC_TAG;
let is_compressed_tiny_enc = tag == util::TINY_ENC_COMPRESSED_MAGIC_TAG;
if !is_normal_tiny_enc && !is_compressed_tiny_enc {
return simple_error!("Tag is not 0x01, but is: 0x{:x}", tag);
}
@@ -38,8 +40,15 @@ pub fn read_tiny_encrypt_meta<R: Read>(r: &mut R) -> XResult<TinyEncryptMeta> {
return simple_error!("Meta too large: {}", length);
}
debugging!("Encrypted meta len: {}", length);
let mut meta_buff = vec![0; length as usize];
opt_result!(r.read_exact(meta_buff.as_mut_slice()), "Read meta failed: {}");
debugging!("Tiny enc meta compressed: {}", is_compressed_tiny_enc);
if is_compressed_tiny_enc {
meta_buff = opt_result!(compress::decompress(&meta_buff), "Decompress meta failed: {}");
}
debugging!("Encrypted meta: {}", String::from_utf8_lossy(&meta_buff));
Ok(opt_result!(serde_json::from_slice(&meta_buff), "Parse meta failed: {}"))
}

View File

@@ -1,10 +1,12 @@
use std::fs::Metadata;
use flate2::Compression;
use rust_util::util_time;
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::util::{encode_base64, get_user_agent, TINY_ENC_AES_GCM};
use crate::util::{encode_base64, get_user_agent, SALT_META, TINY_ENC_AES_GCM};
pub const TINY_ENCRYPT_VERSION_10: &'static str = "1.0";
pub const TINY_ENCRYPT_VERSION_11: &'static str = "1.1";
@@ -16,19 +18,30 @@ pub struct TinyEncryptMeta {
pub version: String,
pub created: u64,
pub user_agent: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub encrypted_comment: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub encrypted_meta: Option<String>,
// ---------------------------------------
#[serde(skip_serializing_if = "Option::is_none")]
pub pgp_envelop: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pgp_fingerprint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub age_envelop: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub age_recipient: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ecdh_envelop: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ecdh_point: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub envelop: Option<String>,
// ---------------------------------------
pub envelops: Option<Vec<TinyEncryptEnvelop>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub encryption_algorithm: Option<String>,
pub nonce: String,
pub file_length: u64,
@@ -41,6 +54,7 @@ pub struct TinyEncryptMeta {
pub struct TinyEncryptEnvelop {
pub r#type: TinyEncryptEnvelopType,
pub kid: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub desc: Option<String>,
pub encrypted_key: String,
}
@@ -78,6 +92,32 @@ impl TinyEncryptEnvelopType {
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EncEncryptedMeta {
#[serde(skip_serializing_if = "Option::is_none")]
pub filename: Option<String>,
}
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: {}");
decrypted = opt_result!(compress::decompress(&decrypted), "Decode faield: {}");
let meta = opt_result!(
serde_json::from_slice::<Self>(&decrypted), "Parse failed: {}");
return Ok(meta);
}
pub fn seal(&self, 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: {}");
Ok(encrypted)
}
}
pub struct EncMetadata {
pub comment: Option<String>,
pub encrypted_comment: Option<String>,

View File

@@ -1,6 +1,6 @@
use std::{fs, io};
use std::io::Write;
use std::path::Path;
use std::path::{Path, PathBuf};
use base64::Engine;
use base64::engine::general_purpose;
@@ -17,6 +17,20 @@ pub const TINY_ENC_CONFIG_FILE: &str = "~/.tinyencrypt/config-rs.json";
pub const TINY_ENC_AES_GCM: &str = "AES/GCM";
pub const TINY_ENC_MAGIC_TAG: u16 = 0x01;
pub const TINY_ENC_COMPRESSED_MAGIC_TAG: u16 = 0x02;
pub const SALT_COMMENT: &[u8] = b"salt:comment";
pub const SALT_META: &[u8] = b"salt:meta";
pub fn get_file_name(path: &PathBuf) -> String {
let path_display = format!("{}", path.display());
if path_display.contains("/") {
if let Some(p) = path_display.split("/").last() {
return p.to_string();
}
}
path_display
}
pub fn require_tiny_enc_file_and_exists(path: impl AsRef<Path>) -> XResult<()> {
let path = path.as_ref();