Skip to content

Commit

Permalink
feat: v1.2.0, support macos secure enclave
Browse files Browse the repository at this point in the history
  • Loading branch information
jht5945 committed Dec 9, 2023
1 parent 369dae2 commit 9baf23d
Show file tree
Hide file tree
Showing 10 changed files with 184 additions and 27 deletions.
12 changes: 11 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "tiny-encrypt"
version = "1.1.2"
version = "1.2.0"
edition = "2021"
license = "MIT"
description = "A simple and tiny file encrypt tool"
Expand All @@ -9,9 +9,10 @@ repository = "https://git.hatter.ink/hatter/tiny-encrypt-rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[features]
default = ["decrypt", "macos"]
default = ["decrypt", "macos", "secure-enclave"]
decrypt = ["openpgp-card", "openpgp-card-pcsc", "yubikey"]
macos = ["security-framework"]
secure-enclave = ["macos", "swift-rs"]

[dependencies]
aes-gcm-stream = "0.2"
Expand Down Expand Up @@ -42,6 +43,10 @@ x25519-dalek = { version = "2.0", features = ["static_secrets", "getrandom"] }
x509-parser = "0.15"
yubikey = { version = "0.8", features = ["untested"], optional = true }
zeroize = "1.7"
swift-rs = { path = "swift-rs", optional = true }

[build-dependencies]
swift-rs = { path = "swift-rs", features = ["build"], optional = true }

[patch.crates-io]
rust-crypto = { git = "https://github.com/jht5945/rust-crypto.git" }
Expand Down
37 changes: 37 additions & 0 deletions src/cmd_decrypt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ use crate::util::SecVec;
use crate::util_digest::DigestWrite;
#[cfg(feature = "macos")]
use crate::util_keychainstatic;
#[cfg(feature = "secure-enclave")]
use crate::util_keychainkey;
use crate::util_progress::Progress;
use crate::wrap_key::WrapKey;

Expand Down Expand Up @@ -435,6 +437,8 @@ pub fn try_decrypt_key(config: &Option<TinyEncryptConfig>,
#[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),
#[cfg(feature = "secure-enclave")]
TinyEncryptEnvelopType::KeyP256 => try_decrypt_se_key_ecdh(config, envelop),
unknown_type => simple_error!("Unknown or unsupported type: {}", unknown_type.get_name()),
}
}
Expand Down Expand Up @@ -479,6 +483,39 @@ fn try_decrypt_key_ecdh(config: &Option<TinyEncryptConfig>,
Ok(decrypted_key)
}

