diff --git a/example/test.ts b/example/test.ts index 0382b4c..9d8f606 100644 --- a/example/test.ts +++ b/example/test.ts @@ -1,5 +1,5 @@ -import {from_hex, to_hex, decrypt_and_verify, gen_signing_key, encrypt_and_sign, public_key_from_secret, constant_time_eq } from "x25519-chacha20poly1305"; +import {from_hex, to_hex, decrypt_and_verify, gen_signing_key, encrypt_and_sign, public_key_from_secret, constant_time_eq, encryptOnly, decryptOnly } from "x25519-chacha20poly1305"; let empty = new Uint8Array(32); let hempty = to_hex(empty); @@ -28,3 +28,16 @@ console.log(decrypted_plaintext); let is_equal = constant_time_eq(decrypted_plaintext, plaintext); console.log("alice encrypted plaintext is equal to bob decrypted ciphertext: ", is_equal); +// Test encryptOnly: + +// Alice encrypts and signs the message to bob. +let encrypted_msg = encryptOnly(alice_sk, plaintext, bob_pk); +console.log(encrypted_msg); + +// Bob decrypts the message. +decrypted_plaintext = decryptOnly(bob_sk, encrypted_msg); + +console.log(decrypted_plaintext); +// Check the original plaintext equals the decrypted plaintext. +is_equal = constant_time_eq(decrypted_plaintext, plaintext); +console.log("alice encrypted plaintext is equal to bob decrypted ciphertext: ", is_equal); diff --git a/pkg/x25519_chacha20poly1305.d.ts b/pkg/x25519_chacha20poly1305.d.ts index b0c7af7..8e35a2b 100644 --- a/pkg/x25519_chacha20poly1305.d.ts +++ b/pkg/x25519_chacha20poly1305.d.ts @@ -49,3 +49,19 @@ export function decrypt_and_verify(sk: Uint8Array, msg: string): Uint8Array; * @returns {boolean} */ export function constant_time_eq(a: Uint8Array, b: Uint8Array): boolean; +/** +* Encrypts, signs, and serializes a SignedMessage to JSON. +* @param {Uint8Array} sk +* @param {Uint8Array} msg +* @param {Uint8Array} pk +* @returns {string} +*/ +export function encryptOnly(sk: Uint8Array, msg: Uint8Array, pk: Uint8Array): string; +/** +* Deserializes and decrypts a json encoded `EncryptedMessage`. +* Returns the plaintext. +* @param {Uint8Array} sk +* @param {string} msg +* @returns {Uint8Array} +*/ +export function decryptOnly(sk: Uint8Array, msg: string): Uint8Array; diff --git a/pkg/x25519_chacha20poly1305.js b/pkg/x25519_chacha20poly1305.js index c56c6f2..a0a0b35 100644 --- a/pkg/x25519_chacha20poly1305.js +++ b/pkg/x25519_chacha20poly1305.js @@ -309,6 +309,74 @@ module.exports.constant_time_eq = function(a, b) { return ret !== 0; }; +/** +* Encrypts, signs, and serializes a SignedMessage to JSON. +* @param {Uint8Array} sk +* @param {Uint8Array} msg +* @param {Uint8Array} pk +* @returns {string} +*/ +module.exports.encryptOnly = function(sk, msg, pk) { + let deferred5_0; + let deferred5_1; + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passArray8ToWasm0(sk, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passArray8ToWasm0(msg, wasm.__wbindgen_malloc); + const len1 = WASM_VECTOR_LEN; + const ptr2 = passArray8ToWasm0(pk, wasm.__wbindgen_malloc); + const len2 = WASM_VECTOR_LEN; + wasm.encryptOnly(retptr, ptr0, len0, ptr1, len1, ptr2, len2); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var r2 = getInt32Memory0()[retptr / 4 + 2]; + var r3 = getInt32Memory0()[retptr / 4 + 3]; + var ptr4 = r0; + var len4 = r1; + if (r3) { + ptr4 = 0; len4 = 0; + throw takeObject(r2); + } + deferred5_0 = ptr4; + deferred5_1 = len4; + return getStringFromWasm0(ptr4, len4); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_free(deferred5_0, deferred5_1, 1); + } +}; + +/** +* Deserializes and decrypts a json encoded `EncryptedMessage`. +* Returns the plaintext. +* @param {Uint8Array} sk +* @param {string} msg +* @returns {Uint8Array} +*/ +module.exports.decryptOnly = function(sk, msg) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passArray8ToWasm0(sk, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passStringToWasm0(msg, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + wasm.decryptOnly(retptr, ptr0, len0, ptr1, len1); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var r2 = getInt32Memory0()[retptr / 4 + 2]; + var r3 = getInt32Memory0()[retptr / 4 + 3]; + if (r3) { + throw takeObject(r2); + } + var v3 = getArrayU8FromWasm0(r0, r1).slice(); + wasm.__wbindgen_free(r0, r1 * 1); + return v3; + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } +}; + function handleError(f, args) { try { return f.apply(this, args); diff --git a/pkg/x25519_chacha20poly1305_bg.wasm b/pkg/x25519_chacha20poly1305_bg.wasm index 7346d12..9b45008 100644 Binary files a/pkg/x25519_chacha20poly1305_bg.wasm and b/pkg/x25519_chacha20poly1305_bg.wasm differ diff --git a/pkg/x25519_chacha20poly1305_bg.wasm.d.ts b/pkg/x25519_chacha20poly1305_bg.wasm.d.ts index 34758ab..8f1b749 100644 --- a/pkg/x25519_chacha20poly1305_bg.wasm.d.ts +++ b/pkg/x25519_chacha20poly1305_bg.wasm.d.ts @@ -8,6 +8,8 @@ export function gen_signing_key(a: number): void; export function encrypt_and_sign(a: number, b: number, c: number, d: number, e: number, f: number, g: number): void; export function decrypt_and_verify(a: number, b: number, c: number, d: number, e: number): void; export function constant_time_eq(a: number, b: number, c: number, d: number): number; +export function encryptOnly(a: number, b: number, c: number, d: number, e: number, f: number, g: number): void; +export function decryptOnly(a: number, b: number, c: number, d: number, e: number): void; export function rustsecp256k1_v0_4_1_context_create(a: number): number; export function rustsecp256k1_v0_4_1_context_destroy(a: number): void; export function rustsecp256k1_v0_4_1_default_illegal_callback_fn(a: number, b: number): void; diff --git a/src/encrypt_only.rs b/src/encrypt_only.rs new file mode 100644 index 0000000..53d5e3e --- /dev/null +++ b/src/encrypt_only.rs @@ -0,0 +1,177 @@ +use super::{derive_static_secret, ValidationErr}; +use chacha20poly1305::{ + aead::{Aead, AeadCore, KeyInit}, + ChaCha20Poly1305, +}; +use js_sys::Error; +use rand_core::OsRng; +use schnorrkel::SecretKey; +use serde::{Deserialize, Serialize}; +use serde_json::to_string; +use sp_core::{sr25519, Bytes}; +use wasm_bindgen::prelude::*; +use x25519_dalek::PublicKey; +use zeroize::Zeroize; + +#[wasm_bindgen(js_name = encryptOnly)] +/// Encrypts, signs, and serializes a SignedMessage to JSON. +pub fn encrypt_only(sk: Vec, msg: Vec, pk: Vec) -> Result { + let mut _raw_pk: [u8; 32] = [0; 32]; + _raw_pk.copy_from_slice(&pk[0..32]); + let _pk = PublicKey::from(_raw_pk); + let _msg = Bytes(msg); + + let mut sk_buff: [u8; 64] = [0; 64]; + sk_buff.copy_from_slice(&sk[0..64]); + if sk.len() != 64 { + return Err(Error::new("Secret key must be 64 bytes")); + } + + let sec_key = + SecretKey::from_ed25519_bytes(sk.as_slice()).map_err(|err| Error::new(&err.to_string()))?; + let pair = sr25519::Pair::from(sec_key); + let encrypted_message = + EncryptedMessage::new(&pair, &_msg, &_pk).map_err(|err| Error::new(&err.to_string()))?; + Ok(encrypted_message + .to_json() + .map_err(|err| Error::new(&err.to_string()))?) +} + +#[wasm_bindgen (js_name = decryptOnly)] +/// Deserializes and decrypts a json encoded `EncryptedMessage`. +/// Returns the plaintext. +pub fn decrypt_only(sk: Vec, msg: String) -> Result, Error> { + let sm: EncryptedMessage = + serde_json::from_str(msg.as_str()).map_err(|err| Error::new(&err.to_string()))?; + + let sec_key = + SecretKey::from_ed25519_bytes(sk.as_slice()).map_err(|err| Error::new(&err.to_string()))?; + let pair = sr25519::Pair::from(sec_key); + Ok(sm + .decrypt(&pair) + .map_err(|err| Error::new(&err.to_string()))?) +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct EncryptedMessage { + /// The encrypted message. + pub msg: Bytes, + /// The intended recipients public key to be included in the signature. + recip: [u8; 32], + /// The signers public parameter used in diffie-hellman. + a: [u8; 32], + /// The message nonce used in ChaCha20Poly1305. + nonce: [u8; 12], +} + +impl EncryptedMessage { + /// Encrypts and signs msg. + /// sk is the sr25519 key used for signing and deriving a symmetric shared key + /// via Diffie-Hellman for encryption. + /// msg is the plaintext message to encrypt and sign + /// recip is the public Diffie-Hellman parameter of the recipient. + pub fn new( + sk: &sr25519::Pair, + msg: &Bytes, + recip: &PublicKey, + ) -> Result { + let mut s = derive_static_secret(sk); + let a = x25519_dalek::PublicKey::from(&s); + let shared_secret = s.diffie_hellman(recip); + s.zeroize(); + + let msg_nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng); // 96-bits; unique per message + let cipher = ChaCha20Poly1305::new_from_slice(shared_secret.as_bytes()) + .map_err(|e| ValidationErr::Conversion(e.to_string()))?; + let ciphertext = cipher + .encrypt(&msg_nonce, msg.0.as_slice()) + .map_err(|e| ValidationErr::Encryption(e.to_string()))?; + let mut static_nonce: [u8; 12] = [0; 12]; + static_nonce.copy_from_slice(&msg_nonce); + + Ok(EncryptedMessage { + msg: sp_core::Bytes(ciphertext), + recip: recip.to_bytes(), + a: *a.as_bytes(), + nonce: static_nonce, + }) + } + + /// Decrypts the message and returns the plaintext. + pub fn decrypt(&self, sk: &sr25519::Pair) -> Result, ValidationErr> { + let mut static_secret = derive_static_secret(sk); + let shared_secret = static_secret.diffie_hellman(&PublicKey::from(self.a)); + static_secret.zeroize(); + let cipher = ChaCha20Poly1305::new_from_slice(shared_secret.as_bytes()) + .map_err(|e| ValidationErr::Conversion(e.to_string()))? + .decrypt( + &generic_array::GenericArray::from(self.nonce), + self.msg.0.as_slice(), + ) + .map_err(|e| ValidationErr::Decryption(e.to_string()))?; + Ok(cipher) + } + + /// Returns the public DH parameter of the message sender. + pub fn sender(&self) -> x25519_dalek::PublicKey { + x25519_dalek::PublicKey::from(self.a) + } + + /// Returns the public DH key of the message recipient. + pub fn recipient(&self) -> PublicKey { + PublicKey::from(self.recip) + } + + /// Returns a serialized json string of self. + pub fn to_json(&self) -> Result { + Ok(to_string(self)?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{mnemonic_to_pair, new_mnemonic}; + + #[test] + fn test_encrypt() { + let plaintext = Bytes(vec![69, 42, 0]); + + let alice = mnemonic_to_pair(&new_mnemonic()).unwrap(); + + let bob = mnemonic_to_pair(&new_mnemonic()).unwrap(); + let bob_secret = derive_static_secret(&bob); + let bob_public_key = PublicKey::from(&bob_secret); + + // Test encryption + let encrypted_message = EncryptedMessage::new(&alice, &plaintext, &bob_public_key).unwrap(); + + // Test decryption + let decrypted_message = encrypted_message.decrypt(&bob).unwrap(); + + // Check the decrypted message equals the plaintext. + assert_eq!(Bytes(decrypted_message), plaintext); + + // Check the encrypted message != the plaintext. + assert_ne!(encrypted_message.msg, plaintext); + } + + #[test] + fn test_decryption_fails_with_wrong_keypair() { + let plaintext = Bytes(vec![69, 42, 0]); + + let alice = mnemonic_to_pair(&new_mnemonic()).unwrap(); + + let bob = mnemonic_to_pair(&new_mnemonic()).unwrap(); + let bob_secret = derive_static_secret(&bob); + let bob_public_key = PublicKey::from(&bob_secret); + + let charlie = mnemonic_to_pair(&new_mnemonic()).unwrap(); + + // Test encryption + let encrypted_message = EncryptedMessage::new(&alice, &plaintext, &bob_public_key).unwrap(); + + // Test decryption + assert!(encrypted_message.decrypt(&charlie).is_err()); + } +} diff --git a/src/lib.rs b/src/lib.rs index 0492253..f4458c5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,11 @@ +mod encrypt_only; use bip39::Mnemonic; use blake2::{Blake2s256, Digest}; use chacha20poly1305::{ aead::{Aead, AeadCore, KeyInit}, ChaCha20Poly1305, }; +pub use encrypt_only::EncryptedMessage; use hex; use js_sys::Error; use rand_core::OsRng;