Skip to content

Commit

Permalink
feat: TS-based integration tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ivan770 committed Feb 21, 2024
1 parent da526db commit 2e53f3d
Show file tree
Hide file tree
Showing 14 changed files with 3,204 additions and 94 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@
/.direnv
/result

# TS/JS
node_modules

# Rust
/target
42 changes: 0 additions & 42 deletions Cargo.lock

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

5 changes: 5 additions & 0 deletions crates/teddybear-crypto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ impl Ed25519<Public> {
}

impl<T> Ed25519<T> {
#[inline]
pub fn document(&self) -> &Document {
&self.document
}

#[inline]
pub fn document_did(&self) -> &str {
&self.document.id
Expand Down
3 changes: 0 additions & 3 deletions crates/teddybear-js/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,3 @@ uuid = { version = "1.7.0", features = ["v4", "js"] }
js-sys = "0.3.68"
wasm-bindgen = "0.2.91"
wasm-bindgen-futures = "0.4.41"

[dev-dependencies]
wasm-bindgen-test = "0.3.41"
37 changes: 10 additions & 27 deletions crates/teddybear-js/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,11 @@ impl PrivateEd25519 {
JWK(self.0.to_x25519_public_jwk())
}

/// Get the key document value.
pub fn document(&self) -> Result<Object, JsError> {
Ok(self.0.document().serialize(&OBJECT_SERIALIZER)?.into())
}

/// Get the document DID value.
///
/// This value is usually used to idenfity an entity as a whole.
Expand Down Expand Up @@ -282,6 +287,11 @@ impl PublicEd25519 {
JWK(self.0.to_x25519_public_jwk())
}

/// Get the key document value.
pub fn document(&self) -> Result<Object, JsError> {
Ok(self.0.document().serialize(&OBJECT_SERIALIZER)?.into())
}

/// Get the document DID value.
///
/// This value is usually used to idenfity an entity as a whole.
Expand Down Expand Up @@ -472,30 +482,3 @@ pub fn encrypt(payload: Uint8Array, recipients: Vec<JWK>) -> Result<Object, JsEr

Ok(jwe.serialize(&OBJECT_SERIALIZER)?.into())
}

#[cfg(test)]
mod tests {
use js_sys::Uint8Array;
use wasm_bindgen_test::wasm_bindgen_test;

use crate::{encrypt, PrivateEd25519};

#[wasm_bindgen_test]
async fn encrypt_and_decrypt() {
let key = PrivateEd25519::generate()
.await
.unwrap_or_else(|_| panic!());

let encrypted = encrypt(
Uint8Array::from(b"Hello, world".as_slice()),
vec![key.to_x25519_public_jwk()],
)
.unwrap_or_else(|_| panic!());

let decrypted = key.decrypt(encrypted).unwrap_or_else(|_| panic!());

let mut buf = [0; 12];
decrypted.copy_to(&mut buf);
assert_eq!(buf.as_slice(), b"Hello, world");
}
}
49 changes: 35 additions & 14 deletions crates/teddybear-jwe/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use askar_crypto::{
encrypt::{KeyAeadInPlace, KeyAeadMeta},
jwk::{FromJwk, ToJwk},
kdf::{ecdh_es::EcdhEs, FromKeyDerivation},
repr::{KeyGen, KeyPublicBytes, KeySecretBytes, ToPublicBytes, ToSecretBytes},
repr::{KeyGen, KeySecretBytes, ToPublicBytes, ToSecretBytes},
Error, ErrorKind,
};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
Expand Down Expand Up @@ -55,17 +55,29 @@ pub fn encrypt(payload: &[u8], recipients: &[&JWK]) -> Result<GeneralJWE<'static
let cek = AesKey::<A256Gcm>::random()?;

let ephemeral_key_pair = X25519KeyPair::random()?;
let producer_info = ephemeral_key_pair.to_public_bytes()?;

let recipients = recipients
.iter()
.map(|recipient| {
let consumer_info =
recipient
.key_id
.as_deref()
.map(str::as_bytes)
.ok_or_else(|| {
Error::from_msg(
ErrorKind::InvalidKeyData,
"Key identifier (consumer info) is not present.",
)
})?;

// FIXME: Remove unnecessary JWK conversion.
let static_peer = X25519KeyPair::from_jwk(
&serde_json::to_string(recipient).expect("JWK serialization should always succeed"),
)?;

let (kek, producer_info, consumer_info) =
create_kek(&ephemeral_key_pair, &static_peer, false)?;
let kek = create_kek(&ephemeral_key_pair, &static_peer, consumer_info, false)?;

let mut cek_buffer = cek.to_secret_bytes()?;

Expand All @@ -82,8 +94,8 @@ pub fn encrypt(payload: &[u8], recipients: &[&JWK]) -> Result<GeneralJWE<'static
&ephemeral_key_pair.to_jwk_public(Some(KeyAlg::X25519))?,
)
.expect("JWK serialization should always succeed"),
producer_info: Base64urlUInt(producer_info),
consumer_info: Base64urlUInt(consumer_info),
producer_info: Base64urlUInt(producer_info.to_vec()),
consumer_info: Base64urlUInt(consumer_info.to_vec()),
},
encrypted_key: Base64urlUInt(cek_buffer.to_vec()),
})
Expand Down Expand Up @@ -115,17 +127,27 @@ pub fn decrypt(jwe: &GeneralJWE<'_>, recipient: &JWK) -> Result<Vec<u8>, Error>
));
}

