feat: v1.2.0, support macos secure enclave
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -1700,7 +1700,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tiny-encrypt"
|
name = "tiny-encrypt"
|
||||||
version = "1.1.2"
|
version = "1.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm-stream",
|
"aes-gcm-stream",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "tiny-encrypt"
|
name = "tiny-encrypt"
|
||||||
version = "1.1.2"
|
version = "1.2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "A simple and tiny file encrypt tool"
|
description = "A simple and tiny file encrypt tool"
|
||||||
|
|||||||
8
justfile
8
justfile
@@ -9,5 +9,9 @@ build:
|
|||||||
build-no-features:
|
build-no-features:
|
||||||
cargo build --release --no-default-features
|
cargo build --release --no-default-features
|
||||||
|
|
||||||
compile-libse-static:
|
# Try build all
|
||||||
cd swift && swiftc se.swift -emit-module -emit-library -static
|
try-build-all:
|
||||||
|
cargo build --no-default-features
|
||||||
|
cargo build --no-default-features --features decrypt
|
||||||
|
cargo build --no-default-features --features macos
|
||||||
|
cargo build --no-default-features --features secure-enclave
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ use crate::util::SecVec;
|
|||||||
use crate::util_digest::DigestWrite;
|
use crate::util_digest::DigestWrite;
|
||||||
#[cfg(feature = "macos")]
|
#[cfg(feature = "macos")]
|
||||||
use crate::util_keychainstatic;
|
use crate::util_keychainstatic;
|
||||||
|
#[cfg(feature = "secure-enclave")]
|
||||||
|
use crate::util_keychainkey;
|
||||||
use crate::util_progress::Progress;
|
use crate::util_progress::Progress;
|
||||||
use crate::wrap_key::WrapKey;
|
use crate::wrap_key::WrapKey;
|
||||||
|
|
||||||
@@ -435,6 +437,8 @@ pub fn try_decrypt_key(config: &Option<TinyEncryptConfig>,
|
|||||||
#[cfg(feature = "macos")]
|
#[cfg(feature = "macos")]
|
||||||
TinyEncryptEnvelopType::StaticX25519 => try_decrypt_key_ecdh_static_x25519(config, envelop),
|
TinyEncryptEnvelopType::StaticX25519 => try_decrypt_key_ecdh_static_x25519(config, envelop),
|
||||||
TinyEncryptEnvelopType::Ecdh | TinyEncryptEnvelopType::EcdhP384 => try_decrypt_key_ecdh(config, envelop, pin, slot),
|
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()),
|
unknown_type => simple_error!("Unknown or unsupported type: {}", unknown_type.get_name()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -479,6 +483,39 @@ fn try_decrypt_key_ecdh(config: &Option<TinyEncryptConfig>,
|
|||||||
Ok(decrypted_key)
|
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>> {
|
fn try_decrypt_key_ecdh_pgp_x25519(envelop: &TinyEncryptEnvelop, pin: &Option<String>) -> XResult<Vec<u8>> {
|
||||||
let wrap_key = WrapKey::parse(&envelop.encrypted_key)?;
|
let wrap_key = WrapKey::parse(&envelop.encrypted_key)?;
|
||||||
let cryptor = match wrap_key.header.enc.as_str() {
|
let cryptor = match wrap_key.header.enc.as_str() {
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ fn encrypt_envelops(cryptor: Cryptor, key: &[u8], envelops: &[&TinyEncryptConfig
|
|||||||
TinyEncryptEnvelopType::PgpX25519 | TinyEncryptEnvelopType::StaticX25519 => {
|
TinyEncryptEnvelopType::PgpX25519 | TinyEncryptEnvelopType::StaticX25519 => {
|
||||||
encrypted_envelops.push(encrypt_envelop_ecdh_x25519(cryptor, key, envelop)?);
|
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)?);
|
encrypted_envelops.push(encrypt_envelop_ecdh(cryptor, key, envelop)?);
|
||||||
}
|
}
|
||||||
TinyEncryptEnvelopType::EcdhP384 => {
|
TinyEncryptEnvelopType::EcdhP384 => {
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ use clap::Args;
|
|||||||
use rust_util::{debugging, information, opt_result, opt_value_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 security_framework::os::macos::keychain::SecKeychain;
|
||||||
|
|
||||||
use crate::{util_keychainkey, util_keychainstatic};
|
|
||||||
use crate::config::TinyEncryptConfigEnvelop;
|
use crate::config::TinyEncryptConfigEnvelop;
|
||||||
use crate::spec::TinyEncryptEnvelopType;
|
use crate::spec::TinyEncryptEnvelopType;
|
||||||
|
#[cfg(feature = "secure-enclave")]
|
||||||
|
use crate::util_keychainkey;
|
||||||
|
use crate::util_keychainstatic;
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
#[derive(Debug, Args)]
|
||||||
pub struct CmdKeychainKey {
|
pub struct CmdKeychainKey {
|
||||||
@@ -27,22 +29,28 @@ const DEFAULT_SERVICE_NAME: &str = "tiny-encrypt";
|
|||||||
|
|
||||||
pub fn keychain_key(cmd_keychain_key: CmdKeychainKey) -> XResult<()> {
|
pub fn keychain_key(cmd_keychain_key: CmdKeychainKey) -> XResult<()> {
|
||||||
if cmd_keychain_key.secure_enclave {
|
if cmd_keychain_key.secure_enclave {
|
||||||
keychain_key_se(cmd_keychain_key)
|
#[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 {
|
} else {
|
||||||
keychain_key_static(cmd_keychain_key)
|
keychain_key_static(cmd_keychain_key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "secure-enclave")]
|
||||||
pub fn keychain_key_se(cmd_keychain_key: CmdKeychainKey) -> XResult<()> {
|
pub fn keychain_key_se(cmd_keychain_key: CmdKeychainKey) -> XResult<()> {
|
||||||
if !util_keychainkey::is_support_se() {
|
if !util_keychainkey::is_support_se() {
|
||||||
return simple_error!("Secure enclave is not supported.");
|
return simple_error!("Secure enclave is not supported.");
|
||||||
}
|
}
|
||||||
let (public_key_hex, private_key_base64) = util_keychainkey::generate_se_p256_keypair()?;
|
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 {
|
let config_envelop = TinyEncryptConfigEnvelop {
|
||||||
r#type: TinyEncryptEnvelopType::KeyP256,
|
r#type: TinyEncryptEnvelopType::KeyP256,
|
||||||
sid: cmd_keychain_key.key_name.clone(),
|
sid: cmd_keychain_key.key_name.clone(),
|
||||||
kid: format!("keychain:{}", &public_key_hex),
|
kid: format!("keychain:02{}", &public_key_compressed_hex),
|
||||||
desc: Some("Keychain Secure Enclave".to_string()),
|
desc: Some("Keychain Secure Enclave".to_string()),
|
||||||
args: Some(vec![
|
args: Some(vec![
|
||||||
private_key_base64
|
private_key_base64
|
||||||
@@ -59,13 +67,13 @@ 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 service_name = cmd_keychain_key.server_name.as_deref().unwrap_or(DEFAULT_SERVICE_NAME);
|
||||||
let sec_keychain = opt_result!(SecKeychain::default(), "Get keychain failed: {}");
|
let sec_keychain = opt_result!(SecKeychain::default(), "Get keychain failed: {}");
|
||||||
let key_name = opt_value_result!(&cmd_keychain_key.key_name, "Key name is required.");
|
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() {
|
if sec_keychain.find_generic_password(service_name, key_name).is_ok() {
|
||||||
return simple_error!("Static x25519 exists: {}.{}", service_name, &key_name);
|
return simple_error!("Static x25519 exists: {}.{}", service_name, &key_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
let (keychain_key, public_key) = util_keychainstatic::generate_static_x25519_secret();
|
let (keychain_key, public_key) = util_keychainstatic::generate_static_x25519_secret();
|
||||||
opt_result!(
|
opt_result!(
|
||||||
sec_keychain.set_generic_password(service_name, &key_name, keychain_key.as_bytes()),
|
sec_keychain.set_generic_password(service_name, key_name, keychain_key.as_bytes()),
|
||||||
"Write static x25519 failed: {}"
|
"Write static x25519 failed: {}"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,25 @@ pub fn is_support_se() -> bool {
|
|||||||
unsafe { is_support_secure_enclave() }
|
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)> {
|
pub fn generate_se_p256_keypair() -> XResult<(String, String)> {
|
||||||
if !is_support_se() {
|
if !is_support_se() {
|
||||||
return simple_error!("Secure enclave is not supported.");
|
return simple_error!("Secure enclave is not supported.");
|
||||||
@@ -22,7 +41,7 @@ pub fn generate_se_p256_keypair() -> XResult<(String, String)> {
|
|||||||
return simple_error!("Generate P256 in secure enclave failed: {}", result);
|
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_key = result.chars().skip(3).collect::<String>();
|
||||||
let public_key_and_private_keys = public_key_and_private_key.split(",").collect::<Vec<_>>();
|
let public_key_and_private_keys = public_key_and_private_key.split(',').collect::<Vec<_>>();
|
||||||
if public_key_and_private_keys.len() != 2 {
|
if public_key_and_private_keys.len() != 2 {
|
||||||
return simple_error!("Generate P256 in secure enclave result is bad: {}", public_key_and_private_key);
|
return simple_error!("Generate P256 in secure enclave result is bad: {}", public_key_and_private_key);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ func computeSecureEnclaveP256Ecdh(privateKeyDataRepresentation: SRString, epheme
|
|||||||
|
|
||||||
let sharedSecret = try p.sharedSecretFromKeyAgreement(
|
let sharedSecret = try p.sharedSecretFromKeyAgreement(
|
||||||
with: ephemeralPublicKey)
|
with: ephemeralPublicKey)
|
||||||
print("Shared secret: \(sharedSecret)")
|
|
||||||
|
|
||||||
return SRString("ok:\(sharedSecret.description)")
|
return SRString("ok:\(sharedSecret.description)")
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
// import Swift
|
|
||||||
// import Foundation
|
|
||||||
// import Security
|
|
||||||
import CryptoKit
|
|
||||||
import LocalAuthentication
|
|
||||||
|
|
||||||
// reference:
|
|
||||||
// https://zenn.dev/iceman/scraps/380f69137c7ea2
|
|
||||||
// https://www.andyibanez.com/posts/cryptokit-secure-enclave/
|
|
||||||
@_cdecl("is_support_secure_enclave")
|
|
||||||
func isSupportSecureEnclave() -> Bool {
|
|
||||||
return SecureEnclave.isAvailable
|
|
||||||
}
|
|
||||||
|
|
||||||
@_cdecl("print_greeting")
|
|
||||||
func printGreeting(modifier: UnsafePointer<CChar>) {
|
|
||||||
print("Hello \(String(cString: modifier))World!")
|
|
||||||
}
|
|
||||||
|
|
||||||
enum StringError: Error {
|
|
||||||
case base64error
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateKeyPair() throws {
|
|
||||||
var error: Unmanaged<CFError>? = nil;
|
|
||||||
guard let accessCtrl = SecAccessControlCreateWithFlags(
|
|
||||||
nil,
|
|
||||||
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
|
||||||
[.privateKeyUsage, .biometryCurrentSet],
|
|
||||||
&error
|
|
||||||
) else {
|
|
||||||
throw error!.takeRetainedValue() as Swift.Error
|
|
||||||
}
|
|
||||||
var privateKeyReference = try CryptoKit.SecureEnclave.P256.KeyAgreement.PrivateKey.init(
|
|
||||||
accessControl: accessCtrl
|
|
||||||
);
|
|
||||||
var dataRepresentation = privateKeyReference.dataRepresentation;
|
|
||||||
print("Private key reference: \(privateKeyReference)");
|
|
||||||
print("Private key reference - publicKey: \(privateKeyReference.publicKey)");
|
|
||||||
print("Private key reference - dataRepresentation: \(privateKeyReference.dataRepresentation)");
|
|
||||||
print("Private key reference - dataRepresentation: \(privateKeyReference.dataRepresentation.base64EncodedString())");
|
|
||||||
}
|
|
||||||
|
|
||||||
func computeEcdh() throws {
|
|
||||||
guard let dataRepresentation = Data(base64Encoded: "BIIB2DGCAdQwgfUMAnJrMYHuMAsMA2JpZAQElx-----"
|
|
||||||
) else {
|
|
||||||
throw StringError.base64error
|
|
||||||
}
|
|
||||||
|
|
||||||
let context = LAContext();
|
|
||||||
let p = try SecureEnclave.P256.KeyAgreement.PrivateKey(
|
|
||||||
dataRepresentation: dataRepresentation, authenticationContext: context)
|
|
||||||
|
|
||||||
let ephemeralSecretKey = P256.KeyAgreement.PrivateKey()
|
|
||||||
let sharedSecret = try ephemeralSecretKey.sharedSecretFromKeyAgreement(with: p.publicKey)
|
|
||||||
let sharedSecret2 = try p.sharedSecretFromKeyAgreement(with: ephemeralSecretKey.publicKey)
|
|
||||||
print("Shared secret: \(sharedSecret)")
|
|
||||||
print("Shared secret: \(sharedSecret2)")
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user