Skip to content

Commit

Permalink
Merge branch 'main' of github.com:bitwarden/sdk into ps/crypto-crate
Browse files Browse the repository at this point in the history
# Conflicts:
#	crates/bitwarden-crypto/src/asymmetric_crypto_key.rs
#	crates/bitwarden-crypto/src/enc_string/asymmetric.rs
#	crates/bitwarden-crypto/src/encryptable.rs
#	crates/bitwarden-crypto/src/keys/key_encryptable.rs
#	crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs
#	crates/bitwarden/src/client/encryption_settings.rs
#	crates/bitwarden/src/crypto/mod.rs
#	crates/bitwarden/src/vault/cipher/attachment.rs
#	crates/bitwarden/src/vault/cipher/card.rs
#	crates/bitwarden/src/vault/cipher/cipher.rs
#	crates/bitwarden/src/vault/cipher/field.rs
#	crates/bitwarden/src/vault/cipher/identity.rs
#	crates/bitwarden/src/vault/cipher/local_data.rs
#	crates/bitwarden/src/vault/cipher/login.rs
#	crates/bitwarden/src/vault/cipher/secure_note.rs
#	crates/bitwarden/src/vault/collection.rs
#	crates/bitwarden/src/vault/folder.rs
#	crates/bitwarden/src/vault/password_history.rs
#	crates/bitwarden/src/vault/send.rs
  • Loading branch information
Hinton committed Jan 12, 2024
2 parents 6116292 + 4c91270 commit de160d9
Show file tree
Hide file tree
Showing 25 changed files with 570 additions and 113 deletions.
163 changes: 163 additions & 0 deletions .github/workflows/build-cli-docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
---
name: Build bws Docker image

on:
push:
paths:
- "crates/bws/**"
workflow_dispatch:
inputs:
sdk_branch:
description: "Server branch name to deploy (examples: 'master', 'rc', 'feature/sm')"
type: string
default: master
pull_request:
paths:
- ".github/workflows/build-cli-docker.yml"
- "crates/bws/**"

env:
_AZ_REGISTRY: bitwardenprod.azurecr.io

jobs:
build-docker:
name: Build Docker image
runs-on: ubuntu-22.04
steps:
- name: Checkout Repository
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0

- name: Check Branch to Publish
env:
PUBLISH_BRANCHES: "master,rc,hotfix-rc"
id: publish-branch-check
run: |
REF=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}
IFS="," read -a publish_branches <<< $PUBLISH_BRANCHES
if [[ "${publish_branches[*]}" =~ "${REF}" ]]; then
echo "is_publish_branch=true" >> $GITHUB_ENV
else
echo "is_publish_branch=false" >> $GITHUB_ENV
fi
########## Set up Docker ##########
- name: Set up QEMU emulators
uses: docker/setup-qemu-action@e81a89b1732b9c48d79cd809d8d81d79c4647a18 # v2.1.0

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4b4e9c3e2d4531116a6f8ba8e71fc6e2cb6e6c8c # v2.5.0

########## Login to Docker registries ##########
- name: Login to Azure - Prod Subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}

- name: Login to Azure ACR
run: az acr login -n ${_AZ_REGISTRY%.azurecr.io}

- name: Login to Azure - CI Subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}

- name: Retrieve github PAT secrets
id: retrieve-secret-pat
uses: bitwarden/gh-actions/get-keyvault-secrets@c86ced0dc8c9daeecf057a6333e6f318db9c5a2b
with:
keyvault: "bitwarden-ci"
secrets: "github-pat-bitwarden-devops-bot-repo-scope"

- name: Setup Docker Trust
if: ${{ env.is_publish_branch == 'true' }}
uses: bitwarden/gh-actions/setup-docker-trust@082f5e05ed97c3601c6f3179250b1a761c4d647f
with:
azure-creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
azure-keyvault-name: "bitwarden-ci"

