feat: v1.1.0, add static x25519 support

This commit is contained in:
2023-12-08 21:36:03 +08:00
parent 883ed0918b
commit fd7e8d35a6
12 changed files with 209 additions and 39 deletions

View File

@@ -9,7 +9,7 @@ use std::time::{Instant, SystemTime};
use clap::Args;
use flate2::Compression;
use openpgp_card::crypto_data::Cryptogram;
use rust_util::{debugging, failure, iff, information, opt_result, println_ex, simple_error, success, util_cmd, util_msg, util_size, util_time, warning, XResult};
use rust_util::{debugging, failure, iff, information, opt_result, opt_value_result, println_ex, simple_error, success, util_cmd, util_msg, util_size, util_time, warning, XResult};
use rust_util::util_time::UnixEpochTime;
use x509_parser::prelude::FromDer;
use x509_parser::x509::SubjectPublicKeyInfo;
@@ -18,6 +18,8 @@ use yubikey::YubiKey;
use zeroize::Zeroize;
use crate::{cmd_encrypt, consts, crypto_simple, util, util_enc_file, util_env, util_envelop, util_file, util_pgp, util_piv};
#[cfg(feature = "macos")]
use crate::util_keychainpasskey;
use crate::compress::GzStreamDecoder;
use crate::config::TinyEncryptConfig;
use crate::consts::{
@@ -61,9 +63,12 @@ pub struct CmdDecrypt {
/// Digest file
#[arg(long, short = 'D')]
pub digest_file: bool,
// Edit file
/// Edit file
#[arg(long, short = 'E')]
pub edit_file: bool,
// Readonly
#[arg(long)]
pub readonly: bool,
/// Digest algorithm (sha1, sha256[default], sha384, sha512 ...)
#[arg(long, short = 'A')]
pub digest_algorithm: Option<String>,
@@ -196,6 +201,10 @@ pub fn decrypt_single(config: &Option<TinyEncryptConfig>,
let do_edit_file = || -> XResult<()> {
let temp_file_content_bytes = run_file_editor_and_wait_content(&editor, &temp_file, secure_editor, &temp_encryption_key_nonce)?;
if cmd_decrypt.readonly {
information!("Readonly, do not check temp file is changed.");
return Ok(());
}
let temp_file_content_bytes = if secure_editor {
let mut decryptor = temp_cryptor.decryptor(&temp_key_nonce)?;
decryptor.decrypt(&temp_file_content_bytes)?
@@ -421,6 +430,8 @@ pub fn try_decrypt_key(config: &Option<TinyEncryptConfig>,
match envelop.r#type {
TinyEncryptEnvelopType::Pgp => try_decrypt_key_pgp(envelop, pin),
TinyEncryptEnvelopType::PgpX25519 => try_decrypt_key_ecdh_pgp_x25519(envelop, pin),
#[cfg(feature = "macos")]
TinyEncryptEnvelopType::StaticX25519 => try_decrypt_key_ecdh_static_x25519(config, envelop),
TinyEncryptEnvelopType::Ecdh | TinyEncryptEnvelopType::EcdhP384 => try_decrypt_key_ecdh(config, envelop, pin, slot),
unknown_type => simple_error!("Unknown or unsupported type: {}", unknown_type.get_name()),
}
@@ -491,6 +502,36 @@ fn try_decrypt_key_ecdh_pgp_x25519(envelop: &TinyEncryptEnvelop, pin: &Option<St
Ok(decrypted_key)
}
#[cfg(feature = "macos")]
fn try_decrypt_key_ecdh_static_x25519(config: &Option<TinyEncryptConfig>, envelop: &TinyEncryptEnvelop) -> XResult<Vec<u8>> {
let wrap_key = WrapKey::parse(&envelop.encrypted_key)?;
let cryptor = match wrap_key.header.enc.as_str() {
ENC_AES256_GCM_X25519 => Cryptor::Aes256Gcm,
ENC_CHACHA20_POLY1305_X25519 => Cryptor::ChaCha20Poly1305,
_ => return simple_error!("Unsupported header enc: {}", &wrap_key.header.enc),
};
let e_pub_key_bytes = wrap_key.header.get_e_pub_key_bytes()?;
let config = opt_value_result!(config, "Tiny encrypt config is not found");
let config_envelop = opt_value_result!(
config.find_by_kid(&envelop.kid), "Cannot find config for: {}", &envelop.kid);
let config_envelop_args = opt_value_result!(&config_envelop.args, "No arguments found for: {}", &envelop.kid);
if config_envelop_args.len() < 3 {
return simple_error!("Not enough arguments for: {}", &envelop.kid);
}
let service_name = &config_envelop_args[1];
let key_name = &config_envelop_args[2];
let shared_secret = opt_result!(
util_keychainpasskey::decrypt_data(service_name, key_name, &e_pub_key_bytes), "Decrypt static x25519 failed: {}");
let key = util::simple_kdf(shared_secret.as_slice());
let key_nonce = KeyNonce { k: &key, n: &wrap_key.nonce };
let decrypted_key = crypto_simple::decrypt(
cryptor, &key_nonce, &wrap_key.encrypted_data)?;
util::zeroize(key);
util::zeroize(shared_secret);
Ok(decrypted_key)
}
fn try_decrypt_key_pgp(envelop: &TinyEncryptEnvelop, pin: &Option<String>) -> XResult<Vec<u8>> {
let mut pgp = util_pgp::get_openpgp()?;
let mut trans = opt_result!(pgp.transaction(), "Connect OpenPGP card failed: {}");
@@ -523,7 +564,9 @@ pub fn select_envelop<'a>(meta: &'a TinyEncryptMeta, key_id: &Option<String>, co
if envelops.len() == 1 {
let selected_envelop = &envelops[0];
success!("Auto selected envelop: #{} {}", 1, util_envelop::format_envelop(selected_envelop, config));
util::read_line("Press enter to continue: ");
if !selected_envelop.r#type.auto_select() {
util::read_line("Press enter to continue: ");
}
return Ok(selected_envelop);
}

View File

@@ -268,7 +268,7 @@ fn encrypt_envelops(cryptor: Cryptor, key: &[u8], envelops: &[&TinyEncryptConfig
TinyEncryptEnvelopType::Pgp => {
encrypted_envelops.push(encrypt_envelop_pgp(key, envelop)?);
}
TinyEncryptEnvelopType::PgpX25519 => {
TinyEncryptEnvelopType::PgpX25519 | TinyEncryptEnvelopType::StaticX25519 => {
encrypted_envelops.push(encrypt_envelop_ecdh_x25519(cryptor, key, envelop)?);
}
TinyEncryptEnvelopType::Ecdh => {

View File

@@ -40,6 +40,7 @@ impl Drop for CmdExecEnv {
}
pub fn exec_env(cmd_exec_env: CmdExecEnv) -> XResult<()> {
util_msg::set_logger_std_out(false);
debugging!("Cmd exec env: {:?}", cmd_exec_env);
let config = TinyEncryptConfig::load(TINY_ENC_CONFIG_FILE).ok();
if cmd_exec_env.arguments.is_empty() {

View File

@@ -1,11 +1,16 @@
use clap::Args;
use rust_util::XResult;
use rust_util::{debugging, information, opt_result, simple_error, success, XResult};
use security_framework::os::macos::keychain::SecKeychain;
use crate::config::TinyEncryptConfigEnvelop;
use crate::spec::TinyEncryptEnvelopType;
use crate::util_keychainpasskey;
#[derive(Debug, Args)]
pub struct CmdKeychainKey {
/// Keychain name, or default
#[arg(long, short = 'c')]
pub keychain_name: Option<String>,
// /// Keychain name, or default
// #[arg(long, short = 'c')]
// pub keychain_name: Option<String>,
/// Service name, or tiny-encrypt
#[arg(long, short = 's')]
pub server_name: Option<String>,
@@ -20,21 +25,38 @@ pub struct CmdKeychainKey {
#[allow(dead_code)]
const DEFAULT_SERVICE_NAME: &str = "tiny-encrypt";
#[allow(dead_code)]
pub enum KeyType {
P256,
P384,
X25519,
}
pub fn keychain_key(cmd_keychain_key: CmdKeychainKey) -> XResult<()> {
let service_name = cmd_keychain_key.server_name.as_deref().unwrap_or(DEFAULT_SERVICE_NAME);
let sec_keychain = opt_result!(SecKeychain::default(), "Get keychain failed: {}");
if sec_keychain.find_generic_password(service_name, &cmd_keychain_key.key_name).is_ok() {
return simple_error!("Static x25519 exists: {}.{}", service_name, &cmd_keychain_key.key_name);
}
let (keychain_key, public_key) = util_keychainpasskey::generate_pass_x25519_static_secret();
opt_result!(
sec_keychain.set_generic_password(service_name, &cmd_keychain_key.key_name, keychain_key.as_bytes()),
"Write static x25519 failed: {}"
);
let public_key_hex = hex::encode(public_key.as_bytes());
debugging!("Keychain key : {}", keychain_key);
success!("Keychain name: {}", &cmd_keychain_key.key_name);
success!("Public key : {}", &public_key_hex);
let config_envelop = TinyEncryptConfigEnvelop {
r#type: TinyEncryptEnvelopType::StaticX25519,
sid: Some(cmd_keychain_key.key_name.clone()),
kid: format!("keychain:{}", &public_key_hex),
desc: Some("Keychain static".to_string()),
args: Some(vec![
"".to_string(),
service_name.to_string(),
cmd_keychain_key.key_name.clone(),
]),
public_part: public_key_hex,
};
information!("Config envelop:\n{}", serde_json::to_string_pretty(&config_envelop).unwrap());
// TODO Under developing
// keychain://keychain_name?sn=service_name&kt=kp-p256&kn=key_name&fp=fingerprint
// keychain_name -> default
// service_name -> tiny-encrypt
// kt=kp-p256|kp-p384|kp-x25519 -> keypair P256, P385 or X25519
// key_name -> key name in keychain
// fingerprint -> hex(SHA256(public_key)[0..4])
pub fn keychain_key(_cmd_keychain_key: CmdKeychainKey) -> XResult<()> {
println!();
Ok(())
}

View File

@@ -42,9 +42,12 @@ pub struct TinyEncryptConfig {
#[serde(rename_all = "camelCase")]
pub struct TinyEncryptConfigEnvelop {
pub r#type: TinyEncryptEnvelopType,
#[serde(skip_serializing_if = "Option::is_none")]
pub sid: Option<String>,
pub kid: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub desc: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub args: Option<Vec<String>>,
pub public_part: String,
}

View File

@@ -1,10 +1,10 @@
pub use cmd_config::CmdConfig;
pub use cmd_config::config;
#[cfg(feature = "smartcard")]
#[cfg(feature = "decrypt")]
pub use cmd_decrypt::CmdDecrypt;
#[cfg(feature = "smartcard")]
#[cfg(feature = "decrypt")]
pub use cmd_decrypt::decrypt;
#[cfg(feature = "smartcard")]
#[cfg(feature = "decrypt")]
pub use cmd_decrypt::decrypt_single;
pub use cmd_directdecrypt::CmdDirectDecrypt;
pub use cmd_directdecrypt::direct_decrypt;
@@ -21,7 +21,9 @@ pub use cmd_version::version;
pub use cmd_initkeychainkey::CmdKeychainKey;
#[cfg(feature = "macos")]
pub use cmd_initkeychainkey::keychain_key;
#[cfg(feature = "decrypt")]
pub use cmd_execenv::CmdExecEnv;
#[cfg(feature = "decrypt")]
pub use cmd_execenv::exec_env;
@@ -30,9 +32,9 @@ mod util;
mod util_env;
mod util_digest;
mod util_progress;
#[cfg(feature = "smartcard")]
#[cfg(feature = "decrypt")]
mod util_piv;
#[cfg(feature = "smartcard")]
#[cfg(feature = "decrypt")]
mod util_pgp;
mod util_p256;
mod util_p384;
@@ -50,11 +52,14 @@ mod util_enc_file;
mod cmd_version;
mod cmd_config;
mod cmd_info;
#[cfg(feature = "smartcard")]
#[cfg(feature = "decrypt")]
mod cmd_decrypt;
mod cmd_encrypt;
mod cmd_directdecrypt;
#[cfg(feature = "macos")]
mod cmd_initkeychainkey;
#[cfg(feature = "macos")]
mod util_keychainpasskey;
#[cfg(feature = "decrypt")]
mod cmd_execenv;

View File

@@ -3,9 +3,11 @@ extern crate core;
use clap::{Parser, Subcommand};
use rust_util::XResult;
use tiny_encrypt::{CmdConfig, CmdDirectDecrypt, CmdEncrypt, CmdExecEnv, CmdInfo, CmdVersion};
#[cfg(feature = "smartcard")]
use tiny_encrypt::{CmdConfig, CmdDirectDecrypt, CmdEncrypt, CmdInfo, CmdVersion};
#[cfg(feature = "decrypt")]
use tiny_encrypt::CmdDecrypt;
#[cfg(feature = "decrypt")]
use tiny_encrypt::CmdExecEnv;
#[cfg(feature = "macos")]
use tiny_encrypt::CmdKeychainKey;
@@ -22,7 +24,7 @@ enum Commands {
/// Encrypt file(s)
#[command(arg_required_else_help = true, short_flag = 'e')]
Encrypt(CmdEncrypt),
#[cfg(feature = "smartcard")]
#[cfg(feature = "decrypt")]
/// Decrypt file(s)
#[command(arg_required_else_help = true, short_flag = 'd')]
Decrypt(CmdDecrypt),
@@ -36,6 +38,7 @@ enum Commands {
/// Keychain Key [pending implementation]
#[command(arg_required_else_help = true, short_flag = 'k')]
KeychainKey(CmdKeychainKey),
#[cfg(feature = "decrypt")]
/// Execute env
#[command(arg_required_else_help = true, short_flag = 'X')]
ExecEnv(CmdExecEnv),
@@ -51,12 +54,13 @@ fn main() -> XResult<()> {
let args = Cli::parse();
match args.command {
Commands::Encrypt(cmd_encrypt) => tiny_encrypt::encrypt(cmd_encrypt),
#[cfg(feature = "smartcard")]
#[cfg(feature = "decrypt")]
Commands::Decrypt(cmd_decrypt) => tiny_encrypt::decrypt(cmd_decrypt),
Commands::DirectDecrypt(cmd_direct_decrypt) => tiny_encrypt::direct_decrypt(cmd_direct_decrypt),
Commands::Info(cmd_info) => tiny_encrypt::info(cmd_info),
#[cfg(feature = "macos")]
Commands::KeychainKey(cmd_keychain_key) => tiny_encrypt::keychain_key(cmd_keychain_key),
#[cfg(feature = "decrypt")]
Commands::ExecEnv(cmd_exec_env) => tiny_encrypt::exec_env(cmd_exec_env),
Commands::Version(cmd_version) => tiny_encrypt::version(cmd_version),
Commands::Config(cmd_config) => tiny_encrypt::config(cmd_config),

View File

@@ -71,6 +71,9 @@ pub enum TinyEncryptEnvelopType {
// OpenPGP X25519
#[serde(rename = "pgp-x25519")]
PgpX25519,
// Static X25519 (less secure)
#[serde(rename = "static-x25519")]
StaticX25519,
// Age, tiny-encrypt-rs is not supported
#[serde(rename = "age")]
Age,
@@ -89,16 +92,30 @@ impl TinyEncryptEnvelopType {
pub fn get_upper_name(&self) -> String {
self.get_name().to_uppercase()
}
pub fn get_name(&self) -> &'static str {
match self {
TinyEncryptEnvelopType::Pgp => "pgp",
TinyEncryptEnvelopType::PgpX25519 => "pgp-x25519",
TinyEncryptEnvelopType::StaticX25519 => "static-x25519",
TinyEncryptEnvelopType::Age => "age",
TinyEncryptEnvelopType::Ecdh => "ecdh",
TinyEncryptEnvelopType::EcdhP384 => "ecdh-p384",
TinyEncryptEnvelopType::Kms => "kms",
}
}
pub fn auto_select(&self) -> bool {
match self {
TinyEncryptEnvelopType::Pgp => false,
TinyEncryptEnvelopType::PgpX25519 => false,
TinyEncryptEnvelopType::StaticX25519 => true,
TinyEncryptEnvelopType::Age => false,
TinyEncryptEnvelopType::Ecdh => false,
TinyEncryptEnvelopType::EcdhP384 => false,
TinyEncryptEnvelopType::Kms => true,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]

View File

@@ -26,7 +26,7 @@ fn get_envelop_desc(envelop: &TinyEncryptEnvelop, config_envelop: &Option<&TinyE
}
pub fn with_width_type(s: &str) -> String {
with_width(s, 10)
with_width(s, 13)
}
pub fn with_width(s: &str, width: usize) -> String {

View File

@@ -0,0 +1,75 @@
use rust_util::{opt_result, simple_error, XResult};
use security_framework::os::macos::keychain::SecKeychain;
use x25519_dalek::{PublicKey, StaticSecret};
use zeroize::Zeroize;
const X2559_PLAIN_PREFIX: &str = "x25519-plain:";
pub struct X25519StaticSecret {
pub secret: Vec<u8>,
}
impl Zeroize for X25519StaticSecret {
fn zeroize(&mut self) {
self.secret.zeroize();
}
}
impl X25519StaticSecret {
pub fn parse(key: &str) -> XResult<Self> {
if !key.starts_with(X2559_PLAIN_PREFIX) {
return simple_error!("Not X25519 plain key");
}
let extract_key_hex = &key[X2559_PLAIN_PREFIX.len()..];
let extract_key = opt_result!(hex::decode(extract_key_hex), "Decode X25519 plain key failed: {}");
Ok(Self {
secret: extract_key,
})
}
pub fn to_str(&self) -> String {
let mut v = String::new();
v.push_str(X2559_PLAIN_PREFIX);
v.push_str(&hex::encode(&self.secret));
v
}
pub fn from_bytes(bytes: &[u8]) -> Self {
Self {
secret: bytes.to_vec(),
}
}
pub fn to_static_secret(&self) -> XResult<StaticSecret> {
let secret_slice = self.secret.as_slice();
let mut inner_secret: [u8; 32] = opt_result!(secret_slice.try_into(), "X25519 secret key error: {}");
let static_secret = StaticSecret::from(inner_secret);
inner_secret.zeroize();
Ok(static_secret)
}
}
pub fn decrypt_data(service_name: &str, key_name: &str, ephemeral_public_key_bytes: &[u8]) -> XResult<Vec<u8>> {
let sec_keychain = opt_result!(SecKeychain::default(), "Get keychain failed: {}");
let (static_x25519, _) = opt_result!(sec_keychain.find_generic_password(service_name, key_name),
"Cannot find static x25519 {}.{}: {}", service_name, key_name);
let static_x25519_bytes = static_x25519.as_ref();
let static_x25519_str = opt_result!(String::from_utf8(static_x25519_bytes.to_vec()), "Parse static x25519 failed: {}");
let x25519_static_secret = X25519StaticSecret::parse(&static_x25519_str)?;
let static_secret = x25519_static_secret.to_static_secret()?;
let inner_ephemeral_public_key: [u8; 32] = opt_result!(
ephemeral_public_key_bytes.try_into(), "X25519 public key error: {}");
let ephemeral_public_key = PublicKey::from(inner_ephemeral_public_key);
let shared_secret = static_secret.diffie_hellman(&ephemeral_public_key);
Ok(shared_secret.as_bytes().to_vec())
}
pub fn generate_pass_x25519_static_secret() -> (String, PublicKey) {
let static_secret = StaticSecret::random();
let public_key: PublicKey = (&static_secret).into();
let static_secret_bytes = static_secret.as_bytes();
let x25519_static_secret = X25519StaticSecret::from_bytes(static_secret_bytes);
(x25519_static_secret.to_str(), public_key)
}