feat: decrypt ecdh works
This commit is contained in:
722
Cargo.lock
generated
722
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tiny-encrypt"
|
||||
version = "0.0.0"
|
||||
version = "0.0.2"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
description = "A simple and tiny file encrypt tool"
|
||||
@@ -14,15 +14,18 @@ base64 = "0.21.0"
|
||||
chrono = "0.4.23"
|
||||
clap = { version = "4.1.4", features = ["derive"] }
|
||||
hex = "0.4.3"
|
||||
openpgp-card = "0.3.3"
|
||||
openpgp-card = "0.3.7"
|
||||
openpgp-card-pcsc = "0.3.0"
|
||||
reqwest = { version = "0.11.14", features = ["blocking", "rustls", "rustls-tls"] }
|
||||
rpassword = "7.2.0"
|
||||
rust_util = "0.6.41"
|
||||
serde = { version = "1.0.152", features = ["derive"] }
|
||||
serde_json = "1.0.93"
|
||||
sha256 = "1.4.0"
|
||||
simpledateformat = "0.1.4"
|
||||
x509-parser = "0.15.1"
|
||||
yubico_manager = "0.9.0"
|
||||
yubikey = { version = "0.8.0", features = ["untested"] }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -2,17 +2,24 @@ use std::fs;
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use aes_gcm_stream::Aes256GcmStreamDecryptor;
|
||||
use openpgp_card::crypto_data::Cryptogram;
|
||||
use openpgp_card::OpenPgp;
|
||||
use rust_util::{debugging, failure, opt_result, simple_error, success, util_term, XResult};
|
||||
use x509_parser::prelude::FromDer;
|
||||
use x509_parser::x509::SubjectPublicKeyInfo;
|
||||
use yubikey::piv::{AlgorithmId, decrypt_data, RetiredSlotId, SlotId};
|
||||
use yubikey::YubiKey;
|
||||
|
||||
use crate::{file, util};
|
||||
use crate::card::get_card;
|
||||
use crate::spec::{TinyEncryptEnvelop, TinyEncryptEnvelopType, TinyEncryptMeta};
|
||||
use crate::util::{decode_base64, TINY_ENC_FILE_EXT};
|
||||
use crate::util::{decode_base64, decode_base64_url_no_pad, ENC_AES256_GCM_P256, simple_kdf, TINY_ENC_FILE_EXT};
|
||||
use crate::wrap_key::WrapKey;
|
||||
|
||||
pub fn decrypt(path: &PathBuf, pin: &Option<String>) -> XResult<()> {
|
||||
pub fn decrypt(path: &PathBuf, pin: &Option<String>, slot: &Option<String>) -> XResult<()> {
|
||||
let path_display = format!("{}", path.display());
|
||||
if !path_display.ends_with(TINY_ENC_FILE_EXT) {
|
||||
return simple_error!("File is not tiny encrypt file: {}", &path_display);
|
||||
@@ -28,7 +35,7 @@ pub fn decrypt(path: &PathBuf, pin: &Option<String>) -> XResult<()> {
|
||||
debugging!("Found meta: {}", serde_json::to_string_pretty(&meta).unwrap());
|
||||
let selected_envelop = select_envelop(&meta)?;
|
||||
|
||||
let key = try_decrypt_key(selected_envelop, pin)?;
|
||||
let key = try_decrypt_key(selected_envelop, pin, slot)?;
|
||||
let nonce = opt_result!( decode_base64(&meta.nonce), "Decode nonce failed: {}");
|
||||
|
||||
debugging!("Decrypt key: {}", hex::encode(&key));
|
||||
@@ -61,13 +68,51 @@ fn decrypt_file(file_in: &mut File, file_out: &mut File, key: &[u8], nonce: &[u8
|
||||
Ok(total_len)
|
||||
}
|
||||
|
||||
fn try_decrypt_key(envelop: &TinyEncryptEnvelop, pin: &Option<String>) -> XResult<Vec<u8>> {
|
||||
fn try_decrypt_key(envelop: &TinyEncryptEnvelop, pin: &Option<String>, slot: &Option<String>) -> XResult<Vec<u8>> {
|
||||
match envelop.r#type {
|
||||
TinyEncryptEnvelopType::Pgp => try_decrypt_key_pgp(envelop, pin),
|
||||
TinyEncryptEnvelopType::Ecdh => try_decrypt_key_ecdh(envelop, pin, slot),
|
||||
unknown_type => return simple_error!("Unknown or not supported type: {}", unknown_type.get_name())
|
||||
}
|
||||
}
|
||||
|
||||
fn try_decrypt_key_ecdh(envelop: &TinyEncryptEnvelop, pin: &Option<String>, slot: &Option<String>) -> XResult<Vec<u8>> {
|
||||
let is_slot_none = slot.as_ref().map(|s| s.is_empty()).unwrap_or(true);
|
||||
if is_slot_none {
|
||||
return simple_error!("--slot is required for ecdh");
|
||||
}
|
||||
let wrap_key = WrapKey::parse(&envelop.encrypted_key)?;
|
||||
if wrap_key.header.enc.as_str() != ENC_AES256_GCM_P256 {
|
||||
return simple_error!("Unsupported header enc.");
|
||||
}
|
||||
let e_pub_key = &wrap_key.header.e_pub_key;
|
||||
let e_pub_key_bytes = opt_result!(decode_base64_url_no_pad(e_pub_key), "Invalid envelop: {}");
|
||||
let (_, subject_public_key_info) = opt_result!( SubjectPublicKeyInfo::from_der(&e_pub_key_bytes), "Invalid envelop: {}");
|
||||
|
||||
let slot = slot.as_ref().unwrap();
|
||||
let pin = read_pin(pin);
|
||||
let epk_bytes = subject_public_key_info.subject_public_key.as_ref();
|
||||
|
||||
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
|
||||
let retired_slot_id = opt_result!(RetiredSlotId::from_str(slot), "Slot not found: {}");
|
||||
let slot_id = SlotId::Retired(retired_slot_id);
|
||||
opt_result!(yk.verify_pin(pin.as_bytes()), "YubiKey verify pin failed: {}");
|
||||
let decrypted_shared_secret = opt_result!(decrypt_data(
|
||||
&mut yk,
|
||||
&epk_bytes,
|
||||
AlgorithmId::EccP256,
|
||||
slot_id,
|
||||
), "Decrypt piv failed: {}");
|
||||
let key = simple_kdf(decrypted_shared_secret.as_slice());
|
||||
let key: [u8; 32] = opt_result!(key.as_slice().try_into(), "Invalid envelop: {}");
|
||||
let mut aes256_gcm = Aes256GcmStreamDecryptor::new(key, &wrap_key.nonce);
|
||||
let mut b1 = aes256_gcm.update(&wrap_key.encrypted_data);
|
||||
let b2 = opt_result!(aes256_gcm.finalize(), "Invalid envelop: {}");
|
||||
b1.extend_from_slice(&b2);
|
||||
|
||||
Ok(b1)
|
||||
}
|
||||
|
||||
fn try_decrypt_key_pgp(envelop: &TinyEncryptEnvelop, pin: &Option<String>) -> XResult<Vec<u8>> {
|
||||
let card = match get_card() {
|
||||
Err(e) => {
|
||||
|
||||
@@ -9,6 +9,7 @@ mod util;
|
||||
mod config;
|
||||
mod spec;
|
||||
mod crypto;
|
||||
mod wrap_key;
|
||||
mod file;
|
||||
mod card;
|
||||
mod cmd_info;
|
||||
@@ -38,6 +39,9 @@ enum Commands {
|
||||
/// PIN
|
||||
#[arg(long)]
|
||||
pin: Option<String>,
|
||||
/// SLOT
|
||||
#[arg(long)]
|
||||
slot: Option<String>,
|
||||
},
|
||||
/// Show file info
|
||||
#[command(arg_required_else_help = true, short_flag = 'I')]
|
||||
@@ -57,9 +61,9 @@ fn main() -> XResult<()> {
|
||||
paths.iter().for_each(|f| information!("{:?}", f));
|
||||
Ok(())
|
||||
}
|
||||
Commands::Decrypt { paths, pin } => {
|
||||
Commands::Decrypt { paths, pin, slot } => {
|
||||
for path in &paths {
|
||||
match cmd_decrypt::decrypt(path, &pin) {
|
||||
match cmd_decrypt::decrypt(path, &pin, &slot) {
|
||||
Ok(_) => success!("Decrypt {} succeed", path.to_str().unwrap_or("N/A")),
|
||||
Err(e) => failure!("Decrypt {} failed: {}", path.to_str().unwrap_or("N/A"), e),
|
||||
}
|
||||
|
||||
17
src/util.rs
17
src/util.rs
@@ -5,12 +5,29 @@ use base64::Engine;
|
||||
use base64::engine::general_purpose;
|
||||
use rust_util::{warning, XResult};
|
||||
|
||||
pub const ENC_AES256_GCM_P256: &str = "aes256-gcm-p256";
|
||||
pub const TINY_ENC_FILE_EXT: &str = ".tinyenc";
|
||||
|
||||
pub fn simple_kdf(input: &[u8]) -> Vec<u8> {
|
||||
let input = hex::decode(sha256::digest(input)).unwrap();
|
||||
let input = hex::decode(sha256::digest(input)).unwrap();
|
||||
let input = hex::decode(sha256::digest(input)).unwrap();
|
||||
let input = hex::decode(sha256::digest(input)).unwrap();
|
||||
let input = hex::decode(sha256::digest(input)).unwrap();
|
||||
let input = hex::decode(sha256::digest(input)).unwrap();
|
||||
let input = hex::decode(sha256::digest(input)).unwrap();
|
||||
let input = hex::decode(sha256::digest(input)).unwrap();
|
||||
input
|
||||
}
|
||||
|
||||
pub fn decode_base64(input: &str) -> XResult<Vec<u8>> {
|
||||
Ok(general_purpose::STANDARD.decode(input)?)
|
||||
}
|
||||
|
||||
pub fn decode_base64_url_no_pad(input: &str) -> XResult<Vec<u8>> {
|
||||
Ok(general_purpose::URL_SAFE_NO_PAD.decode(input)?)
|
||||
}
|
||||
|
||||
pub fn read_number(hint: &str, from: usize, to: usize) -> usize {
|
||||
loop {
|
||||
print!("{} ({}-{}): ", hint, from, to);
|
||||
|
||||
44
src/wrap_key.rs
Normal file
44
src/wrap_key.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use rust_util::{opt_result, simple_error, XResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::util::decode_base64_url_no_pad;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WrapKey {
|
||||
pub header: WrapKeyHeader,
|
||||
pub nonce: Vec<u8>,
|
||||
pub encrypted_data: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WrapKeyHeader {
|
||||
pub kid: Option<String>,
|
||||
pub enc: String,
|
||||
pub e_pub_key: String,
|
||||
}
|
||||
|
||||
impl WrapKey {
|
||||
pub fn parse(wk: &str) -> XResult<WrapKey> {
|
||||
if !wk.starts_with("WK:") {
|
||||
return simple_error!("Wrap key string must starts with WK:");
|
||||
}
|
||||
let wks = wk.split(".").collect::<Vec<_>>();
|
||||
if wks.len() != 3 {
|
||||
return simple_error!("Invalid wrap key.");
|
||||
}
|
||||
let header = wks[0].chars().skip(3).collect::<String>();
|
||||
let header_bytes = opt_result!(decode_base64_url_no_pad(&header), "Invalid wrap key header: {}");
|
||||
let nonce = wks[1];
|
||||
let encrypted_data = wks[2];
|
||||
let header_str = opt_result!(String::from_utf8(header_bytes), "Invalid wrap key header: {}");
|
||||
let header: WrapKeyHeader = opt_result!(serde_json::from_str(&header_str), "Invalid wrap key header: {}");
|
||||
let nonce = opt_result!(decode_base64_url_no_pad(nonce), "Invalid wrap key: {}");
|
||||
let encrypted_data = opt_result!(decode_base64_url_no_pad(encrypted_data), "Invalid wrap key: {}");
|
||||
Ok(WrapKey {
|
||||
header,
|
||||
nonce,
|
||||
encrypted_data,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user