########## Generate image tag and build Docker image ##########
- name: Generate Docker image tag
id: tag
run: |
REF=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}
IMAGE_TAG=$(echo "${REF}" | sed "s#/#-#g") # slash safe branch name
if [[ "${IMAGE_TAG}" == "master" ]]; then
IMAGE_TAG=dev
elif [[ ("${IMAGE_TAG}" == "rc") || ("${IMAGE_TAG}" == "hotfix-rc") ]]; then
IMAGE_TAG=rc
fi
echo "image_tag=${IMAGE_TAG}" >> $GITHUB_OUTPUT
- name: Generate tag list
id: tag-list
env:
IMAGE_TAG: ${{ steps.tag.outputs.image_tag }}
IS_PUBLISH_BRANCH: ${{ env.is_publish_branch }}
run: |
if [[ ("${IMAGE_TAG}" == "dev" || "${IMAGE_TAG}" == "rc") && "${IS_PUBLISH_BRANCH}" == "true" ]]; then
echo "tags=$_AZ_REGISTRY/bws:${IMAGE_TAG},bitwarden/bws:${IMAGE_TAG}" >> $GITHUB_OUTPUT
else
echo "tags=$_AZ_REGISTRY/bws:${IMAGE_TAG}" >> $GITHUB_OUTPUT
fi
- name: Build and push Docker image
uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 # v3.2.0
with:
context: .
file: crates/bws/Dockerfile
platforms: |
linux/amd64,
linux/arm64/v8
push: true
tags: ${{ steps.tag-list.outputs.tags }}
secrets: |
"GH_PAT=${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}"
- name: Log out of Docker and disable Docker Notary
if: ${{ env.is_publish_branch == 'true' }}
run: |
docker logout
echo "DOCKER_CONTENT_TRUST=0" >> $GITHUB_ENV
check-failures:
name: Check for failures
if: always()
runs-on: ubuntu-22.04
needs: build-docker
steps:
- name: Check if any job failed
if: |
github.ref == 'refs/heads/master'
|| github.ref == 'refs/heads/rc'
|| github.ref == 'refs/heads/hotfix-rc'
env:
BUILD_DOCKER_STATUS: ${{ needs.build-docker.result }}
run: |
if [ "$BUILD_DOCKER_STATUS" = "failure" ]; then
exit 1
fi
- name: Login to Azure - CI subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
if: failure()
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}

- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@c86ced0dc8c9daeecf057a6333e6f318db9c5a2b
if: failure()
with:
keyvault: "bitwarden-ci"
secrets: "devops-alerts-slack-webhook-url"

- name: Notify Slack on failure
uses: act10ns/slack@ed1309ab9862e57e9e583e51c7889486b9a00b0f # v2.0.0
if: failure()
env:
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
with:
status: ${{ job.status }}
104 changes: 72 additions & 32 deletions crates/bitwarden-crypto/src/enc_string/asymmetric.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
use std::{fmt::Display, str::FromStr};

use base64::{engine::general_purpose::STANDARD, Engine};
use rsa::{Oaep, RsaPrivateKey};
use rsa::Oaep;
use serde::Deserialize;

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

/// # Encrypted string primitive
///
/// [AsymmEncString] is a Bitwarden specific primitive that represents an asymmetrically encrypted string. They are
/// are used together with the KeyDecryptable and KeyEncryptable traits to encrypt and decrypt
/// data using AsymmetricCryptoKeys.
/// data using [AsymmetricCryptoKey]s.
///
/// The flexibility of the [AsymmEncString] type allows for different encryption algorithms to be used
/// which is represented by the different variants of the enum.
Expand Down Expand Up @@ -95,26 +98,6 @@ impl FromStr for AsymmEncString {
}
}

#[allow(unused)]
impl AsymmEncString {
/// TODO: Convert this to a trait method
pub fn decrypt(&self, key: &RsaPrivateKey) -> Result<Vec<u8>> {
match self {
Self::Rsa2048_OaepSha256_B64 { data } => key.decrypt(Oaep::new::<sha2::Sha256>(), data),
Self::Rsa2048_OaepSha1_B64 { data } => key.decrypt(Oaep::new::<sha1::Sha1>(), data),
#[allow(deprecated)]
Self::Rsa2048_OaepSha256_HmacSha256_B64 { data, mac: _ } => {
key.decrypt(Oaep::new::<sha2::Sha256>(), data)
}
#[allow(deprecated)]
Self::Rsa2048_OaepSha1_HmacSha256_B64 { data, mac: _ } => {
key.decrypt(Oaep::new::<sha1::Sha1>(), data)
}
}
.map_err(|_| CryptoError::KeyDecrypt)
}
}

impl Display for AsymmEncString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let parts: Vec<&[u8]> = match self {
Expand Down Expand Up @@ -166,6 +149,32 @@ impl AsymmEncString {
}
}

impl KeyDecryptable<AsymmetricCryptoKey, Vec<u8>> for AsymmEncString {
fn decrypt_with_key(&self, key: &AsymmetricCryptoKey) -> Result<Vec<u8>> {
use AsymmEncString::*;
Ok(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)]
Rsa2048_OaepSha256_HmacSha256_B64 { data, .. } => {
key.key.decrypt(Oaep::new::<sha2::Sha256>(), data)
}
#[allow(deprecated)]
Rsa2048_OaepSha1_HmacSha256_B64 { data, .. } => {
key.key.decrypt(Oaep::new::<sha1::Sha1>(), data)
}
}
.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())
}
}

