Skip to content

Commit

Permalink
[PM-5691] Extract crypto to separate crate (#402)
Browse files Browse the repository at this point in the history
We should make an effort to break up the large `bitwarden` crate into
smaller more isolated crates. This extracts the cryptographic primitives
into a crate, which better isolates them and may improve compile time
slightly. We should continue to extract further areas.
  • Loading branch information
Hinton authored Jan 15, 2024
1 parent 1028efd commit d2cb4f9
Show file tree
Hide file tree
Showing 87 changed files with 737 additions and 600 deletions.
1 change: 1 addition & 0 deletions .github/workflows/build-rust-crates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ jobs:

package:
- bitwarden
- bitwarden-crypto
- bitwarden-api-api
- bitwarden-api-identity

Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/publish-rust-crates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ on:
required: true
default: true
type: boolean
publish_bitwarden-crypto:
description: "Publish bitwarden-crypto crate"
required: true
default: true
type: boolean

defaults:
run:
Expand Down Expand Up @@ -61,6 +66,7 @@ jobs:
PUBLISH_BITWARDEN: ${{ github.event.inputs.publish_bitwarden }}
PUBLISH_BITWARDEN_API_API: ${{ github.event.inputs.publish_bitwarden-api-api }}
PUBLISH_BITWARDEN_API_IDENTITY: ${{ github.event.inputs.publish_bitwarden-api-identity }}
PUBLISH_BITWARDEN_CRYPTO: ${{ github.event.inputs.publish_bitwarden-crypto }}
run: |
if [[ "$PUBLISH_BITWARDEN" == "false" ]] && [[ "$PUBLISH_BITWARDEN_API_API" == "false" ]] && [[ "$PUBLISH_BITWARDEN_API_IDENTITY" == "false" ]]; then
echo "==================================="
Expand All @@ -87,6 +93,11 @@ jobs:
PACKAGES_LIST="$PACKAGES_LIST bitwarden-api-identity"
fi
if [[ "$PUBLISH_BITWARDEN_CRYPTO" == "true" ]]; then
PACKAGES_COMMAND="$PACKAGES_COMMAND -p bitwarden-crypto"
PACKAGES_LIST="$PACKAGES_LIST bitwarden-crypto"
fi
echo "Packages command: " $PACKAGES_COMMAND
echo "Packages list: " $PACKAGES_LIST
Expand Down
11 changes: 9 additions & 2 deletions .github/workflows/version-bump.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ on:
required: true
type: choice
options:
- napi
- bitwarden
- bitwarden-api-api
- bitwarden-api-identity
- cli
- bitwarden-crypto
- bitwarden-json
- cli
- napi
version_number:
description: "New version (example: '2024.1.0')"
required: true
Expand Down Expand Up @@ -116,6 +117,12 @@ jobs:
if: ${{ inputs.project == 'bitwarden-api-identity' }}
run: cargo-set-version set-version -p bitwarden-api-identity ${{ inputs.version_number }}

### bitwarden-crypto

- name: Bump bitwarden-crypto crate Version
if: ${{ inputs.project == 'bitwarden-crypto' }}
run: cargo-set-version set-version -p bitwarden-crypto ${{ inputs.version_number }}

### cli

- name: Bump cli Version
Expand Down
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,7 @@ languages/swift/BitwardenFFI.xcframework
languages/swift/tmp
languages/swift/.build
languages/swift/.swiftpm
languages/kotlin/sdk/src/main/java/com/bitwarden/sdk/bitwarden_uniffi.kt
languages/kotlin/sdk/src/main/java/com/bitwarden/core/bitwarden.kt
languages/kotlin/sdk/src/main/java/com/bitwarden/**/*.kt

# Schemas
crates/bitwarden-napi/src-ts/bitwarden_client/schemas.ts
Expand Down
39 changes: 29 additions & 10 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 45 additions & 0 deletions crates/bitwarden-crypto/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
[package]
name = "bitwarden-crypto"
version = "0.1.0"
authors = ["Bitwarden Inc"]
license-file = "LICENSE"
repository = "https://github.com/bitwarden/sdk"
homepage = "https://bitwarden.com"
description = """
Bitwarden Cryptographic primitives
"""
keywords = ["bitwarden"]
edition = "2021"
rust-version = "1.57"

[features]
default = []

mobile = ["uniffi"]

[dependencies]
aes = ">=0.8.2, <0.9"
argon2 = { version = ">=0.5.0, <0.6", features = [
"alloc",
], default-features = false }
base64 = ">=0.21.2, <0.22"
cbc = { version = ">=0.1.2, <0.2", features = ["alloc"] }
hkdf = ">=0.12.3, <0.13"
hmac = ">=0.12.1, <0.13"
num-bigint = ">=0.4, <0.5"
num-traits = ">=0.2.15, <0.3"
pbkdf2 = { version = ">=0.12.1, <0.13", default-features = false }
rand = ">=0.8.5, <0.9"
rsa = ">=0.9.2, <0.10"
schemars = { version = ">=0.8, <0.9", features = ["uuid1"] }
serde = { version = ">=1.0, <2.0", features = ["derive"] }
sha1 = ">=0.10.5, <0.11"
sha2 = ">=0.10.6, <0.11"
subtle = ">=2.5.0, <3.0"
thiserror = ">=1.0.40, <2.0"
uniffi = { version = "=0.25.2", optional = true }
uuid = { version = ">=1.3.3, <2.0", features = ["serde"] }

[dev-dependencies]
rand_chacha = "0.3.1"
serde_json = ">=1.0.96, <2.0"
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@
//!
//! Contains low level AES operations used by the rest of the library.
//!
//! **Warning**: Consider carefully if you have to use these functions directly, as generally we
//! expose higher level functions that are easier to use and more secure.
//!
//! In most cases you should use the [EncString] with [KeyEncryptable][super::KeyEncryptable] &
//! [KeyDecryptable][super::KeyDecryptable] instead.
//! In most cases you should use the [EncString][crate::EncString] with
//! [KeyEncryptable][crate::KeyEncryptable] & [KeyDecryptable][crate::KeyDecryptable] instead.
use aes::cipher::{
block_padding::Pkcs7, generic_array::GenericArray, typenum::U32, BlockDecryptMut,
Expand All @@ -16,14 +13,18 @@ use hmac::Mac;
use subtle::ConstantTimeEq;

use crate::{
crypto::{PbkdfSha256Hmac, PBKDF_SHA256_HMAC_OUT_SIZE},
error::{CryptoError, Result},
util::{PbkdfSha256Hmac, PBKDF_SHA256_HMAC_OUT_SIZE},
};

/// Decrypt using AES-256 in CBC mode.
///
/// Behaves similar to [decrypt_aes256_hmac], but does not validate the MAC.
pub fn decrypt_aes256(iv: &[u8; 16], data: Vec<u8>, key: GenericArray<u8, U32>) -> Result<Vec<u8>> {
pub(crate) fn decrypt_aes256(
iv: &[u8; 16],
data: Vec<u8>,
key: GenericArray<u8, U32>,
) -> Result<Vec<u8>> {
// Decrypt data
let iv = GenericArray::from_slice(iv);
let mut data = data;
Expand All @@ -41,7 +42,7 @@ pub fn decrypt_aes256(iv: &[u8; 16], data: Vec<u8>, key: GenericArray<u8, U32>)
/// Decrypt using AES-256 in CBC mode with MAC.
///
/// Behaves similar to [decrypt_aes256], but also validates the MAC.
pub fn decrypt_aes256_hmac(
pub(crate) fn decrypt_aes256_hmac(
iv: &[u8; 16],
mac: &[u8; 32],
data: Vec<u8>,
Expand All @@ -50,7 +51,7 @@ pub fn decrypt_aes256_hmac(
) -> Result<Vec<u8>> {
let res = generate_mac(&mac_key, iv, &data)?;
if res.ct_ne(mac).into() {
return Err(CryptoError::InvalidMac.into());
return Err(CryptoError::InvalidMac);
}
decrypt_aes256(iv, data, key)
}
Expand All @@ -63,7 +64,7 @@ pub fn decrypt_aes256_hmac(
///
/// A AesCbc256_B64 EncString
#[allow(unused)]
pub fn encrypt_aes256(data_dec: &[u8], key: GenericArray<u8, U32>) -> ([u8; 16], Vec<u8>) {
pub(crate) fn encrypt_aes256(data_dec: &[u8], key: GenericArray<u8, U32>) -> ([u8; 16], Vec<u8>) {
let rng = rand::thread_rng();
let (iv, data) = encrypt_aes256_internal(rng, data_dec, key);

Expand All @@ -77,7 +78,7 @@ pub fn encrypt_aes256(data_dec: &[u8], key: GenericArray<u8, U32>) -> ([u8; 16],
/// ## Returns
///
/// A AesCbc256_HmacSha256_B64 EncString
pub fn encrypt_aes256_hmac(
pub(crate) fn encrypt_aes256_hmac(
data_dec: &[u8],
mac_key: GenericArray<u8, U32>,
key: GenericArray<u8, U32>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@ use base64::{engine::general_purpose::STANDARD, Engine};
use rsa::Oaep;
use serde::Deserialize;

use super::{from_b64_vec, split_enc_string};
use crate::{
crypto::{AsymmetricCryptoKey, KeyDecryptable},
error::{CryptoError, EncStringParseError, Error, Result},
error::{CryptoError, EncStringParseError, Result},
AsymmetricCryptoKey, KeyDecryptable,
};

use super::{from_b64_vec, split_enc_string};

/// # Encrypted string primitive
///
/// [AsymmEncString] is a Bitwarden specific primitive that represents an asymmetrically encrypted string. They are
Expand Down Expand Up @@ -64,7 +63,7 @@ impl std::fmt::Debug for AsymmEncString {

/// Deserializes an [AsymmEncString] from a string.
impl FromStr for AsymmEncString {
type Err = Error;
type Err = CryptoError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let (enc_type, parts) = split_enc_string(s);
Expand Down Expand Up @@ -153,7 +152,7 @@ impl AsymmEncString {
impl KeyDecryptable<AsymmetricCryptoKey, Vec<u8>> for AsymmEncString {
fn decrypt_with_key(&self, key: &AsymmetricCryptoKey) -> Result<Vec<u8>> {
use AsymmEncString::*;
Ok(match self {
match self {
Rsa2048_OaepSha256_B64 { data } => key.key.decrypt(Oaep::new::<sha2::Sha256>(), data),
Rsa2048_OaepSha1_B64 { data } => key.key.decrypt(Oaep::new::<sha1::Sha1>(), data),
#[allow(deprecated)]
Expand All @@ -165,14 +164,14 @@ impl KeyDecryptable<AsymmetricCryptoKey, Vec<u8>> for AsymmEncString {
key.key.decrypt(Oaep::new::<sha1::Sha1>(), data)
}
}
.map_err(|_| CryptoError::KeyDecrypt)?)
.map_err(|_| CryptoError::KeyDecrypt)
}
}

impl KeyDecryptable<AsymmetricCryptoKey, String> for AsymmEncString {
fn decrypt_with_key(&self, key: &AsymmetricCryptoKey) -> Result<String> {
let dec: Vec<u8> = self.decrypt_with_key(key)?;
String::from_utf8(dec).map_err(|_| CryptoError::InvalidUtf8String.into())
String::from_utf8(dec).map_err(|_| CryptoError::InvalidUtf8String)
}
}

Expand All @@ -190,7 +189,7 @@ impl schemars::JsonSchema for AsymmEncString {

#[cfg(test)]
mod tests {
use super::AsymmEncString;
use super::{AsymmEncString, AsymmetricCryptoKey, KeyDecryptable};

const RSA_PRIVATE_KEY: &str = "-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX+2hfOQS
Expand Down Expand Up @@ -221,11 +220,8 @@ WjyxP5ZvXu7U96jaJRI8PFMoE06WeVYcdIzrID2HvqH+w0UQJFrLJ/0Mn4stFAEz
XKZBokBGnjFnTnKcs7nv/O8=
-----END PRIVATE KEY-----";

#[cfg(feature = "internal")]
#[test]
fn test_enc_string_rsa2048_oaep_sha256_b64() {
use crate::crypto::{AsymmetricCryptoKey, KeyDecryptable};

let private_key = AsymmetricCryptoKey::from_pem(RSA_PRIVATE_KEY).unwrap();
let enc_str: &str = "3.YFqzW9LL/uLjCnl0RRLtndzGJ1FV27mcwQwGjfJPOVrgCX9nJSUYCCDd0iTIyOZ/zRxG47b6L1Z3qgkEfcxjmrSBq60gijc3E2TBMAg7OCLVcjORZ+i1sOVOudmOPWro6uA8refMrg4lqbieDlbLMzjVEwxfi5WpcL876cD0vYyRwvLO3bzFrsE7x33HHHtZeOPW79RqMn5efsB5Dj9wVheC9Ix9AYDjbo+rjg9qR6guwKmS7k2MSaIQlrDR7yu8LP+ePtiSjx+gszJV5jQGfcx60dtiLQzLS/mUD+RmU7B950Bpx0H7x56lT5yXZbWK5YkoP6qd8B8D2aKbP68Ywg==";
let enc_string: AsymmEncString = enc_str.parse().unwrap();
Expand All @@ -236,11 +232,8 @@ XKZBokBGnjFnTnKcs7nv/O8=
assert_eq!(res, "EncryptMe!");
}

#[cfg(feature = "internal")]
#[test]
fn test_enc_string_rsa2048_oaep_sha1_b64() {
use crate::crypto::{AsymmetricCryptoKey, KeyDecryptable};

let private_key = AsymmetricCryptoKey::from_pem(RSA_PRIVATE_KEY).unwrap();
let enc_str: &str = "4.ZheRb3PCfAunyFdQYPfyrFqpuvmln9H9w5nDjt88i5A7ug1XE0LJdQHCIYJl0YOZ1gCOGkhFu/CRY2StiLmT3iRKrrVBbC1+qRMjNNyDvRcFi91LWsmRXhONVSPjywzrJJXglsztDqGkLO93dKXNhuKpcmtBLsvgkphk/aFvxbaOvJ/FHdK/iV0dMGNhc/9tbys8laTdwBlI5xIChpRcrfH+XpSFM88+Bu03uK67N9G6eU1UmET+pISJwJvMuIDMqH+qkT7OOzgL3t6I0H2LDj+CnsumnQmDsvQzDiNfTR0IgjpoE9YH2LvPXVP2wVUkiTwXD9cG/E7XeoiduHyHjw==";
let enc_string: AsymmEncString = enc_str.parse().unwrap();
Expand All @@ -251,11 +244,8 @@ XKZBokBGnjFnTnKcs7nv/O8=
assert_eq!(res, "EncryptMe!");
}

#[cfg(feature = "internal")]
#[test]
fn test_enc_string_rsa2048_oaep_sha1_hmac_sha256_b64() {
use crate::crypto::{AsymmetricCryptoKey, KeyDecryptable};

let private_key = AsymmetricCryptoKey::from_pem(RSA_PRIVATE_KEY).unwrap();
let enc_str: &str = "6.ThnNc67nNr7GELyuhGGfsXNP2zJnNqhrIsjntEQ27r2qmn8vwdHbTbfO0cwt6YgSibDN0PjiCZ1O3Wb/IFq+vwvyRwFqF9145wBF8CQCbkhV+M0XvO99kh0daovtt120Nve/5ETI5PbPag9VdalKRQWZypJaqQHm5TAQVf4F5wtLlCLMBkzqTk+wkFe7BPMTGn07T+O3eJbTxXvyMZewQ7icJF0MZVA7VyWX9qElmZ89FCKowbf1BMr5pbcQ+0KdXcSVW3to43VkTp7k7COwsuH3M/i1AuVP5YN8ixjyRpvaeGqX/ap2nCHK2Wj5VxgCGT7XEls6ZknnAp9nB9qVjQ==|s3ntw5H/KKD/qsS0lUghTHl5Sm9j6m7YEdNHf0OeAFQ=";
let enc_string: AsymmEncString = enc_str.parse().unwrap();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/// Encrypted string types
mod asymmetric;
mod symmetric;

Expand All @@ -9,7 +10,6 @@ pub use symmetric::EncString;

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

#[cfg(feature = "mobile")]
fn check_length(buf: &[u8], expected: usize) -> Result<()> {
if buf.len() < expected {
return Err(EncStringParseError::InvalidLength {
Expand Down
Loading

0 comments on commit d2cb4f9

Please sign in to comment.