Skip to content

Commit a3882dd

Browse files
authored
feat(rust,rust-nodejs): Add napi encrypt function for xchacha20poly1305 (#5217)
* feat(rust,rust-nodejs): Add napi `encrypt` function for xchacha20poly1305 * chore(rust): lint rust
1 parent e3ba090 commit a3882dd

File tree

5 files changed

+105
-5
lines changed

5 files changed

+105
-5
lines changed

ironfish-rust-nodejs/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export const TRANSACTION_EXPIRATION_LENGTH: number
4747
export const TRANSACTION_FEE_LENGTH: number
4848
export const LATEST_TRANSACTION_VERSION: number
4949
export function verifyTransactions(serializedTransactions: Array<Buffer>): boolean
50+
export function encrypt(plaintext: string, passphrase: string): string
5051
export const enum LanguageCode {
5152
English = 0,
5253
ChineseSimplified = 1,

ironfish-rust-nodejs/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ if (!nativeBinding) {
252252
throw new Error(`Failed to load native binding`)
253253
}
254254

255-
const { FishHashContext, KEY_LENGTH, NONCE_LENGTH, BoxKeyPair, randomBytes, boxMessage, unboxMessage, RollingFilter, initSignalHandler, triggerSegfault, ASSET_ID_LENGTH, ASSET_METADATA_LENGTH, ASSET_NAME_LENGTH, ASSET_LENGTH, Asset, NOTE_ENCRYPTION_KEY_LENGTH, MAC_LENGTH, ENCRYPTED_NOTE_PLAINTEXT_LENGTH, ENCRYPTED_NOTE_LENGTH, NoteEncrypted, PUBLIC_ADDRESS_LENGTH, RANDOMNESS_LENGTH, MEMO_LENGTH, AMOUNT_VALUE_LENGTH, DECRYPTED_NOTE_LENGTH, Note, PROOF_LENGTH, TRANSACTION_SIGNATURE_LENGTH, TRANSACTION_PUBLIC_KEY_RANDOMNESS_LENGTH, TRANSACTION_EXPIRATION_LENGTH, TRANSACTION_FEE_LENGTH, LATEST_TRANSACTION_VERSION, TransactionPosted, Transaction, verifyTransactions, UnsignedTransaction, LanguageCode, generateKey, spendingKeyToWords, wordsToSpendingKey, generatePublicAddressFromIncomingViewKey, generateKeyFromPrivateKey, initializeSapling, FoundBlockResult, ThreadPoolHandler, isValidPublicAddress, CpuCount, getCpuCount, generateRandomizedPublicKey, multisig } = nativeBinding
255+
const { FishHashContext, KEY_LENGTH, NONCE_LENGTH, BoxKeyPair, randomBytes, boxMessage, unboxMessage, RollingFilter, initSignalHandler, triggerSegfault, ASSET_ID_LENGTH, ASSET_METADATA_LENGTH, ASSET_NAME_LENGTH, ASSET_LENGTH, Asset, NOTE_ENCRYPTION_KEY_LENGTH, MAC_LENGTH, ENCRYPTED_NOTE_PLAINTEXT_LENGTH, ENCRYPTED_NOTE_LENGTH, NoteEncrypted, PUBLIC_ADDRESS_LENGTH, RANDOMNESS_LENGTH, MEMO_LENGTH, AMOUNT_VALUE_LENGTH, DECRYPTED_NOTE_LENGTH, Note, PROOF_LENGTH, TRANSACTION_SIGNATURE_LENGTH, TRANSACTION_PUBLIC_KEY_RANDOMNESS_LENGTH, TRANSACTION_EXPIRATION_LENGTH, TRANSACTION_FEE_LENGTH, LATEST_TRANSACTION_VERSION, TransactionPosted, Transaction, verifyTransactions, UnsignedTransaction, encrypt, LanguageCode, generateKey, spendingKeyToWords, wordsToSpendingKey, generatePublicAddressFromIncomingViewKey, generateKeyFromPrivateKey, initializeSapling, FoundBlockResult, ThreadPoolHandler, isValidPublicAddress, CpuCount, getCpuCount, generateRandomizedPublicKey, multisig } = nativeBinding
256256

257257
module.exports.FishHashContext = FishHashContext
258258
module.exports.KEY_LENGTH = KEY_LENGTH
@@ -290,6 +290,7 @@ module.exports.TransactionPosted = TransactionPosted
290290
module.exports.Transaction = Transaction
291291
module.exports.verifyTransactions = verifyTransactions
292292
module.exports.UnsignedTransaction = UnsignedTransaction
293+
module.exports.encrypt = encrypt
293294
module.exports.LanguageCode = LanguageCode
294295
module.exports.generateKey = generateKey
295296
module.exports.spendingKeyToWords = spendingKeyToWords

ironfish-rust-nodejs/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pub mod nacl;
2626
pub mod rolling_filter;
2727
pub mod signal_catcher;
2828
pub mod structs;
29+
pub mod xchacha20poly1305;
2930

3031
#[cfg(feature = "stats")]
3132
pub mod stats;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4+
5+
use ironfish::{
6+
serializing::{bytes_to_hex, hex_to_vec_bytes},
7+
xchacha20poly1305,
8+
};
9+
use napi::bindgen_prelude::*;
10+
use napi_derive::napi;
11+
12+
use crate::to_napi_err;
13+
14+
#[napi]
15+
pub fn encrypt(plaintext: String, passphrase: String) -> Result<String> {
16+
let plaintext_bytes = hex_to_vec_bytes(&plaintext).map_err(to_napi_err)?;
17+
let passphrase_bytes = hex_to_vec_bytes(&passphrase).map_err(to_napi_err)?;
18+
let result =
19+
xchacha20poly1305::encrypt(&plaintext_bytes, &passphrase_bytes).map_err(to_napi_err)?;
20+
21+
let mut vec: Vec<u8> = vec![];
22+
result.write(&mut vec).map_err(to_napi_err)?;
23+
24+
Ok(bytes_to_hex(&vec))
25+
}

ironfish-rust/src/xchacha20poly1305.rs

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
44

5+
use std::io;
6+
57
use argon2::{password_hash::SaltString, Argon2};
68
use chacha20poly1305::aead::Aead;
79
use chacha20poly1305::{Key, KeyInit, XChaCha20Poly1305, XNonce};
@@ -12,14 +14,62 @@ use crate::errors::{IronfishError, IronfishErrorKind};
1214
const KEY_LENGTH: usize = 32;
1315
const NONCE_LENGTH: usize = 24;
1416

17+
#[derive(Debug)]
1518
pub struct EncryptOutput {
16-
pub salt: SaltString,
19+
pub salt: Vec<u8>,
1720

1821
pub nonce: [u8; NONCE_LENGTH],
1922

2023
pub ciphertext: Vec<u8>,
2124
}
2225

26+
impl EncryptOutput {
27+
pub fn write<W: io::Write>(&self, mut writer: W) -> Result<(), IronfishError> {
28+
let salt_len = u32::try_from(self.salt.len())?.to_le_bytes();
29+
writer.write_all(&salt_len)?;
30+
writer.write_all(&self.salt)?;
31+
32+
writer.write_all(&self.nonce)?;
33+
34+
let ciphertext_len = u32::try_from(self.ciphertext.len())?.to_le_bytes();
35+
writer.write_all(&ciphertext_len)?;
36+
writer.write_all(&self.ciphertext)?;
37+
38+
Ok(())
39+
}
40+
41+
pub fn read<R: io::Read>(mut reader: R) -> Result<Self, IronfishError> {
42+
let mut salt_len = [0u8; 4];
43+
reader.read_exact(&mut salt_len)?;
44+
let salt_len = u32::from_le_bytes(salt_len) as usize;
45+
46+
let mut salt = vec![0u8; salt_len];
47+
reader.read_exact(&mut salt)?;
48+
49+
let mut nonce = [0u8; NONCE_LENGTH];
50+
reader.read_exact(&mut nonce)?;
51+
52+
let mut ciphertext_len = [0u8; 4];
53+
reader.read_exact(&mut ciphertext_len)?;
54+
let ciphertext_len = u32::from_le_bytes(ciphertext_len) as usize;
55+
56+
let mut ciphertext = vec![0u8; ciphertext_len];
57+
reader.read_exact(&mut ciphertext)?;
58+
59+
Ok(EncryptOutput {
60+
salt,
61+
nonce,
62+
ciphertext,
63+
})
64+
}
65+
}
66+
67+
impl PartialEq for EncryptOutput {
68+
fn eq(&self, other: &EncryptOutput) -> bool {
69+
self.salt == other.salt && self.nonce == other.nonce && self.ciphertext == other.ciphertext
70+
}
71+
}
72+
2373
fn derive_key(passphrase: &[u8], salt: &[u8]) -> Result<Key, IronfishError> {
2474
let mut key = [0u8; KEY_LENGTH];
2575
let argon2 = Argon2::default();
@@ -33,7 +83,9 @@ fn derive_key(passphrase: &[u8], salt: &[u8]) -> Result<Key, IronfishError> {
3383

3484
pub fn encrypt(plaintext: &[u8], passphrase: &[u8]) -> Result<EncryptOutput, IronfishError> {
3585
let salt = SaltString::generate(&mut thread_rng());
36-
let key = derive_key(passphrase, salt.to_string().as_bytes())?;
86+
let salt_str = salt.to_string();
87+
let salt_bytes = salt_str.as_bytes();
88+
let key = derive_key(passphrase, salt_bytes)?;
3789

3890
let cipher = XChaCha20Poly1305::new(&key);
3991
let mut nonce_bytes = [0u8; NONCE_LENGTH];
@@ -45,7 +97,7 @@ pub fn encrypt(plaintext: &[u8], passphrase: &[u8]) -> Result<EncryptOutput, Iro
4597
.map_err(|_| IronfishError::new(IronfishErrorKind::FailedXChaCha20Poly1305Encryption))?;
4698

4799
Ok(EncryptOutput {
48-
salt,
100+
salt: salt_bytes.to_vec(),
49101
nonce: nonce_bytes,
50102
ciphertext,
51103
})
@@ -57,7 +109,7 @@ pub fn decrypt(
57109
) -> Result<Vec<u8>, IronfishError> {
58110
let nonce = XNonce::from_slice(&encrypted_output.nonce);
59111

60-
let key = derive_key(passphrase, encrypted_output.salt.to_string().as_bytes())?;
112+
let key = derive_key(passphrase, &encrypted_output.salt[..])?;
61113
let cipher = XChaCha20Poly1305::new(&key);
62114

63115
cipher
@@ -69,6 +121,8 @@ pub fn decrypt(
69121
mod test {
70122
use crate::xchacha20poly1305::{decrypt, encrypt};
71123

124+
use super::EncryptOutput;
125+
72126
#[test]
73127
fn test_valid_passphrase() {
74128
let plaintext = "thisissensitivedata";
@@ -94,4 +148,22 @@ mod test {
94148
decrypt(encrypted_output, incorrect_passphrase.as_bytes())
95149
.expect_err("should fail decryption");
96150
}
151+
152+
#[test]
153+
fn test_encrypt_output_serialization() {
154+
let plaintext = "thisissensitivedata";
155+
let passphrase = "supersecretpassword";
156+
157+
let encrypted_output = encrypt(plaintext.as_bytes(), passphrase.as_bytes())
158+
.expect("should successfully encrypt");
159+
160+
let mut vec: Vec<u8> = vec![];
161+
encrypted_output
162+
.write(&mut vec)
163+
.expect("should serialize successfully");
164+
165+
let deserialized = EncryptOutput::read(&vec[..]).expect("should deserialize successfully");
166+
167+
assert_eq!(encrypted_output, deserialized);
168+
}
97169
}

0 commit comments

Comments
 (0)