mirror of
https://github.com/bitwarden/browser
synced 2026-01-27 06:43:41 +00:00
SSH Agent v2: Add ssh key primitive types
Co-authored-by: Bernd Schoolmann <mail@quexten.com>
This commit is contained in:
16
apps/desktop/desktop_native/Cargo.lock
generated
16
apps/desktop/desktop_native/Cargo.lock
generated
@@ -351,9 +351,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.7.3"
|
||||
version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3"
|
||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||
|
||||
[[package]]
|
||||
name = "basic-toml"
|
||||
@@ -3083,6 +3083,9 @@ dependencies = [
|
||||
"bcrypt-pbkdf",
|
||||
"ed25519-dalek",
|
||||
"num-bigint-dig",
|
||||
"p256",
|
||||
"p384",
|
||||
"p521",
|
||||
"rand_core 0.6.4",
|
||||
"rsa",
|
||||
"sec1",
|
||||
@@ -3094,6 +3097,15 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ssh_agent"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
"ssh-key",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
|
||||
@@ -9,6 +9,7 @@ members = [
|
||||
"napi",
|
||||
"process_isolation",
|
||||
"proxy",
|
||||
"ssh_agent",
|
||||
"windows_plugin_authenticator",
|
||||
]
|
||||
|
||||
|
||||
19
apps/desktop/desktop_native/ssh_agent/Cargo.toml
Normal file
19
apps/desktop/desktop_native/ssh_agent/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "ssh_agent"
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
version = { workspace = true }
|
||||
publish = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
ssh-key = { version = "=0.6.7", features = [
|
||||
"encryption",
|
||||
"ed25519",
|
||||
"rsa",
|
||||
"rand_core",
|
||||
] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
193
apps/desktop/desktop_native/ssh_agent/src/crypto/mod.rs
Normal file
193
apps/desktop/desktop_native/ssh_agent/src/crypto/mod.rs
Normal file
@@ -0,0 +1,193 @@
|
||||
//! Cryptographic key management for the SSH agent.
|
||||
//!
|
||||
//! This module provides the core primative types and functionality for managing
|
||||
//! SSH keys in the Bitwarden SSH agent.
|
||||
//!
|
||||
//! The module exposes several key types:
|
||||
//!
|
||||
//! - [`KeyData`] - Complete SSH key with metadata (private key, public key, name, cipher ID)
|
||||
//! - [`PrivateKey`] - SSH private key (Ed25519 or RSA)
|
||||
//! - [`PublicKey`] - SSH public key with algorithm and blob data
|
||||
//!
|
||||
//! # Supported Algorithms
|
||||
//!
|
||||
//! - Ed25519 keys
|
||||
//! - RSA keys
|
||||
//!
|
||||
//! ECDSA keys are not currently supported (PM-29894)
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use ssh_key::private::{Ed25519Keypair, RsaKeypair};
|
||||
|
||||
/// Represents an SSH key with its associated metadata.
|
||||
///
|
||||
/// Contains the private and public components of an SSH key,
|
||||
/// along with a human-readable name and the cipher ID from the vault.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct KeyData {
|
||||
private_key: PrivateKey,
|
||||
public_key: PublicKey,
|
||||
name: String,
|
||||
cipher_id: String,
|
||||
}
|
||||
|
||||
impl KeyData {
|
||||
/// Creates a new `KeyData` instance.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `private_key` - The private key component (Ed25519 or RSA)
|
||||
/// * `public_key` - The public key component with algorithm and blob
|
||||
/// * `name` - A human-readable name for the key
|
||||
/// * `cipher_id` - The vault cipher identifier associated with this key
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A new `KeyData` instance containing all provided key data and metadata.
|
||||
pub(crate) fn new(
|
||||
private_key: PrivateKey,
|
||||
public_key: PublicKey,
|
||||
name: String,
|
||||
cipher_id: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
private_key,
|
||||
public_key,
|
||||
name,
|
||||
cipher_id,
|
||||
}
|
||||
}
|
||||
|
||||
/// # Returns
|
||||
///
|
||||
/// A reference to the [`PublicKey`] containing the algorithm and blob.
|
||||
pub(crate) fn public_key(&self) -> &PublicKey {
|
||||
&self.public_key
|
||||
}
|
||||
|
||||
/// # Returns
|
||||
///
|
||||
/// A reference to the [`PrivateKey`] enum variant (Ed25519 or RSA).
|
||||
pub(crate) fn private_key(&self) -> &PrivateKey {
|
||||
&self.private_key
|
||||
}
|
||||
|
||||
/// # Returns
|
||||
///
|
||||
/// A reference to the human-readable name string for this key.
|
||||
pub(crate) fn name(&self) -> &String {
|
||||
&self.name
|
||||
}
|
||||
|
||||
/// # Returns
|
||||
///
|
||||
/// A reference to the cipher ID string that links this key to a vault entry.
|
||||
pub(crate) fn cipher_id(&self) -> &String {
|
||||
&self.cipher_id
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an SSH private key.
|
||||
///
|
||||
/// Supported key types: Ed25519, RSA.
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub(crate) enum PrivateKey {
|
||||
Ed25519(Ed25519Keypair),
|
||||
Rsa(RsaKeypair),
|
||||
}
|
||||
|
||||
impl TryFrom<ssh_key::private::PrivateKey> for PrivateKey {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(key: ssh_key::private::PrivateKey) -> Result<Self, Self::Error> {
|
||||
match key.algorithm() {
|
||||
ssh_key::Algorithm::Ed25519 => Ok(Self::Ed25519(
|
||||
key.key_data()
|
||||
.ed25519()
|
||||
.ok_or(anyhow!("Failed to parse ed25519 key"))?
|
||||
.to_owned(),
|
||||
)),
|
||||
ssh_key::Algorithm::Rsa { hash: _ } => Ok(Self::Rsa(
|
||||
key.key_data()
|
||||
.rsa()
|
||||
.ok_or(anyhow!("Failed to parse RSA key"))?
|
||||
.to_owned(),
|
||||
)),
|
||||
_ => Err(anyhow!("Unsupported key type")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an SSH public key.
|
||||
///
|
||||
/// Contains the algorithm identifier (e.g., "ssh-ed25519", "ssh-rsa")
|
||||
/// and the binary blob of the public key data.
|
||||
#[derive(Clone, Ord, Eq, PartialOrd, PartialEq)]
|
||||
pub(crate) struct PublicKey {
|
||||
pub alg: String,
|
||||
pub blob: Vec<u8>,
|
||||
}
|
||||
|
||||
impl PublicKey {
|
||||
pub(crate) fn alg(&self) -> &str {
|
||||
&self.alg
|
||||
}
|
||||
|
||||
pub(crate) fn blob(&self) -> &[u8] {
|
||||
&self.blob
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for PublicKey {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "PublicKey(\"{self}\")")
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PublicKey {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use base64::{prelude::BASE64_STANDARD, Engine as _};
|
||||
|
||||
write!(f, "{} {}", self.alg(), BASE64_STANDARD.encode(self.blob()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ssh_key::{
|
||||
private::{Ed25519Keypair, RsaKeypair},
|
||||
rand_core::OsRng,
|
||||
LineEnding,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn create_valid_ed25519_key_string() -> String {
|
||||
let ed25519_keypair = Ed25519Keypair::random(&mut OsRng);
|
||||
let ssh_key =
|
||||
ssh_key::PrivateKey::new(ssh_key::private::KeypairData::Ed25519(ed25519_keypair), "")
|
||||
.unwrap();
|
||||
ssh_key.to_openssh(LineEnding::LF).unwrap().to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_privatekey_from_ed25519() {
|
||||
let key_string = create_valid_ed25519_key_string();
|
||||
let ssh_key = ssh_key::PrivateKey::from_openssh(&key_string).unwrap();
|
||||
|
||||
let private_key = PrivateKey::try_from(ssh_key).unwrap();
|
||||
assert!(matches!(private_key, PrivateKey::Ed25519(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_privatekey_from_rsa() {
|
||||
let rsa_keypair = RsaKeypair::random(&mut OsRng, 2048).unwrap();
|
||||
let ssh_key =
|
||||
ssh_key::PrivateKey::new(ssh_key::private::KeypairData::Rsa(rsa_keypair), "").unwrap();
|
||||
|
||||
let private_key = PrivateKey::try_from(ssh_key).unwrap();
|
||||
assert!(matches!(private_key, PrivateKey::Rsa(_)));
|
||||
}
|
||||
}
|
||||
7
apps/desktop/desktop_native/ssh_agent/src/lib.rs
Normal file
7
apps/desktop/desktop_native/ssh_agent/src/lib.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
//! Bitwarden SSH Agent implementation
|
||||
//!
|
||||
//! <https://www.ietf.org/archive/id/draft-miller-ssh-agent-11.html#RFC4253>
|
||||
|
||||
#![allow(dead_code)] // TODO remove when all code is used in follow-up PR
|
||||
|
||||
mod crypto;
|
||||
Reference in New Issue
Block a user