#[cfg(feature = "secure-enclave")]
fn try_decrypt_se_key_ecdh(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_P256 => Cryptor::Aes256Gcm,
ENC_CHACHA20_POLY1305_P256 => 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.is_empty() {
return simple_error!("Not enough arguments for: {}", &envelop.kid);
}
let private_key_base64 = &config_envelop_args[0];

let shared_secret = opt_result!(util_keychainkey::decrypt_data(
private_key_base64,
&e_pub_key_bytes
), "Decrypt via secure enclave 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_ecdh_pgp_x25519(envelop: &TinyEncryptEnvelop, pin: &Option<String>) -> XResult<Vec<u8>> {
let wrap_key = WrapKey::parse(&envelop.encrypted_key)?;
let cryptor = match wrap_key.header.enc.as_str() {
Expand Down
2 changes: 1 addition & 1 deletion src/cmd_encrypt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ fn encrypt_envelops(cryptor: Cryptor, key: &[u8], envelops: &[&TinyEncryptConfig
TinyEncryptEnvelopType::PgpX25519 | TinyEncryptEnvelopType::StaticX25519 => {
encrypted_envelops.push(encrypt_envelop_ecdh_x25519(cryptor, key, envelop)?);
}
TinyEncryptEnvelopType::Ecdh => {
TinyEncryptEnvelopType::Ecdh | TinyEncryptEnvelopType::KeyP256 => {
encrypted_envelops.push(encrypt_envelop_ecdh(cryptor, key, envelop)?);
}
TinyEncryptEnvelopType::EcdhP384 => {
Expand Down
60 changes: 51 additions & 9 deletions src/cmd_initkeychainkey.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
use clap::Args;
use rust_util::{debugging, information, opt_result, simple_error, success, XResult};
use rust_util::{debugging, information, opt_result, opt_value_result, simple_error, success, XResult};
use security_framework::os::macos::keychain::SecKeychain;

use crate::config::TinyEncryptConfigEnvelop;
use crate::spec::TinyEncryptEnvelopType;
#[cfg(feature = "secure-enclave")]
use crate::util_keychainkey;
use crate::util_keychainstatic;

#[derive(Debug, Args)]
pub struct CmdKeychainKey {
/// Secure Enclave
#[arg(long, short = 'S')]
pub secure_enclave: bool,
// /// Keychain name, or default
// #[arg(long, short = 'c')]
// pub keychain_name: Option<String>,
Expand All @@ -16,39 +21,76 @@ pub struct CmdKeychainKey {
pub server_name: Option<String>,
/// Key name
#[arg(long, short = 'n')]
pub key_name: String,
pub key_name: Option<String>,
}

#[allow(dead_code)]
const DEFAULT_SERVICE_NAME: &str = "tiny-encrypt";

pub fn keychain_key(cmd_keychain_key: CmdKeychainKey) -> XResult<()> {
if cmd_keychain_key.secure_enclave {
#[cfg(feature = "secure-enclave")]
return keychain_key_se(cmd_keychain_key);
#[cfg(not(feature = "secure-enclave"))]
return simple_error!("Feature secure-enclave is not built");
} else {
keychain_key_static(cmd_keychain_key)
}
}

#[cfg(feature = "secure-enclave")]
pub fn keychain_key_se(cmd_keychain_key: CmdKeychainKey) -> XResult<()> {
if !util_keychainkey::is_support_se() {
return simple_error!("Secure enclave is not supported.");
}
let (public_key_hex, private_key_base64) = util_keychainkey::generate_se_p256_keypair()?;
let public_key_compressed_hex = public_key_hex.chars()
.skip(2).take(public_key_hex.len() / 2 - 1).collect::<String>();

let config_envelop = TinyEncryptConfigEnvelop {
r#type: TinyEncryptEnvelopType::KeyP256,
sid: cmd_keychain_key.key_name.clone(),
kid: format!("keychain:02{}", &public_key_compressed_hex),
desc: Some("Keychain Secure Enclave".to_string()),
args: Some(vec![
private_key_base64
]),
public_part: public_key_hex,
};

information!("Config envelop:\n{}", serde_json::to_string_pretty(&config_envelop).unwrap());

Ok(())
}

pub fn keychain_key_static(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 key_name = opt_value_result!(&cmd_keychain_key.key_name, "Key name is required.");
if sec_keychain.find_generic_password(service_name, key_name).is_ok() {
return simple_error!("Static x25519 exists: {}.{}", service_name, &key_name);
}

let (keychain_key, public_key) = util_keychainstatic::generate_pass_x25519_static_secret();
let (keychain_key, public_key) = util_keychainstatic::generate_static_x25519_secret();
opt_result!(
sec_keychain.set_generic_password(service_name, &cmd_keychain_key.key_name, keychain_key.as_bytes()),
sec_keychain.set_generic_password(service_name, 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!("Keychain name: {}", &key_name);
success!("Public key : {}", &public_key_hex);

let config_envelop = TinyEncryptConfigEnvelop {
r#type: TinyEncryptEnvelopType::StaticX25519,
sid: Some(cmd_keychain_key.key_name.clone()),
sid: Some(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(),
key_name.clone(),
]),
public_part: public_key_hex,
};
Expand Down
16 changes: 10 additions & 6 deletions src/cmd_version.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
use clap::Args;
use rust_util::XResult;
use rust_util::{iff, XResult};

use crate::util;
#[cfg(feature = "secure-enclave")]
use crate::util_keychainkey;

#[derive(Debug, Args)]
pub struct CmdVersion {}

pub fn version(_cmd_version: CmdVersion) -> XResult<()> {
let mut features: Vec<&str> = vec![];
let mut features: Vec<String> = vec![];
#[cfg(feature = "decrypt")]
features.push("decrypt");
features.push("decrypt".to_string());
#[cfg(feature = "macos")]
features.push("macos");
if features.is_empty() { features.push("-"); }
features.push("macos".to_string());
#[cfg(feature = "secure-enclave")]
features.push(format!("secure-enclave{}", iff!(util_keychainkey::is_support_se(), "*", "")));
if features.is_empty() { features.push("-".to_string()); }
println!(
"User-Agent: {} [ with features: {} ]\n{}",
"User-Agent: {} [with features: {}]\n{}",
util::get_user_agent(),
features.join(", "),
env!("CARGO_PKG_DESCRIPTION")
Expand Down
15 changes: 8 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,19 @@ pub use cmd_encrypt::CmdEncrypt;
pub use cmd_encrypt::encrypt;
pub use cmd_encrypt::encrypt_single;
pub use cmd_encrypt::encrypt_single_file_out;
#[cfg(feature = "decrypt")]
pub use cmd_execenv::CmdExecEnv;
#[cfg(feature = "decrypt")]
pub use cmd_execenv::exec_env;
pub use cmd_info::CmdInfo;
pub use cmd_info::info;
pub use cmd_info::info_single;
pub use cmd_version::CmdVersion;
pub use cmd_version::version;
#[cfg(feature = "macos")]
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;

pub use cmd_version::CmdVersion;
pub use cmd_version::version;

mod consts;
mod util;
Expand Down Expand Up @@ -62,4 +61,6 @@ mod cmd_initkeychainkey;
mod util_keychainstatic;
#[cfg(feature = "decrypt")]
mod cmd_execenv;
#[cfg(feature = "secure-enclave")]
mod util_keychainkey;

5 changes: 5 additions & 0 deletions src/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ pub enum TinyEncryptEnvelopType {
// Static X25519 (less secure)
#[serde(rename = "static-x25519")]
StaticX25519,
// Key P256 (Private key in Secure Enclave)
#[serde(rename = "key-p256")]
KeyP256,
// Age, tiny-encrypt-rs is not supported
#[serde(rename = "age")]
Age,
Expand All @@ -98,6 +101,7 @@ impl TinyEncryptEnvelopType {
TinyEncryptEnvelopType::Pgp => "pgp",
TinyEncryptEnvelopType::PgpX25519 => "pgp-x25519",
TinyEncryptEnvelopType::StaticX25519 => "static-x25519",
TinyEncryptEnvelopType::KeyP256 => "key-p256",
TinyEncryptEnvelopType::Age => "age",
TinyEncryptEnvelopType::Ecdh => "ecdh",
TinyEncryptEnvelopType::EcdhP384 => "ecdh-p384",
Expand All @@ -110,6 +114,7 @@ impl TinyEncryptEnvelopType {
TinyEncryptEnvelopType::Pgp => false,
TinyEncryptEnvelopType::PgpX25519 => false,
TinyEncryptEnvelopType::StaticX25519 => true,
TinyEncryptEnvelopType::KeyP256 => true,
TinyEncryptEnvelopType::Age => false,
TinyEncryptEnvelopType::Ecdh => false,
TinyEncryptEnvelopType::EcdhP384 => false,
Expand Down
53 changes: 53 additions & 0 deletions src/util_keychainkey.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use rust_util::{opt_result, simple_error, XResult};
use swift_rs::{Bool, SRString};
use swift_rs::swift;

use crate::util;

swift!(fn is_support_secure_enclave() -> Bool);
swift!(fn generate_secure_enclave_p256_keypair() -> SRString);
swift!(fn compute_secure_enclave_p256_ecdh(private_key_base64: SRString, ephemera_public_key_base64: SRString) -> SRString);

pub fn is_support_se() -> bool {
unsafe { is_support_secure_enclave() }
}


pub fn decrypt_data(private_key_base64: &str, ephemeral_public_key_bytes: &[u8]) -> XResult<Vec<u8>> {
let ephemera_public_key_base64 = util::encode_base64(ephemeral_public_key_bytes);
let result = unsafe {
compute_secure_enclave_p256_ecdh(
SRString::from(private_key_base64), SRString::from(ephemera_public_key_base64.as_str()),
)
};
let result = result.as_str();
if !result.starts_with("ok:SharedSecret:") {
return simple_error!("ECDH P256 in secure enclave failed: {}", result);
}

let shared_secret_hex = result.chars().skip("ok:SharedSecret:".len()).collect::<String>();
let shared_secret_hex = shared_secret_hex.trim();

Ok(opt_result!(hex::decode(shared_secret_hex), "Decrypt shared secret hex: {}, failed: {}", shared_secret_hex))
}

pub fn generate_se_p256_keypair() -> XResult<(String, String)> {
if !is_support_se() {
return simple_error!("Secure enclave is not supported.");
}
let result = unsafe { generate_secure_enclave_p256_keypair() };
let result = result.as_str();
if !result.starts_with("ok:") {
return simple_error!("Generate P256 in secure enclave failed: {}", result);
}
let public_key_and_private_key = result.chars().skip(3).collect::<String>();
let public_key_and_private_keys = public_key_and_private_key.split(',').collect::<Vec<_>>();
if public_key_and_private_keys.len() != 2 {
return simple_error!("Generate P256 in secure enclave result is bad: {}", public_key_and_private_key);
}
let public_key = hex::encode(
opt_result!(util::decode_base64(public_key_and_private_keys[0]), "Public key is not base64 encoded: {}"));
let private_key = public_key_and_private_keys[1].to_string();

Ok((public_key, private_key))
}
2 changes: 1 addition & 1 deletion src/util_keychainstatic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ pub fn decrypt_data(service_name: &str, key_name: &str, ephemeral_public_key_byt
Ok(shared_secret.as_bytes().to_vec())
}

pub fn generate_pass_x25519_static_secret() -> (String, PublicKey) {
pub fn generate_static_x25519_secret() -> (String, PublicKey) {
let static_secret = StaticSecret::random();
let public_key: PublicKey = (&static_secret).into();
let static_secret_bytes = static_secret.as_bytes();
Expand Down

0 comments on commit 9baf23d

Please sign in to comment.