use base64::{Engine as _, engine::general_purpose};
use ed25519_dalek::{
Signature as Ed25519Signature, Signer as _, SigningKey, Verifier as _, VerifyingKey,
pkcs8::{
DecodePrivateKey as _, DecodePublicKey as _, EncodePrivateKey as _, EncodePublicKey as _,
},
};
use rsa::{
Oaep, RsaPrivateKey, RsaPublicKey,
pkcs8::{DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey},
pss::{BlindedSigningKey, Signature as RsaPssSignature, VerifyingKey as RsaPssVerifyingKey},
rand_core::OsRng as LegacyOsRng,
sha2::{Digest, Sha256},
signature::{RandomizedSigner, SignatureEncoding, Verifier},
};
use crate::domain::{error::DotLockError, model::DotLockResult};
pub const RECIPIENT_ALG: &str = "rsa-oaep-sha256";
pub const RECIPIENT_ALG_X25519: &str = "x25519-sealedbox";
pub const IDENTITY_ALG_ED25519: &str = "ed25519";
pub const IDENTITY_ALG_RSA: &str = "rsa-3072";
pub struct GeneratedIdentity {
pub private_key_pem: String,
pub public_key_pem: String,
pub fingerprint: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IdentityProtection<'a> {
Encrypted(&'a str),
Plain,
}
enum PrivateIdentityKey {
Ed25519(Box<SigningKey>),
Rsa(Box<RsaPrivateKey>),
}
enum PublicIdentityKey {
Ed25519(Box<VerifyingKey>),
Rsa(Box<RsaPublicKey>),
}
fn parse_private_key(private_key_pem: &str) -> DotLockResult<PrivateIdentityKey> {
if let Ok(signing_key) = SigningKey::from_pkcs8_pem(private_key_pem) {
return Ok(PrivateIdentityKey::Ed25519(Box::new(signing_key)));
}
RsaPrivateKey::from_pkcs8_pem(private_key_pem)
.map(|key| PrivateIdentityKey::Rsa(Box::new(key)))
.map_err(|e| DotLockError::Crypto(format!("failed to parse private key: {e}")))
}
fn parse_public_key_pem(public_key_pem: &str) -> DotLockResult<PublicIdentityKey> {
if let Ok(verifying_key) = VerifyingKey::from_public_key_pem(public_key_pem) {
return Ok(PublicIdentityKey::Ed25519(Box::new(verifying_key)));
}
RsaPublicKey::from_public_key_pem(public_key_pem)
.map(|key| PublicIdentityKey::Rsa(Box::new(key)))
.map_err(|e| DotLockError::Crypto(format!("failed to parse public key: {e}")))
}
fn parse_public_key_der(der: &[u8]) -> DotLockResult<PublicIdentityKey> {
if let Ok(verifying_key) = VerifyingKey::from_public_key_der(der) {
return Ok(PublicIdentityKey::Ed25519(Box::new(verifying_key)));
}
RsaPublicKey::from_public_key_der(der)
.map(|key| PublicIdentityKey::Rsa(Box::new(key)))
.map_err(|e| DotLockError::Crypto(format!("failed to parse public key: {e}")))
}
fn parse_public_key_b64(public_key_b64: &str) -> DotLockResult<PublicIdentityKey> {
let der = general_purpose::STANDARD
.decode(public_key_b64)
.map_err(|e| DotLockError::Crypto(format!("failed to decode public key: {e}")))?;
parse_public_key_der(&der)
}
impl PublicIdentityKey {
fn to_der(&self) -> DotLockResult<Vec<u8>> {
match self {
PublicIdentityKey::Ed25519(key) => key
.to_public_key_der()
.map(|der| der.as_ref().to_vec())
.map_err(|e| DotLockError::Crypto(format!("failed to encode public key: {e}"))),
PublicIdentityKey::Rsa(key) => key
.to_public_key_der()
.map(|der| der.as_ref().to_vec())
.map_err(|e| DotLockError::Crypto(format!("failed to encode public key: {e}"))),
}
}
fn recipient_alg(&self) -> &'static str {
match self {
PublicIdentityKey::Ed25519(_) => RECIPIENT_ALG_X25519,
PublicIdentityKey::Rsa(_) => RECIPIENT_ALG,
}
}
}
pub fn generate_identity(protection: IdentityProtection<'_>) -> DotLockResult<GeneratedIdentity> {
let mut seed = zeroize::Zeroizing::new([0u8; 32]);
getrandom::fill(seed.as_mut())
.map_err(|e| DotLockError::Crypto(format!("failed to generate identity key: {e}")))?;
let signing_key = SigningKey::from_bytes(&seed);
let private_key_pem = match protection {
IdentityProtection::Encrypted(passphrase) => signing_key
.to_pkcs8_encrypted_pem(passphrase, pkcs8::LineEnding::LF)
.map_err(|e| DotLockError::Crypto(format!("failed to encode private key: {e}")))?
.to_string(),
IdentityProtection::Plain => signing_key
.to_pkcs8_pem(pkcs8::LineEnding::LF)
.map_err(|e| DotLockError::Crypto(format!("failed to encode private key: {e}")))?
.to_string(),
};
let public_key_pem = signing_key
.verifying_key()
.to_public_key_pem(pkcs8::LineEnding::LF)
.map_err(|e| DotLockError::Crypto(format!("failed to encode public key: {e}")))?;
let fingerprint = fingerprint_public_key(&public_key_pem)?;
Ok(GeneratedIdentity {
private_key_pem,
public_key_pem,
fingerprint,
})
}
#[cfg(test)]
pub(crate) fn generate_legacy_rsa_identity(
protection: IdentityProtection<'_>,
) -> DotLockResult<GeneratedIdentity> {
let mut rng = LegacyOsRng;
let private_key = RsaPrivateKey::new(&mut rng, 2048)
.map_err(|e| DotLockError::Crypto(format!("failed to generate identity key: {e}")))?;
let public_key = RsaPublicKey::from(&private_key);
let private_key_pem = match protection {
IdentityProtection::Encrypted(passphrase) => private_key
.to_pkcs8_encrypted_pem(&mut rng, passphrase, rsa::pkcs8::LineEnding::LF)
.map_err(|e| DotLockError::Crypto(format!("failed to encode private key: {e}")))?
.to_string(),
IdentityProtection::Plain => private_key
.to_pkcs8_pem(rsa::pkcs8::LineEnding::LF)
.map_err(|e| DotLockError::Crypto(format!("failed to encode private key: {e}")))?
.to_string(),
};
let public_key_pem = public_key
.to_public_key_pem(rsa::pkcs8::LineEnding::LF)
.map_err(|e| DotLockError::Crypto(format!("failed to encode public key: {e}")))?;
let fingerprint = fingerprint_public_key(&public_key_pem)?;
Ok(GeneratedIdentity {
private_key_pem,
public_key_pem,
fingerprint,
})
}
pub fn decrypt_private_key_pem(
encrypted_private_key_pem: &str,
passphrase: &str,
) -> DotLockResult<String> {
if let Ok(signing_key) =
SigningKey::from_pkcs8_encrypted_pem(encrypted_private_key_pem, passphrase)
{
return signing_key
.to_pkcs8_pem(pkcs8::LineEnding::LF)
.map_err(|e| DotLockError::Crypto(format!("failed to encode private key: {e}")))
.map(|pem| pem.to_string());
}
let private_key =
RsaPrivateKey::from_pkcs8_encrypted_pem(encrypted_private_key_pem, passphrase)
.map_err(|_| DotLockError::InvalidIdentityPassphrase)?;
private_key
.to_pkcs8_pem(rsa::pkcs8::LineEnding::LF)
.map_err(|e| DotLockError::Crypto(format!("failed to encode private key: {e}")))
.map(|pem| pem.to_string())
}
pub fn identity_alg_for_private_key(private_key_pem: &str) -> DotLockResult<&'static str> {
Ok(match parse_private_key(private_key_pem)? {
PrivateIdentityKey::Ed25519(_) => IDENTITY_ALG_ED25519,
PrivateIdentityKey::Rsa(_) => IDENTITY_ALG_RSA,
})
}
pub fn recipient_alg_for_public_key(public_key_pem: &str) -> DotLockResult<&'static str> {
Ok(parse_public_key_pem(public_key_pem)?.recipient_alg())
}
pub fn recipient_alg_for_public_key_b64(public_key_b64: &str) -> DotLockResult<&'static str> {
Ok(parse_public_key_b64(public_key_b64)?.recipient_alg())
}
pub fn fingerprint_public_key(public_key_pem: &str) -> DotLockResult<String> {
let der = parse_public_key_pem(public_key_pem)?.to_der()?;
let digest = Sha256::digest(&der);
Ok(hex_lower(&digest[..16]))
}
pub fn encode_public_key_b64(public_key_pem: &str) -> DotLockResult<String> {
let der = parse_public_key_pem(public_key_pem)?.to_der()?;
Ok(general_purpose::STANDARD.encode(der))
}
fn wrap_for_parsed_key(data: &[u8; 32], public_key: &PublicIdentityKey) -> DotLockResult<String> {
match public_key {
PublicIdentityKey::Ed25519(verifying_key) => {
let curve_public =
crypto_box::PublicKey::from(verifying_key.to_montgomery().to_bytes());
let sealed = curve_public
.seal(&mut rand_core::OsRng, data)
.map_err(|_| DotLockError::Crypto("failed to wrap project key".to_string()))?;
Ok(general_purpose::STANDARD.encode(sealed))
}
PublicIdentityKey::Rsa(rsa_public) => {
let mut rng = LegacyOsRng;
let encrypted = rsa_public
.encrypt(&mut rng, Oaep::new::<Sha256>(), data)
.map_err(|e| DotLockError::Crypto(format!("failed to wrap project key: {e}")))?;
Ok(general_purpose::STANDARD.encode(encrypted))
}
}
}
pub fn wrap_dek_for_public_key(dek: &[u8; 32], public_key_pem: &str) -> DotLockResult<String> {
wrap_for_parsed_key(dek, &parse_public_key_pem(public_key_pem)?)
}
pub fn wrap_dek_for_public_key_b64(dek: &[u8; 32], public_key_b64: &str) -> DotLockResult<String> {
wrap_for_parsed_key(dek, &parse_public_key_b64(public_key_b64)?)
}
pub fn unwrap_dek_with_private_key(
wrapped_dek_b64: &str,
private_key_pem: &str,
) -> DotLockResult<[u8; 32]> {
let wrapped = general_purpose::STANDARD
.decode(wrapped_dek_b64)
.map_err(|e| DotLockError::Crypto(format!("failed to decode wrapped project key: {e}")))?;
let decrypted = match parse_private_key(private_key_pem)? {
PrivateIdentityKey::Ed25519(signing_key) => {
let curve_secret = crypto_box::SecretKey::from(signing_key.to_scalar_bytes());
curve_secret
.unseal(&wrapped)
.map_err(|_| DotLockError::Crypto("failed to unwrap project key".to_string()))?
}
PrivateIdentityKey::Rsa(private_key) => private_key
.decrypt(Oaep::new::<Sha256>(), &wrapped)
.map_err(|e| DotLockError::Crypto(format!("failed to unwrap project key: {e}")))?,
};
decrypted
.try_into()
.map_err(|_| DotLockError::Crypto("invalid project key size".to_string()))
}
fn sign_payload(payload: &[u8], private_key_pem: &str) -> DotLockResult<String> {
match parse_private_key(private_key_pem)? {
PrivateIdentityKey::Ed25519(signing_key) => {
let signature = signing_key.sign(payload);
Ok(general_purpose::STANDARD.encode(signature.to_bytes()))
}
PrivateIdentityKey::Rsa(private_key) => {
let signing_key = BlindedSigningKey::<Sha256>::new(*private_key);
let mut rng = LegacyOsRng;
let signature = signing_key.sign_with_rng(&mut rng, payload);
Ok(general_purpose::STANDARD.encode(signature.to_bytes()))
}
}
}
fn verify_payload(
payload: &[u8],
signature_b64: &str,
public_key: &PublicIdentityKey,
what: &str,
) -> DotLockResult<()> {
let signature = general_purpose::STANDARD
.decode(signature_b64)
.map_err(|e| DotLockError::Crypto(format!("failed to decode {what} signature: {e}")))?;
match public_key {
PublicIdentityKey::Ed25519(verifying_key) => {
let signature = Ed25519Signature::from_slice(&signature).map_err(|e| {
DotLockError::Crypto(format!("failed to parse {what} signature: {e}"))
})?;
verifying_key
.verify(payload, &signature)
.map_err(|_| DotLockError::Crypto(format!("{what} signature invalid")))
}
PublicIdentityKey::Rsa(rsa_public) => {
let verifying_key = RsaPssVerifyingKey::<Sha256>::new((**rsa_public).clone());
let signature = RsaPssSignature::try_from(signature.as_slice()).map_err(|e| {
DotLockError::Crypto(format!("failed to parse {what} signature: {e}"))
})?;
verifying_key
.verify(payload, &signature)
.map_err(|_| DotLockError::Crypto(format!("{what} signature invalid")))
}
}
}
pub fn sign_audit_entry_hash(entry_hash: &str, private_key_pem: &str) -> DotLockResult<String> {
sign_payload(entry_hash.as_bytes(), private_key_pem)
}
pub fn verify_audit_entry_hash_signature(
entry_hash: &str,
signature_b64: &str,
public_key_pem: &str,
) -> DotLockResult<()> {
verify_payload(
entry_hash.as_bytes(),
signature_b64,
&parse_public_key_pem(public_key_pem)?,
"audit",
)
}
pub fn sign_recipient_grant(payload: &[u8], private_key_pem: &str) -> DotLockResult<String> {
sign_payload(payload, private_key_pem)
}
pub fn verify_recipient_grant(
payload: &[u8],
signature_b64: &str,
public_key_b64: &str,
) -> DotLockResult<()> {
verify_payload(
payload,
signature_b64,
&parse_public_key_b64(public_key_b64)?,
"recipient grant",
)
}
fn hex_lower(bytes: &[u8]) -> String {
let mut output = String::with_capacity(bytes.len() * 2);
for byte in bytes {
use std::fmt::Write as _;
let _ = write!(&mut output, "{byte:02x}");
}
output
}
#[cfg(test)]
mod tests {
use super::{
IDENTITY_ALG_ED25519, IDENTITY_ALG_RSA, IdentityProtection, RECIPIENT_ALG,
RECIPIENT_ALG_X25519, decrypt_private_key_pem, encode_public_key_b64, generate_identity,
generate_legacy_rsa_identity, identity_alg_for_private_key, recipient_alg_for_public_key,
recipient_alg_for_public_key_b64, sign_audit_entry_hash, sign_recipient_grant,
unwrap_dek_with_private_key, verify_audit_entry_hash_signature, verify_recipient_grant,
wrap_dek_for_public_key, wrap_dek_for_public_key_b64,
};
#[test]
fn generated_identity_wraps_and_unwraps_project_key() {
let identity =
generate_identity(IdentityProtection::Encrypted("hunter2")).expect("identity");
let dek = [7u8; 32];
let private_key_pem =
decrypt_private_key_pem(&identity.private_key_pem, "hunter2").expect("decrypt pem");
let wrapped = wrap_dek_for_public_key(&dek, &identity.public_key_pem).expect("wrap");
let unwrapped = unwrap_dek_with_private_key(&wrapped, &private_key_pem).expect("unwrap");
assert_eq!(unwrapped, dek);
assert!(identity.private_key_pem.contains("ENCRYPTED PRIVATE KEY"));
}
#[test]
fn new_identities_are_ed25519_never_rsa() {
let identity = generate_identity(IdentityProtection::Plain).expect("identity");
assert_eq!(
identity_alg_for_private_key(&identity.private_key_pem).expect("alg"),
IDENTITY_ALG_ED25519
);
assert_eq!(
recipient_alg_for_public_key(&identity.public_key_pem).expect("alg"),
RECIPIENT_ALG_X25519
);
assert!(identity.private_key_pem.len() < 400);
let wrapped = wrap_dek_for_public_key(&[7u8; 32], &identity.public_key_pem).expect("wrap");
use base64::Engine as _;
let sealed = base64::engine::general_purpose::STANDARD
.decode(&wrapped)
.expect("b64");
assert_eq!(sealed.len(), 32 + 16 + 32);
}
#[test]
fn public_key_b64_roundtrip_wraps_project_key() {
let identity =
generate_identity(IdentityProtection::Encrypted("hunter2")).expect("identity");
let dek = [9u8; 32];
let public_key_b64 = encode_public_key_b64(&identity.public_key_pem).expect("encode");
let private_key_pem =
decrypt_private_key_pem(&identity.private_key_pem, "hunter2").expect("decrypt pem");
let wrapped = wrap_dek_for_public_key_b64(&dek, &public_key_b64).expect("wrap");
let unwrapped = unwrap_dek_with_private_key(&wrapped, &private_key_pem).expect("unwrap");
assert_eq!(unwrapped, dek);
assert_eq!(
recipient_alg_for_public_key_b64(&public_key_b64).expect("alg"),
RECIPIENT_ALG_X25519
);
}
#[test]
fn plain_identity_generates_unencrypted_private_key() {
let identity = generate_identity(IdentityProtection::Plain).expect("identity");
assert!(identity.private_key_pem.contains("BEGIN PRIVATE KEY"));
assert!(!identity.private_key_pem.contains("ENCRYPTED PRIVATE KEY"));
}
#[test]
fn audit_signature_roundtrip() {
let identity = generate_identity(IdentityProtection::Plain).expect("identity");
let signature =
sign_audit_entry_hash("sha256:test", &identity.private_key_pem).expect("sign");
verify_audit_entry_hash_signature("sha256:test", &signature, &identity.public_key_pem)
.expect("verify");
use base64::Engine as _;
let raw = base64::engine::general_purpose::STANDARD
.decode(&signature)
.expect("b64");
assert_eq!(raw.len(), 64);
}
#[test]
fn legacy_rsa_identity_still_works_through_the_same_api() {
let identity = generate_legacy_rsa_identity(IdentityProtection::Encrypted("hunter2"))
.expect("legacy identity");
let private_key_pem =
decrypt_private_key_pem(&identity.private_key_pem, "hunter2").expect("decrypt pem");
assert_eq!(
identity_alg_for_private_key(&private_key_pem).expect("alg"),
IDENTITY_ALG_RSA
);
assert_eq!(
recipient_alg_for_public_key(&identity.public_key_pem).expect("alg"),
RECIPIENT_ALG
);
let dek = [3u8; 32];
let wrapped = wrap_dek_for_public_key(&dek, &identity.public_key_pem).expect("wrap");
assert_eq!(
unwrap_dek_with_private_key(&wrapped, &private_key_pem).expect("unwrap"),
dek
);
let signature = sign_audit_entry_hash("sha256:test", &private_key_pem).expect("sign");
verify_audit_entry_hash_signature("sha256:test", &signature, &identity.public_key_pem)
.expect("verify");
}
#[test]
fn mixed_rsa_and_ed25519_recipients_resolve_per_key() {
let modern = generate_identity(IdentityProtection::Plain).expect("modern");
let legacy =
generate_legacy_rsa_identity(IdentityProtection::Plain).expect("legacy identity");
let dek = [5u8; 32];
let for_modern = wrap_dek_for_public_key(&dek, &modern.public_key_pem).expect("wrap");
let for_legacy = wrap_dek_for_public_key(&dek, &legacy.public_key_pem).expect("wrap");
assert_eq!(
unwrap_dek_with_private_key(&for_modern, &modern.private_key_pem).expect("unwrap"),
dek
);
assert_eq!(
unwrap_dek_with_private_key(&for_legacy, &legacy.private_key_pem).expect("unwrap"),
dek
);
assert!(unwrap_dek_with_private_key(&for_modern, &legacy.private_key_pem).is_err());
assert!(unwrap_dek_with_private_key(&for_legacy, &modern.private_key_pem).is_err());
for identity in [&modern, &legacy] {
let public_key_b64 =
encode_public_key_b64(&identity.public_key_pem).expect("encode public key");
let signature =
sign_recipient_grant(b"payload", &identity.private_key_pem).expect("sign");
verify_recipient_grant(b"payload", &signature, &public_key_b64).expect("verify");
assert!(verify_recipient_grant(b"tampered", &signature, &public_key_b64).is_err());
}
}
}