mirror of
https://github.com/morgan9e/bitwarden-cli
synced 2026-04-15 00:34:33 +09:00
Inital commit
This commit is contained in:
129
src/crypto.rs
Normal file
129
src/crypto.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
|
||||
use base64::{engine::general_purpose::STANDARD as B64, Engine};
|
||||
use hmac::{Hmac, Mac};
|
||||
use rand::RngCore;
|
||||
use sha2::Sha256;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>;
|
||||
type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
|
||||
|
||||
pub struct SymKey {
|
||||
raw: Zeroizing<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl SymKey {
|
||||
pub fn new(data: Vec<u8>) -> Self {
|
||||
assert_eq!(data.len(), 64);
|
||||
Self {
|
||||
raw: Zeroizing::new(data),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate() -> Self {
|
||||
let mut buf = vec![0u8; 64];
|
||||
rand::thread_rng().fill_bytes(&mut buf);
|
||||
Self::new(buf)
|
||||
}
|
||||
|
||||
pub fn raw(&self) -> &[u8] {
|
||||
&self.raw
|
||||
}
|
||||
|
||||
fn enc_key(&self) -> &[u8] {
|
||||
&self.raw[..32]
|
||||
}
|
||||
|
||||
fn mac_key(&self) -> &[u8] {
|
||||
&self.raw[32..]
|
||||
}
|
||||
}
|
||||
|
||||
/// Encrypt a plaintext string into "2.iv|ct|mac" format.
|
||||
pub fn encrypt(plaintext: &str, key: &SymKey) -> String {
|
||||
let mut iv = [0u8; 16];
|
||||
rand::thread_rng().fill_bytes(&mut iv);
|
||||
|
||||
let ct = Aes256CbcEnc::new(key.enc_key().into(), &iv.into())
|
||||
.encrypt_padded_vec_mut::<Pkcs7>(plaintext.as_bytes());
|
||||
|
||||
let mut hmac = Hmac::<Sha256>::new_from_slice(key.mac_key()).unwrap();
|
||||
hmac.update(&iv);
|
||||
hmac.update(&ct);
|
||||
let mac = hmac.finalize().into_bytes();
|
||||
|
||||
format!("2.{}|{}|{}", B64.encode(iv), B64.encode(&ct), B64.encode(mac))
|
||||
}
|
||||
|
||||
/// Decrypt a "TYPE.iv|ct|mac" enc-string, return raw bytes.
|
||||
pub fn decrypt(enc_str: &str, key: &SymKey) -> Result<Zeroizing<Vec<u8>>, &'static str> {
|
||||
let (_t, rest) = enc_str.split_once('.').ok_or("bad format")?;
|
||||
let parts: Vec<&str> = rest.split('|').collect();
|
||||
if parts.len() < 2 {
|
||||
return Err("bad format");
|
||||
}
|
||||
|
||||
let iv = B64.decode(parts[0]).map_err(|_| "bad iv")?;
|
||||
let ct = B64.decode(parts[1]).map_err(|_| "bad ct")?;
|
||||
|
||||
if parts.len() > 2 {
|
||||
let mac = B64.decode(parts[2]).map_err(|_| "bad mac")?;
|
||||
let mut hmac = Hmac::<Sha256>::new_from_slice(key.mac_key()).unwrap();
|
||||
hmac.update(&iv);
|
||||
hmac.update(&ct);
|
||||
hmac.verify_slice(&mac).map_err(|_| "MAC mismatch")?;
|
||||
}
|
||||
|
||||
let pt = Aes256CbcDec::new(key.enc_key().into(), iv.as_slice().into())
|
||||
.decrypt_padded_vec_mut::<Pkcs7>(&ct)
|
||||
.map_err(|_| "decrypt failed")?;
|
||||
|
||||
Ok(Zeroizing::new(pt))
|
||||
}
|
||||
|
||||
/// Decrypt an enc-string to a UTF-8 string, or empty on failure.
|
||||
pub fn decrypt_str(enc: Option<&str>, key: &SymKey) -> String {
|
||||
match enc {
|
||||
Some(s) if !s.is_empty() => decrypt(s, key)
|
||||
.map(|b| String::from_utf8_lossy(&b).into_owned())
|
||||
.unwrap_or_default(),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize enc-string to the JSON form Bitwarden IPC expects.
|
||||
pub fn enc_to_json(enc_str: &str) -> serde_json::Value {
|
||||
let (t, rest) = enc_str.split_once('.').unwrap_or(("2", enc_str));
|
||||
let parts: Vec<&str> = rest.split('|').collect();
|
||||
let mut m = serde_json::Map::new();
|
||||
m.insert(
|
||||
"encryptionType".into(),
|
||||
serde_json::json!(t.parse::<u32>().unwrap_or(2)),
|
||||
);
|
||||
m.insert("encryptedString".into(), serde_json::json!(enc_str));
|
||||
if let Some(iv) = parts.first() {
|
||||
m.insert("iv".into(), serde_json::json!(iv));
|
||||
}
|
||||
if let Some(data) = parts.get(1) {
|
||||
m.insert("data".into(), serde_json::json!(data));
|
||||
}
|
||||
if let Some(mac) = parts.get(2) {
|
||||
m.insert("mac".into(), serde_json::json!(mac));
|
||||
}
|
||||
serde_json::Value::Object(m)
|
||||
}
|
||||
|
||||
/// Parse the JSON enc-string form back to "TYPE.iv|data|mac".
|
||||
pub fn json_to_enc(v: &serde_json::Value) -> String {
|
||||
if let Some(s) = v.get("encryptedString").and_then(|s| s.as_str()) {
|
||||
return s.to_string();
|
||||
}
|
||||
let t = v
|
||||
.get("encryptionType")
|
||||
.and_then(|t| t.as_u64())
|
||||
.unwrap_or(2);
|
||||
let iv = v.get("iv").and_then(|s| s.as_str()).unwrap_or("");
|
||||
let data = v.get("data").and_then(|s| s.as_str()).unwrap_or("");
|
||||
let mac = v.get("mac").and_then(|s| s.as_str()).unwrap_or("");
|
||||
format!("{t}.{iv}|{data}|{mac}")
|
||||
}
|
||||
Reference in New Issue
Block a user