/// Usually we wouldn't want to expose AsymmEncStrings in the API or the schemas.
/// But during the transition phase we will expose endpoints using the AsymmEncString type.
impl schemars::JsonSchema for AsymmEncString {
Expand All @@ -182,11 +191,7 @@ impl schemars::JsonSchema for AsymmEncString {
mod tests {
use super::AsymmEncString;

#[test]
fn test_enc_string_rsa2048_oaep_sha1_hmac_sha256_b64() {
use rsa::{pkcs8::DecodePrivateKey, RsaPrivateKey};

let rsa_private_key: &str = "-----BEGIN PRIVATE KEY-----
const RSA_PRIVATE_KEY: &str = "-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX+2hfOQS
8HzYUS2oc/jGVTZpv+/Ryuoh9d8ihYX9dd0cYh2tl6KWdFc88lPUH11Oxqy20Rk2
e5r/RF6T9yM0Me3NPnaKt+hlhLtfoc0h86LnhD56A9FDUfuI0dVnPcrwNv0YJIo9
Expand Down Expand Up @@ -214,15 +219,50 @@ Is3v1kkf5I0X8DnOhwb+HPxNaiEdmO7ckm8+tPVgppLcG0+tMdLjigFQiDUQk2y3
WjyxP5ZvXu7U96jaJRI8PFMoE06WeVYcdIzrID2HvqH+w0UQJFrLJ/0Mn4stFAEz
XKZBokBGnjFnTnKcs7nv/O8=
-----END PRIVATE KEY-----";
let private_key = RsaPrivateKey::from_pkcs8_pem(rsa_private_key).unwrap();

#[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();

assert_eq!(enc_string.enc_type(), 3);

let res: String = enc_string.decrypt_with_key(&private_key).unwrap();
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();

assert_eq!(enc_string.enc_type(), 4);

let res: String = enc_string.decrypt_with_key(&private_key).unwrap();
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();

assert_eq!(enc_string.enc_type(), 6);

let res = enc_string.decrypt(&private_key).unwrap();

assert_eq!(std::str::from_utf8(&res).unwrap(), "EncryptMe!");
let res: String = enc_string.decrypt_with_key(&private_key).unwrap();
assert_eq!(res, "EncryptMe!");
}

#[test]
Expand Down
8 changes: 4 additions & 4 deletions crates/bitwarden-crypto/src/enc_string/symmetric.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,13 +222,13 @@ impl EncString {
}

impl LocateKey for EncString {}
impl KeyEncryptable<EncString> for &[u8] {
impl KeyEncryptable<SymmetricCryptoKey, EncString> for &[u8] {
fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
EncString::encrypt_aes256_hmac(self, key.mac_key.ok_or(CryptoError::InvalidMac)?, key.key)
}
}

impl KeyDecryptable<Vec<u8>> for EncString {
impl KeyDecryptable<SymmetricCryptoKey, Vec<u8>> for EncString {
fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<Vec<u8>> {
match self {
EncString::AesCbc256_HmacSha256_B64 { iv, mac, data } => {
Expand All @@ -241,13 +241,13 @@ impl KeyDecryptable<Vec<u8>> for EncString {
}
}

impl KeyEncryptable<EncString> for String {
impl KeyEncryptable<SymmetricCryptoKey, EncString> for String {
fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
self.as_bytes().encrypt_with_key(key)
}
}

impl KeyDecryptable<String> for EncString {
impl KeyDecryptable<SymmetricCryptoKey, String> for EncString {
fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<String> {
let dec: Vec<u8> = self.decrypt_with_key(key)?;
String::from_utf8(dec).map_err(|_| CryptoError::InvalidUtf8String)
Expand Down
4 changes: 2 additions & 2 deletions crates/bitwarden-crypto/src/encryptable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pub trait Decryptable<Output> {
fn decrypt(&self, enc: &dyn KeyContainer, org_id: &Option<Uuid>) -> Result<Output>;
}

impl<T: KeyEncryptable<Output> + LocateKey, Output> Encryptable<Output> for T {
impl<T: KeyEncryptable<SymmetricCryptoKey, Output> + LocateKey, Output> Encryptable<Output> for T {
fn encrypt(self, enc: &dyn KeyContainer, org_id: &Option<Uuid>) -> Result<Output> {
let key = self
.locate_key(enc, org_id)
Expand All @@ -37,7 +37,7 @@ impl<T: KeyEncryptable<Output> + LocateKey, Output> Encryptable<Output> for T {
}
}

impl<T: KeyDecryptable<Output> + LocateKey, Output> Decryptable<Output> for T {
impl<T: KeyDecryptable<SymmetricCryptoKey, Output> + LocateKey, Output> Decryptable<Output> for T {
fn decrypt(&self, enc: &dyn KeyContainer, org_id: &Option<Uuid>) -> Result<Output> {
let key = self
.locate_key(enc, org_id)
Expand Down
Loading

0 comments on commit de160d9

Please sign in to comment.