Skip to content

Commit

Permalink
Login with device (#529)
Browse files Browse the repository at this point in the history
Login with device and approve device.
  • Loading branch information
Hinton authored Jan 24, 2024
1 parent 5958061 commit 8f31441
Show file tree
Hide file tree
Showing 11 changed files with 349 additions and 22 deletions.
9 changes: 5 additions & 4 deletions crates/bitwarden-crypto/src/enc_string/asymmetric.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use super::{from_b64_vec, split_enc_string};
use crate::{
error::{CryptoError, EncStringParseError, Result},
rsa::encrypt_rsa2048_oaep_sha1,
AsymmetricCryptoKey, KeyDecryptable,
AsymmetricCryptoKey, AsymmetricEncryptable, KeyDecryptable,
};

/// # Encrypted string primitive
Expand Down Expand Up @@ -138,11 +138,12 @@ impl serde::Serialize for AsymmetricEncString {
}

impl AsymmetricEncString {
pub(crate) fn encrypt_rsa2048_oaep_sha1(
/// Encrypt and produce a [AsymmetricEncString::Rsa2048_OaepSha1_B64] variant.
pub fn encrypt_rsa2048_oaep_sha1(
data_dec: &[u8],
key: &AsymmetricCryptoKey,
key: &dyn AsymmetricEncryptable,
) -> Result<AsymmetricEncString> {
let enc = encrypt_rsa2048_oaep_sha1(&key.key, data_dec)?;
let enc = encrypt_rsa2048_oaep_sha1(key.to_public_key(), data_dec)?;
Ok(AsymmetricEncString::Rsa2048_OaepSha1_B64 { data: enc })
}

Expand Down
104 changes: 99 additions & 5 deletions crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,45 @@
use rsa::RsaPrivateKey;
use rsa::{pkcs8::DecodePublicKey, RsaPrivateKey, RsaPublicKey};

use super::key_encryptable::CryptoKey;
use crate::error::{CryptoError, Result};

/// An asymmetric encryption key. Used to encrypt and decrypt [`EncString`](crate::EncString)
/// Trait to allow both [`AsymmetricCryptoKey`] and [`AsymmetricPublicCryptoKey`] to be used to
/// encrypt [AsymmetricEncString](crate::AsymmetricEncString).
pub trait AsymmetricEncryptable {
fn to_public_key(&self) -> &RsaPublicKey;
}

/// An asymmetric public encryption key. Can only encrypt
/// [AsymmetricEncString](crate::AsymmetricEncString), usually accompanied by a
/// [AsymmetricCryptoKey]
pub struct AsymmetricPublicCryptoKey {
pub(crate) key: RsaPublicKey,
}

impl AsymmetricPublicCryptoKey {
/// Build a public key from the SubjectPublicKeyInfo DER.
pub fn from_der(der: &[u8]) -> Result<Self> {
Ok(Self {
key: rsa::RsaPublicKey::from_public_key_der(der)
.map_err(|_| CryptoError::InvalidKey)?,
})
}
}

impl AsymmetricEncryptable for AsymmetricPublicCryptoKey {
fn to_public_key(&self) -> &RsaPublicKey {
&self.key
}
}

/// An asymmetric encryption key. Contains both the public and private key. Can be used to both
/// encrypt and decrypt [`AsymmetricEncString`](crate::AsymmetricEncString).
pub struct AsymmetricCryptoKey {
pub(crate) key: RsaPrivateKey,
}

impl AsymmetricCryptoKey {
/// Generate a random AsymmetricCryptoKey (RSA-2048)
/// Generate a random AsymmetricCryptoKey (RSA-2048).
pub fn generate<R: rand::CryptoRng + rand::RngCore>(rng: &mut R) -> Self {
let bits = 2048;

Expand Down Expand Up @@ -45,7 +75,6 @@ impl AsymmetricCryptoKey {
pub fn to_public_der(&self) -> Result<Vec<u8>> {
use rsa::pkcs8::EncodePublicKey;
Ok(self
.key
.to_public_key()
.to_public_key_der()
.map_err(|_| CryptoError::InvalidKey)?
Expand All @@ -54,6 +83,12 @@ impl AsymmetricCryptoKey {
}
}

impl AsymmetricEncryptable for AsymmetricCryptoKey {
fn to_public_key(&self) -> &RsaPublicKey {
self.key.as_ref()
}
}

impl CryptoKey for AsymmetricCryptoKey {}

// We manually implement these to make sure we don't print any sensitive data
Expand All @@ -67,7 +102,9 @@ impl std::fmt::Debug for AsymmetricCryptoKey {
mod tests {
use base64::{engine::general_purpose::STANDARD, Engine};

use super::AsymmetricCryptoKey;
use crate::{
AsymmetricCryptoKey, AsymmetricEncString, AsymmetricPublicCryptoKey, KeyDecryptable,
};

#[test]
fn test_asymmetric_crypto_key() {
Expand Down Expand Up @@ -111,4 +148,61 @@ DnqOsltgPomWZ7xVfMkm9niL2OA=
assert_eq!(der_key.to_der().unwrap(), der_key_vec);
assert_eq!(pem_key.to_der().unwrap(), der_key_vec);
}

#[test]
fn test_encrypt_public_decrypt_private() {
let private_key = STANDARD
.decode(concat!(
"MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCu9xd+vmkIPoqH",
"NejsFZzkd1xuCn1TqGTT7ANhAEnbI/yaVt3caI30kwUC2WIToFpNgu7Ej0x2TteY",
"OgrLrdcC4jy1SifmKYv/v3ZZxrd/eqttmH2k588panseRwHK3LVk7xA+URhQ/bjL",
"gPM59V0uR1l+z1fmooeJPFz5WSXNObc9Jqnh45FND+U/UYHXTLSomTn7jgZFxJBK",
"veS7q6Lat7wAnYZCF2dnPmhZoJv+SKPltA8HAGsgQGWBF1p5qxV1HrAUk8kBBnG2",
"paj0w8p5UM6RpDdCuvKH7j1LiuWffn3b9Z4dgzmE7jsMmvzoQtypzIKaSxhqzvFO",
"od9V8dJdAgMBAAECggEAGGIYjOIB1rOKkDHP4ljXutI0mCRPl3FMDemiBeppoIfZ",
"G/Q3qpAKmndDt0Quwh/yfcNdvZhf1kwCCTWri/uPz5fSUIyDV3TaTRu0ZWoHaBVj",
"Hxylg+4HRZUQj+Vi50/PWr/jQmAAVMcrMfcoTl82q2ynmP/R1vM3EsXOCjTliv5B",
"XlMPRjj/9PDBH0dnnVcAPDOpflzOTL2f4HTFEMlmg9/tZBnd96J/cmfhjAv9XpFL",
"FBAFZzs5pz0rwCNSR8QZNonnK7pngVUlGDLORK58y84tGmxZhGdne3CtCWey/sJ4",
"7QF0Pe8YqWBU56926IY6DcSVBuQGZ6vMCNlU7J8D2QKBgQDXyh3t2TicM/n1QBLk",
"zLoGmVUmxUGziHgl2dnJiGDtyOAU3+yCorPgFaCie29s5qm4b0YEGxUxPIrRrEro",
"h0FfKn9xmr8CdmTPTcjJW1+M7bxxq7oBoU/QzKXgIHlpeCjjnvPJt0PcNkNTjCXv",
"shsrINh2rENoe/x79eEfM/N5eQKBgQDPkYSmYyALoNq8zq0A4BdR+F5lb5Fj5jBH",
"Jk68l6Uti+0hRbJ2d1tQTLkU+eCPQLGBl6fuc1i4K5FV7v14jWtRPdD7wxrkRi3j",
"ilqQwLBOU6Bj3FK4DvlLF+iYTuBWj2/KcxflXECmsjitKHLK6H7kFEiuJql+NAHU",
"U9EFXepLBQKBgQDQ+HCnZ1bFHiiP8m7Zl9EGlvK5SwlnPV9s+F1KJ4IGhCNM09UM",
"ZVfgR9F5yCONyIrPiyK40ylgtwqQJlOcf281I8irUXpsfg7+Gou5Q31y0r9NLUpC",
"Td8niyePtqMdGjouxD2+OHXFCd+FRxFt4IMi7vnxYr0csAVAXkqWlw7PsQKBgH/G",
"/PnQm7GM3BrOwAGB8dksJDAddkshMScblezTDYP0V43b8firkTLliCo5iNum357/",
"VQmdSEhXyag07yR/Kklg3H2fpbZQ3X7tdMMXW3FcWagfwWw9C4oGtdDM/Z1Lv23J",
"XDR9je8QV4OBGul+Jl8RfYx3kG94ZIfo8Qt0vP5hAoGARjAzdCGYz42NwaUk8n94",
"W2RuKHtTV9vtjaAbfPFbZoGkT7sXNJVlrA0C+9f+H9rOTM3mX59KrjmLVzde4Vhs",
"avWMShuK4vpAiDQLU7GyABvi5CR6Ld+AT+LSzxHhVe0ASOQPNCA2SOz3RQvgPi7R",
"GDgRMUB6cL3IRVzcR0dC6cY=",
))
.unwrap();

let public_key = STANDARD
.decode(concat!(
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArvcXfr5pCD6KhzXo7BWc",
"5Hdcbgp9U6hk0+wDYQBJ2yP8mlbd3GiN9JMFAtliE6BaTYLuxI9Mdk7XmDoKy63X",
"AuI8tUon5imL/792Wca3f3qrbZh9pOfPKWp7HkcByty1ZO8QPlEYUP24y4DzOfVd",
"LkdZfs9X5qKHiTxc+VklzTm3PSap4eORTQ/lP1GB10y0qJk5+44GRcSQSr3ku6ui",
"2re8AJ2GQhdnZz5oWaCb/kij5bQPBwBrIEBlgRdaeasVdR6wFJPJAQZxtqWo9MPK",
"eVDOkaQ3Qrryh+49S4rln3592/WeHYM5hO47DJr86ELcqcyCmksYas7xTqHfVfHS",
"XQIDAQAB",
))
.unwrap();

let private_key = AsymmetricCryptoKey::from_der(&private_key).unwrap();
let public_key = AsymmetricPublicCryptoKey::from_der(&public_key).unwrap();

let plaintext = "Hello, world!";
let encrypted =
AsymmetricEncString::encrypt_rsa2048_oaep_sha1(plaintext.as_bytes(), &public_key)
.unwrap();
let decrypted: String = encrypted.decrypt_with_key(&private_key).unwrap();

assert_eq!(plaintext, decrypted);
}
}
5 changes: 4 additions & 1 deletion crates/bitwarden-crypto/src/keys/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ mod symmetric_crypto_key;
pub use symmetric_crypto_key::derive_symmetric_key;
pub use symmetric_crypto_key::SymmetricCryptoKey;
mod asymmetric_crypto_key;
pub use asymmetric_crypto_key::AsymmetricCryptoKey;
pub use asymmetric_crypto_key::{
AsymmetricCryptoKey, AsymmetricEncryptable, AsymmetricPublicCryptoKey,
};

mod user_key;
pub use user_key::UserKey;
mod device_key;
Expand Down
8 changes: 2 additions & 6 deletions crates/bitwarden-crypto/src/rsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,11 @@ pub(crate) fn make_key_pair(key: &SymmetricCryptoKey) -> Result<RsaKeyPair> {
})
}

pub(super) fn encrypt_rsa2048_oaep_sha1(
private_key: &RsaPrivateKey,
data: &[u8],
) -> Result<Vec<u8>> {
pub(super) fn encrypt_rsa2048_oaep_sha1(public_key: &RsaPublicKey, data: &[u8]) -> Result<Vec<u8>> {
let mut rng = rand::thread_rng();

let padding = Oaep::new::<Sha1>();
private_key
.to_public_key()
public_key
.encrypt(&mut rng, padding, data)
.map_err(|e| CryptoError::RsaError(e.into()))
}
22 changes: 20 additions & 2 deletions crates/bitwarden-uniffi/src/auth/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use std::sync::Arc;

use bitwarden::auth::{password::MasterPasswordPolicyOptions, RegisterKeyResponse};
use bitwarden_crypto::{HashPurpose, Kdf};
use bitwarden::auth::{
password::MasterPasswordPolicyOptions, AuthRequestResponse, RegisterKeyResponse,
};
use bitwarden_crypto::{AsymmetricEncString, HashPurpose, Kdf};

use crate::{error::Result, Client};

Expand Down Expand Up @@ -91,4 +93,20 @@ impl ClientAuth {
.validate_password(password, password_hash.to_string())
.await?)
}

/// Initialize a new auth request
pub async fn new_auth_request(&self, email: String) -> Result<AuthRequestResponse> {
Ok(self.0 .0.write().await.auth().new_auth_request(&email)?)
}

/// Approve an auth request
pub async fn approve_auth_request(&self, public_key: String) -> Result<AsymmetricEncString> {
Ok(self
.0
.0
.write()
.await
.auth()
.approve_auth_request(public_key)?)
}
}
3 changes: 2 additions & 1 deletion crates/bitwarden-uniffi/src/uniffi_support.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use bitwarden_crypto::EncString;
use bitwarden_crypto::{AsymmetricEncString, EncString};

// Forward the type definitions to the main bitwarden crate
type DateTime = chrono::DateTime<chrono::Utc>;
uniffi::ffi_converter_forward!(DateTime, bitwarden::UniFfiTag, crate::UniFfiTag);
uniffi::ffi_converter_forward!(EncString, bitwarden::UniFfiTag, crate::UniFfiTag);
uniffi::ffi_converter_forward!(AsymmetricEncString, bitwarden::UniFfiTag, crate::UniFfiTag);
137 changes: 137 additions & 0 deletions crates/bitwarden/src/auth/auth_request.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
use base64::{engine::general_purpose::STANDARD, Engine};
use bitwarden_crypto::{
fingerprint, AsymmetricCryptoKey, AsymmetricEncString, AsymmetricPublicCryptoKey,
};
#[cfg(feature = "mobile")]
use bitwarden_crypto::{KeyDecryptable, SymmetricCryptoKey};
use bitwarden_generators::{password, PasswordGeneratorRequest};

use crate::{error::Error, Client};

#[cfg_attr(feature = "mobile", derive(uniffi::Record))]
pub struct AuthRequestResponse {
/// Base64 encoded private key
/// This key is temporarily passed back and will most likely not be available in the future
pub private_key: String,
/// Base64 encoded public key
pub public_key: String,
/// Fingerprint of the public key
pub fingerprint: String,
/// Access code
pub access_code: String,
}

/// Initiate a new auth request.
///
/// Generates a private key and access code. The pulic key is uploaded to the server and transmitted
/// to another device. Where the user confirms the validity by confirming the fingerprint. The user
/// key is then encrypted using the public key and returned to the initiating device.
pub(crate) fn new_auth_request(email: &str) -> Result<AuthRequestResponse, Error> {
let mut rng = rand::thread_rng();

let key = AsymmetricCryptoKey::generate(&mut rng);

let spki = key.to_public_der()?;

let fingerprint = fingerprint(email, &spki)?;
let b64 = STANDARD.encode(&spki);

Ok(AuthRequestResponse {
private_key: STANDARD.encode(key.to_der()?),
public_key: b64,
fingerprint,
access_code: password(PasswordGeneratorRequest {
length: 25,
lowercase: true,
uppercase: true,
numbers: true,
special: false,
..Default::default()
})?,
})
}

/// Decrypt the user key using the private key generated previously.
#[cfg(feature = "mobile")]
pub(crate) fn auth_request_decrypt_user_key(
private_key: String,
user_key: AsymmetricEncString,
) -> Result<SymmetricCryptoKey, Error> {
let key = AsymmetricCryptoKey::from_der(&STANDARD.decode(private_key)?)?;
let key: String = user_key.decrypt_with_key(&key)?;

Ok(key.parse()?)
}

/// Approve an auth request.
///
/// Encrypts the user key with a public key.
pub(crate) fn approve_auth_request(
client: &mut Client,
public_key: String,
) -> Result<AsymmetricEncString, Error> {
let public_key = AsymmetricPublicCryptoKey::from_der(&STANDARD.decode(public_key)?)?;

let enc = client.get_encryption_settings()?;
let key = enc.get_key(&None).ok_or(Error::VaultLocked)?;

Ok(AsymmetricEncString::encrypt_rsa2048_oaep_sha1(
&key.to_vec(),
&public_key,
)?)
}

#[test]
fn test_auth_request() {
let request = new_auth_request("[email protected]").unwrap();

let secret =
"w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==";

let private_key =
AsymmetricCryptoKey::from_der(&STANDARD.decode(&request.private_key).unwrap()).unwrap();

let encrypted =
AsymmetricEncString::encrypt_rsa2048_oaep_sha1(secret.as_bytes(), &private_key).unwrap();

let decrypted = auth_request_decrypt_user_key(request.private_key, encrypted).unwrap();

assert_eq!(decrypted.to_base64(), secret);
}

#[cfg(test)]
mod tests {
use std::num::NonZeroU32;

use bitwarden_crypto::Kdf;

use super::*;
use crate::client::{LoginMethod, UserLoginMethod};

#[test]
fn test_approve() {
let mut client = Client::new(None);
client.set_login_method(LoginMethod::User(UserLoginMethod::Username {
client_id: "123".to_owned(),
email: "[email protected]".to_owned(),
kdf: Kdf::PBKDF2 {
iterations: NonZeroU32::new(600_000).unwrap(),
},
}));

let user_key = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=".parse().unwrap();
let private_key ="2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4=".parse().unwrap();
client
.initialize_user_crypto("asdfasdfasdf", user_key, private_key)
.unwrap();

let public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnRtpYLp9QLaEUkdPkWZX6TrMUKFoSaFamBKDL0NlS6xwtETTqYIxRVsvnHii3Dhz+fh3aHQVyBa1rBXogeH3MLERzNADwZhpWtBT9wKCXY5o0fIWYdZV/Nf0Y+0ZoKdImrGPLPmyHGfCqrvrK7g09q8+3kXUlkdAImlQqc5TiYwiHBfUQVTBq/Ae7a0FEpajx1NUM4h3edpCYxbvnpSTuzMgbmbUUS4gdCaheA2ibYxy/zkLzsaLygoibMyGNl9Y8J5n7dDrVXpUKZTihVfXwHfEZwtKNunWsmmt8rEJWVpguUDEDVSUogoxQcNaCi7KHn9ioSip76hg1jLpypO3WwIDAQAB";

// Verify fingerprint
let pbkey = STANDARD.decode(public_key).unwrap();
let fingerprint = fingerprint("[email protected]", &pbkey).unwrap();
assert_eq!(fingerprint, "spill-applaud-sweep-habitable-shrunk");

approve_auth_request(&mut client, public_key.to_owned()).unwrap();
}
}
Loading

0 comments on commit 8f31441

Please sign in to comment.