let consumer_info = recipient
.key_id
.as_deref()
.map(str::as_bytes)
.ok_or_else(|| {
Error::from_msg(
ErrorKind::InvalidKeyData,
"Key identifier (consumer info) is not present.",
)
})?;

// FIXME: Remove unnecessary JWK conversion.
let recipient = X25519KeyPair::from_jwk(
let askar_recipient = X25519KeyPair::from_jwk(
&serde_json::to_string(recipient).expect("JWK serialization should always succeed"),
)?;

let matching_recipient = jwe
.recipients
.iter()
.find(|key| {
key.header.algorithm == "ECDH-ES+A256KW"
&& recipient.with_public_bytes(|val| val == key.header.consumer_info.0)
key.header.algorithm == "ECDH-ES+A256KW" && consumer_info == key.header.consumer_info.0
})
.ok_or_else(|| Error::from_msg(ErrorKind::Encryption, "Recipient not found."))?;

Expand All @@ -134,7 +156,7 @@ pub fn decrypt(jwe: &GeneralJWE<'_>, recipient: &JWK) -> Result<Vec<u8>, Error>
.expect("JWK serialization should always succeed"),
)?;

let (kek, _, _) = create_kek(&ephemeral_key_pair, &recipient, true)?;
let kek = create_kek(&ephemeral_key_pair, &askar_recipient, consumer_info, true)?;

let mut cek_buffer = matching_recipient.encrypted_key.0.clone();
kek.decrypt_in_place(&mut cek_buffer, &[], &[])?;
Expand Down Expand Up @@ -186,27 +208,26 @@ fn decrypt_with_cek(
Ok(())
}

#[allow(clippy::type_complexity)]
fn create_kek(
ephemeral_key_pair: &X25519KeyPair,
recipient: &X25519KeyPair,
consumer_info: &[u8],
receive: bool,
) -> Result<(AesKey<A256Kw>, Vec<u8>, Vec<u8>), Error> {
) -> Result<AesKey<A256Kw>, Error> {
let producer_info = ephemeral_key_pair.to_public_bytes()?;
let consumer_info = recipient.to_public_bytes()?;

let key_info = EcdhEs::new(
ephemeral_key_pair,
recipient,
b"ECDH-ES+A256KW",
&producer_info,
&consumer_info,
consumer_info,
receive,
);

let kek = AesKey::<A256Kw>::from_key_derivation(key_info)?;

Ok((kek, producer_info.to_vec(), consumer_info.to_vec()))
Ok(kek)
}

#[cfg(test)]
Expand Down
23 changes: 19 additions & 4 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,6 @@
pkgs.wasm-bindgen-cli
pkgs.binaryen
pkgs.llvmPackages.lld

# Testing
pkgs.nodejs-slim
];
};

Expand All @@ -122,7 +119,8 @@
};
in {
devShells.default = pkgs.mkShell {
buildInputs = [rustToolchain];
buildInputs = [rustToolchain pkgs.nodejs pkgs.yarn];
inputsFrom = [esm];
};

packages = {
Expand All @@ -142,6 +140,23 @@
checks = {
inherit cjs esm;

node = pkgs.callPackage ./nix/node-testing.nix {
inherit cjs;

src = nix-filter.lib.filter {
root = ./tests;

include = [
"src"
"package.json"
"tsconfig.json"
"yarn.lock"
];
};

yarnLockHash = "sha256-OZfOCAp8tmswJkjesnpo1RJ6XJW49iIolpL1snctBtk=";
};

my-crate-clippy = craneLib.cargoClippy (commonArgs
// {
inherit cargoArtifacts;
Expand Down
56 changes: 56 additions & 0 deletions nix/node-testing.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
cjs,
fetchYarnDeps,
src,
stdenvNoCC,
nodejs,
prefetch-yarn-deps,
yarn,
yarnLockHash
}:
stdenvNoCC.mkDerivation {
inherit src;
inherit (cjs) version;

pname = "teddybear-tests";

offlineCache = fetchYarnDeps {
yarnLock = "${src}/yarn.lock";
hash = yarnLockHash;
};

nativeBuildInputs = [
nodejs
yarn
prefetch-yarn-deps
];

postPatch = ''
export HOME=$(mktemp -d)
yarn config --offline set yarn-offline-mirror $offlineCache
fixup-yarn-lock yarn.lock
# For easier test development, "teddybear-tests" package contains pre-installed
# Teddybear from NPM. However, the NPM version obviously does not correspond to the
# Teddybear version that is meant to be tested, so we dynamically replace it here.
#
# This will only work if Teddybear continues to not require any third-party dependencies.
yarn remove @vaultie/teddybear-node
yarn add file:${cjs}
yarn install \
--offline \
--ignore-scripts \
--no-progress \
--non-interactive
patchShebangs node_modules/
'';

buildPhase = ''
yarn test
touch $out
'';
}
5 changes: 1 addition & 4 deletions nix/package.nix
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@ in
--release
'';

checkPhaseCargoCommand = ''
wasm-pack test --node \
crates/teddybear-js
'';
doCheck = false;

doInstallCargoArtifacts = false;

Expand Down
Loading

0 comments on commit 2e53f3d

Please sign in to comment.