mirror of
https://github.com/morgan9e/bitwarden-desktop-agent
synced 2026-04-13 15:55:03 +09:00
Fixed TPM2 and Linux compatibility
This commit is contained in:
49
Cargo.lock
generated
49
Cargo.lock
generated
@@ -351,6 +351,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"sha1",
|
||||
"sha2",
|
||||
"tempfile",
|
||||
"ureq",
|
||||
"uuid",
|
||||
"zeroize",
|
||||
@@ -397,6 +398,22 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
@@ -687,6 +704,12 @@ version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.1"
|
||||
@@ -993,6 +1016,19 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.37"
|
||||
@@ -1210,6 +1246,19 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.2"
|
||||
|
||||
@@ -33,6 +33,9 @@ uuid = { version = "1", features = ["v4"] }
|
||||
zeroize = { version = "1", features = ["derive"] }
|
||||
aes-gcm = "0.10"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
lto = true
|
||||
|
||||
10
Makefile
10
Makefile
@@ -24,14 +24,14 @@ launchd-unload:
|
||||
|
||||
systemd:
|
||||
mkdir -p $(HOME)/.config/systemd/user
|
||||
sed 's|%h/.local/bin|$(PREFIX)|' docs/bw-agent.service \
|
||||
> $(HOME)/.config/systemd/user/bw-agent.service
|
||||
sed 's|%h/.local/bin|$(PREFIX)|' docs/com.bitwarden.agent.service \
|
||||
> $(HOME)/.config/systemd/user/com.bitwarden.agent.service
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now bw-agent
|
||||
systemctl --user enable --now com.bitwarden.agent
|
||||
|
||||
systemd-unload:
|
||||
systemctl --user disable --now bw-agent 2>/dev/null || true
|
||||
rm -f $(HOME)/.config/systemd/user/bw-agent.service
|
||||
systemctl --user disable --now com.bitwarden.agent 2>/dev/null || true
|
||||
rm -f $(HOME)/.config/systemd/user/com.bitwarden.agent.service
|
||||
systemctl --user daemon-reload
|
||||
|
||||
clean:
|
||||
|
||||
@@ -80,12 +80,12 @@ fn ssh_askpass() -> Option<Prompter> {
|
||||
}
|
||||
|
||||
fn which(name: &str) -> Option<String> {
|
||||
Command::new("which")
|
||||
.arg(name)
|
||||
.output()
|
||||
.ok()
|
||||
.filter(|o| o.status.success())
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
|
||||
std::env::var_os("PATH")?
|
||||
.to_str()?
|
||||
.split(':')
|
||||
.map(|dir| std::path::Path::new(dir).join(name))
|
||||
.find(|p| p.is_file())
|
||||
.map(|p| p.to_string_lossy().into_owned())
|
||||
}
|
||||
|
||||
pub fn get_prompter(name: Option<&str>) -> Prompter {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use base64::{engine::general_purpose::STANDARD as B64, Engine};
|
||||
use hmac::{Hmac, Mac};
|
||||
use sha2::Sha256;
|
||||
use zeroize::Zeroizing;
|
||||
use zeroize::{Zeroize, Zeroizing};
|
||||
|
||||
use crate::askpass::Prompter;
|
||||
use crate::crypto::{enc_string_decrypt_bytes, SymmetricKey};
|
||||
@@ -44,10 +44,12 @@ pub fn login(
|
||||
password, email, kdf_type, kdf_iter, kdf_mem, kdf_par,
|
||||
));
|
||||
|
||||
let pw_hash = {
|
||||
let mut pw_hash = {
|
||||
let mut buf = [0u8; 32];
|
||||
pbkdf2::pbkdf2_hmac::<Sha256>(master_key.as_slice(), password.as_bytes(), 1, &mut buf);
|
||||
B64.encode(buf)
|
||||
let h = B64.encode(buf);
|
||||
buf.zeroize();
|
||||
h
|
||||
};
|
||||
|
||||
let device_id = uuid::Uuid::new_v4().to_string();
|
||||
@@ -64,6 +66,7 @@ pub fn login(
|
||||
|
||||
crate::log::info(&format!("token {identity}/connect/token"));
|
||||
let token_resp = try_login(&format!("{identity}/connect/token"), &form, prompt);
|
||||
pw_hash.zeroize();
|
||||
|
||||
let enc_user_key = extract_encrypted_user_key(&token_resp);
|
||||
crate::log::info("decrypting user key...");
|
||||
|
||||
@@ -8,7 +8,8 @@ use zeroize::Zeroize;
|
||||
|
||||
use crate::askpass::Prompter;
|
||||
use crate::crypto::{
|
||||
enc_string_decrypt, enc_string_encrypt, enc_string_to_json, json_to_enc_string, SymmetricKey,
|
||||
enc_string_decrypt_bytes, enc_string_encrypt, enc_string_to_json, json_to_enc_string,
|
||||
SymmetricKey,
|
||||
};
|
||||
use crate::storage::KeyStore;
|
||||
|
||||
@@ -95,7 +96,7 @@ impl BiometricBridge {
|
||||
|
||||
let key = self.sessions.get(app_id).unwrap();
|
||||
let enc_str = json_to_enc_string(enc_msg);
|
||||
let plaintext = match enc_string_decrypt(&enc_str, key) {
|
||||
let plaintext = match enc_string_decrypt_bytes(&enc_str, key) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
crate::log::error("message decryption failed");
|
||||
@@ -103,7 +104,7 @@ impl BiometricBridge {
|
||||
}
|
||||
};
|
||||
|
||||
let data: Value = serde_json::from_str(&plaintext).ok()?;
|
||||
let data: Value = serde_json::from_slice(&plaintext).ok()?;
|
||||
let cmd = data.get("command")?.as_str()?.to_string();
|
||||
let mid = data.get("messageId").and_then(|m| m.as_i64()).unwrap_or(0);
|
||||
|
||||
@@ -111,8 +112,9 @@ impl BiometricBridge {
|
||||
let resp = self.dispatch(&cmd, mid)?;
|
||||
|
||||
let key = self.sessions.get(app_id).unwrap();
|
||||
let resp_json = serde_json::to_string(&resp).unwrap();
|
||||
let mut resp_json = serde_json::to_string(&resp).unwrap();
|
||||
let encrypted = enc_string_encrypt(&resp_json, key);
|
||||
resp_json.zeroize();
|
||||
|
||||
Some(json!({
|
||||
"appId": app_id,
|
||||
@@ -171,20 +173,21 @@ impl BiometricBridge {
|
||||
}
|
||||
|
||||
fn unseal_key(&self) -> Option<String> {
|
||||
let pw = (self.prompt)(&format!("Enter {} password:", self.store.name()))?;
|
||||
match self.store.load(&self.uid, &pw) {
|
||||
let mut pw = (self.prompt)(&format!("Enter {} password:", self.store.name()))?;
|
||||
let result = match self.store.load(&self.uid, &pw) {
|
||||
Ok(mut raw) => {
|
||||
let len = raw.len();
|
||||
let b64 = B64.encode(&raw);
|
||||
raw.zeroize();
|
||||
crate::log::info(&format!("unsealed {len}B from {}", self.store.name()));
|
||||
crate::log::info("wiped key from memory");
|
||||
Some(b64)
|
||||
}
|
||||
Err(e) => {
|
||||
crate::log::error(&format!("unseal failed: {e}"));
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
pw.zeroize();
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,11 +60,6 @@ pub fn enc_string_encrypt(plaintext: &str, key: &SymmetricKey) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn enc_string_decrypt(enc_str: &str, key: &SymmetricKey) -> Result<String, &'static str> {
|
||||
let raw = enc_string_decrypt_bytes(enc_str, key)?;
|
||||
String::from_utf8(raw.to_vec()).map_err(|_| "invalid utf8")
|
||||
}
|
||||
|
||||
pub fn enc_string_decrypt_bytes(enc_str: &str, key: &SymmetricKey) -> Result<Zeroizing<Vec<u8>>, &'static str> {
|
||||
let (_t, rest) = enc_str.split_once('.').ok_or("bad format")?;
|
||||
let parts: Vec<&str> = rest.split('|').collect();
|
||||
|
||||
@@ -11,6 +11,12 @@ pub fn socket_path() -> PathBuf {
|
||||
}
|
||||
|
||||
fn dirs_cache() -> PathBuf {
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Ok(xdg) = std::env::var("XDG_CACHE_HOME") {
|
||||
if !xdg.is_empty() {
|
||||
return PathBuf::from(xdg);
|
||||
}
|
||||
}
|
||||
dirs_home().join(".cache")
|
||||
}
|
||||
|
||||
|
||||
30
src/main.rs
30
src/main.rs
@@ -26,7 +26,8 @@ struct Args {
|
||||
#[arg(long, default_value = "https://vault.bitwarden.com")]
|
||||
server: String,
|
||||
|
||||
#[arg(long)]
|
||||
#[cfg_attr(target_os = "linux", arg(long, help = "Key storage backend [pin, tpm2]"))]
|
||||
#[cfg_attr(not(target_os = "linux"), arg(long, help = "Key storage backend [pin]"))]
|
||||
backend: Option<String>,
|
||||
|
||||
#[arg(long)]
|
||||
@@ -68,7 +69,7 @@ fn main() {
|
||||
"re-enrolling"
|
||||
});
|
||||
|
||||
let pw = args
|
||||
let mut pw = args
|
||||
.password
|
||||
.clone()
|
||||
.or_else(|| prompt("master password:"))
|
||||
@@ -76,22 +77,26 @@ fn main() {
|
||||
|
||||
log::info(&format!("logging in as {email}"));
|
||||
let (mut key_bytes, server_uid) = auth::login(email, &pw, &args.server, &prompt);
|
||||
pw.zeroize();
|
||||
log::info(&format!("authenticated, uid={server_uid}"));
|
||||
|
||||
let auth = prompt(&format!("choose {} password:", store.name()))
|
||||
let mut auth = prompt(&format!("choose {} password:", store.name()))
|
||||
.unwrap_or_else(|| log::fatal("no password provided"));
|
||||
let auth2 = prompt(&format!("confirm {} password:", store.name()))
|
||||
let mut auth2 = prompt(&format!("confirm {} password:", store.name()))
|
||||
.unwrap_or_else(|| log::fatal("no password provided"));
|
||||
if auth != auth2 {
|
||||
auth.zeroize();
|
||||
auth2.zeroize();
|
||||
log::fatal("passwords don't match");
|
||||
}
|
||||
auth2.zeroize();
|
||||
|
||||
store
|
||||
.store(&uid, &key_bytes, &auth)
|
||||
.unwrap_or_else(|e| log::fatal(&format!("store failed: {e}")));
|
||||
auth.zeroize();
|
||||
key_bytes.zeroize();
|
||||
log::info(&format!("key sealed via {}", store.name()));
|
||||
log::info("wiped key from memory");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -101,26 +106,31 @@ fn main() {
|
||||
None => log::fatal("no enrolled key found"),
|
||||
};
|
||||
|
||||
let old_pw = prompt(&format!("current {} password:", store.name()))
|
||||
let mut old_pw = prompt(&format!("current {} password:", store.name()))
|
||||
.unwrap_or_else(|| log::fatal("no password provided"));
|
||||
let mut data = store
|
||||
.load(&uid, &old_pw)
|
||||
.unwrap_or_else(|e| log::fatal(&format!("unseal failed: {e}")));
|
||||
old_pw.zeroize();
|
||||
|
||||
let new_pw = prompt(&format!("new {} password:", store.name()))
|
||||
let mut new_pw = prompt(&format!("new {} password:", store.name()))
|
||||
.unwrap_or_else(|| log::fatal("no password provided"));
|
||||
let new_pw2 = prompt(&format!("confirm {} password:", store.name()))
|
||||
let mut new_pw2 = prompt(&format!("confirm {} password:", store.name()))
|
||||
.unwrap_or_else(|| log::fatal("no password provided"));
|
||||
if new_pw != new_pw2 {
|
||||
new_pw.zeroize();
|
||||
new_pw2.zeroize();
|
||||
data.zeroize();
|
||||
log::fatal("passwords don't match");
|
||||
}
|
||||
new_pw2.zeroize();
|
||||
|
||||
store
|
||||
.store(&uid, &data, &new_pw)
|
||||
.unwrap_or_else(|e| log::fatal(&format!("seal failed: {e}")));
|
||||
new_pw.zeroize();
|
||||
data.zeroize();
|
||||
log::info("pin changed");
|
||||
log::info("wiped key from memory");
|
||||
log::info("password changed");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ use std::thread;
|
||||
const MAX_MSG: usize = 1024 * 1024;
|
||||
|
||||
fn socket_path() -> String {
|
||||
// always use $HOME/.cache — never XDG_CACHE_HOME, which Flatpak
|
||||
// overrides to a sandboxed path the agent can't see
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".into());
|
||||
PathBuf::from(home)
|
||||
.join(".cache")
|
||||
@@ -65,7 +67,11 @@ fn send_ipc(sock: &mut UnixStream, data: &[u8]) {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut sock = UnixStream::connect(socket_path()).unwrap_or_else(|_| std::process::exit(1));
|
||||
let sock_addr = socket_path();
|
||||
let mut sock = UnixStream::connect(&sock_addr).unwrap_or_else(|e| {
|
||||
eprintln!("bw-proxy: connect {sock_addr}: {e}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
send_ipc(&mut sock, b"{\"command\":\"connected\"}");
|
||||
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
import Foundation
|
||||
import Security
|
||||
import LocalAuthentication
|
||||
|
||||
let service = "com.bitwarden.agent"
|
||||
let algo = SecKeyAlgorithm.eciesEncryptionCofactorVariableIVX963SHA256AESGCM
|
||||
|
||||
func sepTag(_ label: String) -> Data {
|
||||
"\(service).sep.\(label)".data(using: .utf8)!
|
||||
}
|
||||
|
||||
func getSEPKey(_ label: String, password: String?) -> SecKey? {
|
||||
var q: [String: Any] = [
|
||||
kSecClass as String: kSecClassKey,
|
||||
kSecAttrApplicationTag as String: sepTag(label),
|
||||
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
|
||||
kSecReturnRef as String: true,
|
||||
]
|
||||
|
||||
if let pw = password {
|
||||
let ctx = LAContext()
|
||||
ctx.setCredential(pw.data(using: .utf8), type: .applicationPassword)
|
||||
q[kSecUseAuthenticationContext as String] = ctx
|
||||
}
|
||||
|
||||
var ref: CFTypeRef?
|
||||
let s = SecItemCopyMatching(q as CFDictionary, &ref)
|
||||
if s == errSecSuccess { return (ref as! SecKey) }
|
||||
if s != errSecItemNotFound { fatal("keychain query: \(s)") }
|
||||
return nil
|
||||
}
|
||||
|
||||
func createSEPKey(_ label: String, password: String) -> SecKey {
|
||||
var err: Unmanaged<CFError>?
|
||||
|
||||
let ctx = LAContext()
|
||||
ctx.setCredential(password.data(using: .utf8), type: .applicationPassword)
|
||||
|
||||
guard let access = SecAccessControlCreateWithFlags(
|
||||
nil,
|
||||
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
||||
[.privateKeyUsage, .applicationPassword],
|
||||
&err
|
||||
) else {
|
||||
fatal("access control: \(err!.takeRetainedValue())")
|
||||
}
|
||||
|
||||
let attrs: [String: Any] = [
|
||||
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
|
||||
kSecAttrKeySizeInBits as String: 256,
|
||||
kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
|
||||
kSecPrivateKeyAttrs as String: [
|
||||
kSecAttrIsPermanent as String: true,
|
||||
kSecAttrApplicationTag as String: sepTag(label),
|
||||
kSecAttrAccessControl as String: access,
|
||||
] as [String: Any],
|
||||
kSecUseAuthenticationContext as String: ctx,
|
||||
]
|
||||
|
||||
guard let key = SecKeyCreateRandomKey(attrs as CFDictionary, &err) else {
|
||||
fatal("create SEP key: \(err!.takeRetainedValue())")
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func removeSEPKey(_ label: String) {
|
||||
let q: [String: Any] = [
|
||||
kSecClass as String: kSecClassKey,
|
||||
kSecAttrApplicationTag as String: sepTag(label),
|
||||
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
|
||||
]
|
||||
SecItemDelete(q as CFDictionary)
|
||||
}
|
||||
|
||||
func storeBlob(_ label: String, _ data: Data) {
|
||||
let q: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: label,
|
||||
]
|
||||
SecItemDelete(q as CFDictionary)
|
||||
|
||||
var add = q
|
||||
add[kSecValueData as String] = data
|
||||
add[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly
|
||||
|
||||
let s = SecItemAdd(add as CFDictionary, nil)
|
||||
if s != errSecSuccess { fatal("store blob: \(s)") }
|
||||
}
|
||||
|
||||
func loadBlob(_ label: String) -> Data? {
|
||||
let q: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: label,
|
||||
kSecReturnData as String: true,
|
||||
]
|
||||
var ref: CFTypeRef?
|
||||
return SecItemCopyMatching(q as CFDictionary, &ref) == errSecSuccess ? (ref as! Data) : nil
|
||||
}
|
||||
|
||||
func removeBlob(_ label: String) {
|
||||
let q: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: label,
|
||||
]
|
||||
SecItemDelete(q as CFDictionary)
|
||||
}
|
||||
|
||||
func hasBlob(_ label: String) -> Bool {
|
||||
let q: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: label,
|
||||
]
|
||||
return SecItemCopyMatching(q as CFDictionary, nil) == errSecSuccess
|
||||
}
|
||||
|
||||
func encrypt(_ pubKey: SecKey, _ data: Data) -> Data {
|
||||
var err: Unmanaged<CFError>?
|
||||
guard let ct = SecKeyCreateEncryptedData(pubKey, algo, data as CFData, &err) else {
|
||||
fatal("encrypt: \(err!.takeRetainedValue())")
|
||||
}
|
||||
return ct as Data
|
||||
}
|
||||
|
||||
func decrypt(_ privKey: SecKey, _ data: Data) -> Data {
|
||||
var err: Unmanaged<CFError>?
|
||||
guard let pt = SecKeyCreateDecryptedData(privKey, algo, data as CFData, &err) else {
|
||||
fatal("decrypt: \(err!.takeRetainedValue())")
|
||||
}
|
||||
return pt as Data
|
||||
}
|
||||
|
||||
func readStdin() -> Data {
|
||||
var buf = Data()
|
||||
while let line = readLine(strippingNewline: false) {
|
||||
buf.append(line.data(using: .utf8)!)
|
||||
}
|
||||
let trimmed = String(data: buf, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard let decoded = Data(base64Encoded: trimmed) else {
|
||||
fatal("invalid base64 on stdin")
|
||||
}
|
||||
return decoded
|
||||
}
|
||||
|
||||
func fatal(_ msg: String) -> Never {
|
||||
FileHandle.standardError.write("sep-helper: \(msg)\n".data(using: .utf8)!)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
func usage() -> Never {
|
||||
FileHandle.standardError.write("usage: sep-helper <store|load|remove|has> <label> [password]\n".data(using: .utf8)!)
|
||||
exit(2)
|
||||
}
|
||||
|
||||
let args = CommandLine.arguments
|
||||
if args.count < 3 { usage() }
|
||||
let cmd = args[1]
|
||||
let label = args[2]
|
||||
let password: String? = args.count > 3 ? args[3] : nil
|
||||
|
||||
switch cmd {
|
||||
case "store":
|
||||
guard let pw = password else { fatal("password required for store") }
|
||||
let data = readStdin()
|
||||
removeSEPKey(label)
|
||||
let privKey = createSEPKey(label, password: pw)
|
||||
guard let pubKey = SecKeyCopyPublicKey(privKey) else { fatal("no public key") }
|
||||
let ct = encrypt(pubKey, data)
|
||||
storeBlob(label, ct)
|
||||
|
||||
case "load":
|
||||
guard let pw = password else { fatal("password required for load") }
|
||||
guard let privKey = getSEPKey(label, password: pw) else { fatal("no SEP key for \(label)") }
|
||||
guard let ct = loadBlob(label) else { fatal("no data for \(label)") }
|
||||
let pt = decrypt(privKey, ct)
|
||||
print(pt.base64EncodedString())
|
||||
|
||||
case "remove":
|
||||
removeBlob(label)
|
||||
removeSEPKey(label)
|
||||
|
||||
case "has":
|
||||
exit(hasBlob(label) ? 0 : 1)
|
||||
|
||||
default:
|
||||
usage()
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
pub mod pin;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod tpm2;
|
||||
|
||||
pub trait KeyStore {
|
||||
fn name(&self) -> &str;
|
||||
@@ -12,7 +14,28 @@ pub trait KeyStore {
|
||||
|
||||
pub fn get_backend(preferred: Option<&str>) -> Box<dyn KeyStore> {
|
||||
match preferred {
|
||||
Some("pin") | None => Box::new(pin::PinKeyStore::new(None)),
|
||||
Some("pin") => Box::new(pin::PinKeyStore::new(None)),
|
||||
#[cfg(target_os = "linux")]
|
||||
Some("tpm2") => Box::new(tpm2::Tpm2KeyStore::new(None)),
|
||||
None => {
|
||||
let pin = pin::PinKeyStore::new(None);
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let tpm = tpm2::Tpm2KeyStore::new(None);
|
||||
// if keys already exist, use whichever backend owns them
|
||||
if tpm.find_key().is_some() {
|
||||
return Box::new(tpm);
|
||||
}
|
||||
if pin.find_key().is_some() {
|
||||
return Box::new(pin);
|
||||
}
|
||||
// no existing keys — prefer TPM2 if available
|
||||
if tpm.is_available() {
|
||||
return Box::new(tpm);
|
||||
}
|
||||
}
|
||||
Box::new(pin)
|
||||
}
|
||||
Some(other) => crate::log::fatal(&format!("unknown backend: {other}")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,19 @@ const SCRYPT_LOG_N: u8 = 17;
|
||||
const SCRYPT_R: u32 = 8;
|
||||
const SCRYPT_P: u32 = 1;
|
||||
|
||||
fn store_dir() -> PathBuf {
|
||||
pub(super) fn cache_dir() -> PathBuf {
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Ok(xdg) = std::env::var("XDG_CACHE_HOME") {
|
||||
if !xdg.is_empty() {
|
||||
return PathBuf::from(xdg);
|
||||
}
|
||||
}
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".into());
|
||||
PathBuf::from(home)
|
||||
.join(".cache")
|
||||
PathBuf::from(home).join(".cache")
|
||||
}
|
||||
|
||||
fn store_dir() -> PathBuf {
|
||||
cache_dir()
|
||||
.join("com.bitwarden.desktop")
|
||||
.join("keys")
|
||||
}
|
||||
|
||||
190
src/storage/tpm2.rs
Normal file
190
src/storage/tpm2.rs
Normal file
@@ -0,0 +1,190 @@
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use super::KeyStore;
|
||||
|
||||
fn store_dir() -> PathBuf {
|
||||
super::pin::cache_dir()
|
||||
.join("com.bitwarden.desktop")
|
||||
.join("keys")
|
||||
}
|
||||
|
||||
pub struct Tpm2KeyStore {
|
||||
dir: PathBuf,
|
||||
}
|
||||
|
||||
impl Tpm2KeyStore {
|
||||
pub fn new(dir: Option<PathBuf>) -> Self {
|
||||
let dir = dir.unwrap_or_else(store_dir);
|
||||
fs::create_dir_all(&dir).ok();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(&dir, fs::Permissions::from_mode(0o700)).ok();
|
||||
}
|
||||
Self { dir }
|
||||
}
|
||||
|
||||
fn pub_path(&self, uid: &str) -> PathBuf {
|
||||
self.dir.join(format!("{uid}.pub"))
|
||||
}
|
||||
|
||||
fn priv_path(&self, uid: &str) -> PathBuf {
|
||||
self.dir.join(format!("{uid}.priv"))
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyStore for Tpm2KeyStore {
|
||||
fn name(&self) -> &str {
|
||||
"tpm2"
|
||||
}
|
||||
|
||||
fn is_available(&self) -> bool {
|
||||
Command::new("tpm2_getcap")
|
||||
.arg("properties-fixed")
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn has_key(&self, uid: &str) -> bool {
|
||||
self.pub_path(uid).exists() && self.priv_path(uid).exists()
|
||||
}
|
||||
|
||||
fn store(&self, uid: &str, data: &[u8], auth: &str) -> Result<(), String> {
|
||||
let tmp = tempfile::tempdir().map_err(|e| e.to_string())?;
|
||||
let ctx = tmp.path().join("primary.ctx");
|
||||
let dat = tmp.path().join("data.bin");
|
||||
|
||||
fs::write(&dat, data).map_err(|e| e.to_string())?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(&dat, fs::Permissions::from_mode(0o600)).ok();
|
||||
}
|
||||
|
||||
run(&[
|
||||
"tpm2_createprimary", "-C", "o", "-g", "sha256", "-G", "aes256cfb",
|
||||
"-c", &ctx.to_string_lossy(),
|
||||
], None)?;
|
||||
|
||||
let pub_path = self.pub_path(uid);
|
||||
let priv_path = self.priv_path(uid);
|
||||
|
||||
let result = run(&[
|
||||
"tpm2_create", "-C", &ctx.to_string_lossy(),
|
||||
"-i", &dat.to_string_lossy(),
|
||||
"-u", &pub_path.to_string_lossy(),
|
||||
"-r", &priv_path.to_string_lossy(),
|
||||
"-p", "file:-",
|
||||
], Some(auth.as_bytes()));
|
||||
|
||||
// overwrite plaintext before temp dir cleanup
|
||||
let mut zeros = vec![0u8; data.len()];
|
||||
fs::File::create(&dat).ok().map(|mut f| f.write_all(&zeros));
|
||||
zeros.zeroize();
|
||||
|
||||
result?;
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(&pub_path, fs::Permissions::from_mode(0o600)).ok();
|
||||
fs::set_permissions(&priv_path, fs::Permissions::from_mode(0o600)).ok();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load(&self, uid: &str, auth: &str) -> Result<Vec<u8>, String> {
|
||||
let tmp = tempfile::tempdir().map_err(|e| e.to_string())?;
|
||||
let ctx = tmp.path().join("primary.ctx");
|
||||
let loaded = tmp.path().join("loaded.ctx");
|
||||
|
||||
run(&[
|
||||
"tpm2_createprimary", "-C", "o", "-g", "sha256", "-G", "aes256cfb",
|
||||
"-c", &ctx.to_string_lossy(),
|
||||
], None)?;
|
||||
|
||||
run(&[
|
||||
"tpm2_load", "-C", &ctx.to_string_lossy(),
|
||||
"-u", &self.pub_path(uid).to_string_lossy(),
|
||||
"-r", &self.priv_path(uid).to_string_lossy(),
|
||||
"-c", &loaded.to_string_lossy(),
|
||||
], None)?;
|
||||
|
||||
let mut child = Command::new("tpm2_unseal")
|
||||
.args(["-c", &loaded.to_string_lossy(), "-p", "file:-"])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
let _ = stdin.write_all(auth.as_bytes());
|
||||
}
|
||||
|
||||
let mut out = child.wait_with_output().map_err(|e| e.to_string())?;
|
||||
|
||||
if !out.status.success() {
|
||||
out.stdout.zeroize();
|
||||
return Err(String::from_utf8_lossy(&out.stderr).trim().to_string());
|
||||
}
|
||||
|
||||
out.stderr.zeroize();
|
||||
Ok(out.stdout)
|
||||
}
|
||||
|
||||
fn remove(&self, uid: &str) {
|
||||
for p in [self.pub_path(uid), self.priv_path(uid)] {
|
||||
if p.exists() {
|
||||
fs::remove_file(p).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_key(&self) -> Option<String> {
|
||||
let entries = fs::read_dir(&self.dir).ok()?;
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|e| e.to_str()) == Some("pub") {
|
||||
let uid = path.file_stem()?.to_str()?;
|
||||
if self.priv_path(uid).exists() {
|
||||
return Some(uid.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn run(args: &[&str], stdin_data: Option<&[u8]>) -> Result<(), String> {
|
||||
let mut cmd = Command::new(args[0]);
|
||||
cmd.args(&args[1..]).stderr(Stdio::piped());
|
||||
|
||||
if stdin_data.is_some() {
|
||||
cmd.stdin(Stdio::piped());
|
||||
}
|
||||
|
||||
let mut child = cmd.spawn().map_err(|e| format!("{}: {e}", args[0]))?;
|
||||
|
||||
if let Some(data) = stdin_data {
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
let _ = stdin.write_all(data);
|
||||
}
|
||||
}
|
||||
|
||||
let out = child.wait_with_output().map_err(|e| e.to_string())?;
|
||||
|
||||
if !out.status.success() {
|
||||
return Err(String::from_utf8_lossy(&out.stderr).trim().to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user