self_encryption_nodejs/
lib.rs

1//! Node.js bindings for self-encryption.
2//!
3//! This library provides Node.js bindings for the self-encryption library, which
4//! provides convergent encryption on file-based data and produces a `DataMap` type and
5//! several chunks of encrypted data. Each chunk is up to 1MB in size and has an index and a name.
6//! This name is the SHA3-256 hash of the content, which allows the chunks to be self-validating.
7//!
8//! Storage of the encrypted chunks or DataMap is outside the scope of this library
9//! and must be implemented by the user.
10
11use napi::JsBuffer;
12use napi::JsObject;
13use napi::NapiRaw;
14use napi::Result;
15use napi::Status;
16use napi::bindgen_prelude::*;
17use napi_derive::napi;
18use self_encryption::bytes::Bytes;
19use self_encryption::xor_name::XOR_NAME_LEN;
20use std::path::Path;
21
22pub mod util;
23
24// Convert Rust errors to JavaScript errors
25fn map_error<E>(err: E) -> napi::Error
26where
27    E: std::error::Error,
28{
29    let mut err_str = String::new();
30    err_str.push_str(&format!("{err:?}: {err}\n"));
31    let mut source = err.source();
32    while let Some(err) = source {
33        err_str.push_str(&format!(" Caused by: {err:?}: {err}\n"));
34        source = err.source();
35    }
36
37    napi::Error::new(Status::GenericFailure, err_str)
38}
39
40fn try_from_big_int<T: TryFrom<u64>>(value: BigInt, arg: &str) -> Result<T> {
41    let (_signed, value, losless) = value.get_u64();
42    if losless {
43        if let Ok(value) = T::try_from(value) {
44            return Ok(value);
45        }
46    }
47
48    Err(napi::Error::new(
49        Status::InvalidArg,
50        format!(
51            "expected `{arg}` to fit in a {}",
52            std::any::type_name::<T>()
53        ),
54    ))
55}
56
57/// The minimum size (before compression) of an individual chunk of a file, defined as 1B.
58#[napi]
59pub const MIN_CHUNK_SIZE: usize = self_encryption::MIN_CHUNK_SIZE;
60/// The maximum size (before compression) of an individual chunk of a file, defaulting as 1MiB.
61#[napi]
62pub const MAX_CHUNK_SIZE: usize = self_encryption::MAX_CHUNK_SIZE;
63/// Controls the compression-speed vs compression-density tradeoffs. The higher the quality, the slower the compression. Range is 0 to 11.
64#[napi]
65pub const COMPRESSION_QUALITY: i32 = self_encryption::COMPRESSION_QUALITY;
66/// The minimum size (before compression) of data to be self-encrypted, defined as 3B.
67#[napi]
68pub const MIN_ENCRYPTABLE_BYTES: usize = self_encryption::MIN_ENCRYPTABLE_BYTES;
69
70/// A 256-bit number, viewed as a point in XOR space.
71///
72/// This wraps an array of 32 bytes, i. e. a number between 0 and 2<sup>256</sup> - 1.
73///
74/// XOR space is the space of these numbers, with the [XOR metric][1] as a notion of distance,
75/// i. e. the points with IDs `x` and `y` are considered to have distance `x xor y`.
76///
77/// [1]: https://en.wikipedia.org/wiki/Kademlia#System_details
78#[napi]
79#[derive(Clone)]
80pub struct XorName(self_encryption::XorName);
81
82#[napi]
83impl XorName {
84    /// Create a new XorName from content bytes.
85    #[napi(factory)]
86    pub fn from_content(content: Uint8Array) -> Self {
87        Self(self_encryption::XorName::from_content(content.as_ref()))
88    }
89
90    /// Get the underlying bytes of the XorName.
91    #[napi]
92    pub fn as_bytes(&self) -> Uint8Array {
93        Uint8Array::from(self.0.0.to_vec())
94    }
95
96    #[napi]
97    pub fn to_hex(&self) -> String {
98        hex::encode(self.0.0)
99    }
100
101    #[napi]
102    pub fn from_hex(hex: String) -> Result<Self> {
103        let mut bytes = [0u8; XOR_NAME_LEN];
104        hex::decode_to_slice(hex, &mut bytes)
105            .map_err(|e| napi::Error::new(Status::InvalidArg, e.to_string()))?;
106        Ok(Self(self_encryption::XorName(bytes)))
107    }
108}
109
110/// This is - in effect - a partial decryption key for an encrypted chunk of data.
111///
112/// It holds pre- and post-encryption hashes as well as the original
113/// (pre-compression) size for a given chunk.
114/// This information is required for successful recovery of a chunk, as well as for the
115/// encryption/decryption of it's two immediate successors, modulo the number of chunks in the
116/// corresponding DataMap.
117#[napi]
118pub struct ChunkInfo(self_encryption::ChunkInfo);
119
120#[napi]
121impl ChunkInfo {
122    #[napi(constructor)]
123    pub fn new(index: u32, dst_hash: &XorName, src_hash: &XorName, src_size: u32) -> Self {
124        Self(self_encryption::ChunkInfo {
125            index: index as usize,
126            dst_hash: dst_hash.0,
127            src_hash: src_hash.0,
128            src_size: src_size as usize,
129        })
130    }
131
132    #[napi(getter)]
133    pub fn index(&self) -> u32 {
134        self.0.index as u32
135    }
136
137    #[napi(getter)]
138    pub fn dst_hash(&self) -> XorName {
139        XorName(self.0.dst_hash)
140    }
141
142    #[napi(getter)]
143    pub fn src_hash(&self) -> XorName {
144        XorName(self.0.src_hash)
145    }
146
147    #[napi(getter)]
148    pub fn src_size(&self) -> u32 {
149        self.0.src_size as u32
150    }
151}
152
153/// Holds the information that is required to recover the content of the encrypted file.
154/// This is held as a vector of `ChunkInfo`, i.e. a list of the file's chunk hashes.
155/// Only files larger than 3072 bytes (3 * MIN_CHUNK_SIZE) can be self-encrypted.
156/// Smaller files will have to be batched together.
157#[napi]
158pub struct DataMap(self_encryption::DataMap);
159
160#[napi]
161#[allow(clippy::len_without_is_empty)]
162impl DataMap {
163    /// A new instance from a vec of partial keys.
164    ///
165    /// Sorts on instantiation.
166    /// The algorithm requires this to be a sorted list to allow get_pad_iv_key to obtain the
167    /// correct pre-encryption hashes for decryption/encryption.
168    #[napi]
169    pub fn new(keys: Vec<&ChunkInfo>) -> Self {
170        Self(self_encryption::DataMap::new(
171            keys.iter().map(|ci| ci.0.clone()).collect(),
172        ))
173    }
174
175    /// Creates a new DataMap with a specified child value
176    #[napi]
177    pub fn with_child(keys: Vec<&ChunkInfo>, child: BigInt) -> Result<Self> {
178        let child = try_from_big_int(child, "child")?;
179
180        Ok(Self(self_encryption::DataMap::with_child(
181            keys.iter().map(|ci| ci.0.clone()).collect(),
182            child,
183        )))
184    }
185
186    /// Original (pre-encryption) size of the file.
187    #[napi]
188    pub fn original_file_size(&self) -> usize {
189        self.0.original_file_size()
190    }
191
192    /// Returns the list of chunks pre and post encryption hashes if present.
193    #[napi]
194    pub fn infos(&self) -> Vec<ChunkInfo> {
195        self.0.infos().into_iter().map(ChunkInfo).collect()
196    }
197
198    /// Returns the child value if set
199    #[napi]
200    pub fn child(&self) -> Option<usize> {
201        self.0.child
202    }
203
204    /// Returns the number of chunks in the DataMap
205    #[napi]
206    pub fn len(&self) -> usize {
207        self.0.len()
208    }
209
210    /// Returns true if this DataMap has a child value
211    #[napi]
212    pub fn is_child(&self) -> bool {
213        self.0.is_child()
214    }
215}
216
217/// A JavaScript wrapper for the EncryptedChunk struct.
218///
219/// EncryptedChunk represents an encrypted piece of data along with its
220/// metadata like size and hash. Chunks are stored separately and
221/// referenced by the DataMap.
222#[napi]
223#[derive(Clone)]
224pub struct EncryptedChunk(self_encryption::EncryptedChunk);
225
226#[napi]
227impl EncryptedChunk {
228    /// Get the size of the original content before encryption.
229    #[napi]
230    pub fn content_size(&self) -> u32 {
231        self.0.content.len() as u32
232    }
233
234    /// Get the hash of the encrypted chunk.
235    #[napi]
236    pub fn hash(&self) -> Uint8Array {
237        Uint8Array::from(
238            self_encryption::XorName::from_content(&self.0.content)
239                .0
240                .to_vec(),
241        )
242    }
243
244    /// Get the content of the encrypted chunk.
245    #[napi]
246    pub fn content(&self) -> Uint8Array {
247        Uint8Array::from(self.0.content.to_vec())
248    }
249}
250
251/// Encrypt raw data into chunks.
252///
253/// This function takes raw data, splits it into chunks, encrypts them,
254/// and returns a DataMap and list of encrypted chunks.
255#[napi]
256pub fn encrypt(data: Uint8Array) -> Result<EncryptResult> {
257    let data = Bytes::copy_from_slice(data.as_ref());
258    let (data_map, chunks) = self_encryption::encrypt(data).map_err(map_error)?;
259
260    Ok(EncryptResult { data_map, chunks })
261}
262
263/// Decrypts data using chunks retrieved from any storage backend via the provided retrieval function.
264#[napi]
265pub fn decrypt(data_map: &DataMap, chunks: Vec<&EncryptedChunk>) -> Result<Uint8Array> {
266    let inner_chunks = chunks
267        .into_iter()
268        .map(|chunk| chunk.0.clone())
269        .collect::<Vec<_>>();
270
271    let bytes = self_encryption::decrypt(&data_map.0, &inner_chunks).map_err(map_error)?;
272
273    Ok(Uint8Array::from(bytes.to_vec()))
274}
275
276/// Decrypts data using a DataMap and stored chunks.
277///
278/// This function retrieves encrypted chunks using the provided callback,
279/// decrypts them according to the DataMap, and writes the result to a file.
280#[napi]
281pub fn decrypt_from_storage(
282    env: Env,
283    data_map: &DataMap,
284    output_file: String,
285    #[napi(ts_arg_type = "(xorName: XorName) => Uint8Array")] get_chunk: JsFunction,
286) -> Result<()> {
287    let output_path = Path::new(&output_file);
288
289    let get_chunk_wrapper = |xor_name: self_encryption::XorName| -> self_encryption::Result<Bytes> {
290        let xor_name = XorName(xor_name.clone());
291        let xor_name = unsafe { XorName::to_napi_value(env.raw(), xor_name) }.unwrap();
292        let xor_name = unsafe { napi::JsUnknown::from_napi_value(env.raw(), xor_name) }.unwrap();
293
294        // Call the JavaScript function with the chunk name
295        let result = get_chunk.call(None, &[xor_name]).map_err(|e| {
296            self_encryption::Error::Generic(format!("`getChunk` call resulted in error: {e}\n"))
297        })?;
298
299        let data =
300            unsafe { Uint8Array::from_napi_value(env.raw(), result.raw()) }.map_err(|e| {
301                self_encryption::Error::Generic(format!(
302                    "Could not convert getChunk result to Uint8Array: {e}\n"
303                ))
304            })?;
305
306        Ok(Bytes::copy_from_slice(data.as_ref()))
307    };
308
309    self_encryption::decrypt_from_storage(&data_map.0, output_path, get_chunk_wrapper)
310        .map_err(map_error)
311}
312
313/// Decrypts data from storage in a streaming fashion using parallel chunk retrieval.
314///
315/// This function retrieves the encrypted chunks in parallel using the provided `getChunkParallel` function,
316/// decrypts them, and writes the decrypted data directly to the specified output file path.
317#[napi]
318pub fn streaming_decrypt_from_storage(
319    env: Env,
320    data_map: &DataMap,
321    output_file: String,
322    #[napi(ts_arg_type = "(xorNames: XorName[]) => Uint8Array")] get_chunk_parallel: JsFunction,
323) -> Result<()> {
324    let output_path = Path::new(&output_file);
325
326    let get_chunk_parallel_wrapper =
327        |xor_name: &[self_encryption::XorName]| -> self_encryption::Result<Vec<Bytes>> {
328            // `Vec<XorName>` -> `Vec<JsXorName> -> `Vec<JsUnknown>`
329            let xor_names = xor_name
330                .iter()
331                .map(|xor_name| {
332                    let xor_name = XorName(xor_name.clone());
333                    let xor_name = unsafe { XorName::to_napi_value(env.raw(), xor_name) }.unwrap();
334                    unsafe { napi::JsUnknown::from_napi_value(env.raw(), xor_name) }.unwrap()
335                })
336                .collect::<Vec<_>>();
337
338            // `Vec<JsUnknown>` -> JS `Array` -> `JsObject` -> `JsUnknown`
339            let xor_names = Array::from_vec(&env, xor_names)
340                .map_err(|e| {
341                    self_encryption::Error::Generic(format!("Could not create array: {e}\n"))
342                })?
343                // Map JS `Array` to `JsObject` (as `Array` can not be converted to `JsUnknown`)
344                .coerce_to_object()
345                .map_err(|e| {
346                    self_encryption::Error::Generic(format!("Could not create array: {e}\n"))
347                })?
348                .into_unknown();
349
350            // Call the JavaScript function with the XOR names
351            let result = get_chunk_parallel.call(None, &[xor_names]).map_err(|e| {
352                self_encryption::Error::Generic(format!(
353                    "`getChunkParallel` call resulted in error: {e}\n"
354                ))
355            })?;
356
357            let data =
358                unsafe { JsObject::from_napi_value(env.raw(), result.raw()) }.map_err(|e| {
359                    self_encryption::Error::Generic(format!(
360                        "Could not convert getChunkParallel result to Array: {e}\n"
361                    ))
362                })?;
363
364            let mut data_vec = vec![];
365            for i in 0..data.get_array_length().map_err(|e| {
366                self_encryption::Error::Generic(format!(
367                    "Expect getChunkParallel to return array: {e}\n"
368                ))
369            })? {
370                let item = data.get_element::<JsBuffer>(i).map_err(|e| {
371                    self_encryption::Error::Generic(format!(
372                        "Could not get element from getChunkParallel result: {e}\n"
373                    ))
374                })?;
375                let item = item.into_ref().map_err(|e| {
376                    self_encryption::Error::Generic(format!(
377                        "Could not get element from getChunkParallel result: {e}\n"
378                    ))
379                })?;
380
381                data_vec.push(Bytes::copy_from_slice(item.as_ref()));
382            }
383
384            Ok(data_vec)
385        };
386
387    self_encryption::streaming_decrypt_from_storage(
388        &data_map.0,
389        output_path,
390        get_chunk_parallel_wrapper,
391    )
392    .map_err(map_error)
393}
394
395// Reads a file in chunks, encrypts them, and stores them using a provided functor. Returns a DataMap.
396#[napi]
397pub fn streaming_encrypt_from_file(
398    env: Env,
399    file_path: String,
400    #[napi(ts_arg_type = "(xorName: XorName, bytes: Uint8Array) => undefined")]
401    chunk_store: JsFunction,
402) -> Result<DataMap> {
403    let file_path = Path::new(&file_path);
404
405    let chunk_store_wrapper = |xor_name: self_encryption::XorName,
406                               bytes: Bytes|
407     -> self_encryption::Result<()> {
408        let xor_name = XorName(xor_name);
409        let xor_name = unsafe { XorName::to_napi_value(env.raw(), xor_name) }.unwrap();
410        let xor_name = unsafe { napi::JsUnknown::from_napi_value(env.raw(), xor_name) }.unwrap();
411
412        let bytes = Uint8Array::from(bytes.to_vec());
413        let bytes = unsafe { Uint8Array::to_napi_value(env.raw(), bytes) }.unwrap();
414        let bytes = unsafe { napi::JsUnknown::from_napi_value(env.raw(), bytes) }.unwrap();
415
416        let _ = chunk_store.call(None, &[xor_name, bytes]).map_err(|e| {
417            self_encryption::Error::Generic(format!("`chunkStore` call resulted in error: {e}\n"))
418        })?;
419
420        Ok(())
421    };
422
423    self_encryption::streaming_encrypt_from_file(file_path, chunk_store_wrapper)
424        .map(|dm| DataMap(dm))
425        .map_err(map_error)
426}
427
428/// Encrypt a file and store its chunks.
429///
430/// This function reads a file, splits it into chunks, encrypts them,
431/// and stores them in the specified directory.
432#[napi]
433pub fn encrypt_from_file(input_file: String, output_dir: String) -> Result<EncryptFromFileResult> {
434    let input_path = Path::new(&input_file);
435    let output_path = Path::new(&output_dir);
436
437    let (data_map, chunk_names) =
438        self_encryption::encrypt_from_file(input_path, output_path).map_err(map_error)?;
439
440    let chunk_names = chunk_names.iter().map(|name| hex::encode(name.0)).collect();
441
442    Ok(EncryptFromFileResult {
443        data_map,
444        chunk_names,
445    })
446}
447
448/// Result type for the encrypt_from_file function
449#[napi]
450pub struct EncryptFromFileResult {
451    pub(crate) data_map: self_encryption::DataMap,
452    pub(crate) chunk_names: Vec<String>,
453}
454
455#[napi]
456impl EncryptFromFileResult {
457    #[napi(getter)]
458    pub fn data_map(&self) -> DataMap {
459        DataMap(self.data_map.clone())
460    }
461
462    #[napi(getter)]
463    pub fn chunk_names(&self) -> Vec<String> {
464        self.chunk_names.clone()
465    }
466}
467
468/// Result type for the encrypt function
469#[napi]
470pub struct EncryptResult {
471    pub(crate) data_map: self_encryption::DataMap,
472    pub(crate) chunks: Vec<self_encryption::EncryptedChunk>,
473}
474
475#[napi]
476impl EncryptResult {
477    #[napi(getter)]
478    pub fn data_map(&self) -> DataMap {
479        DataMap(self.data_map.clone())
480    }
481
482    #[napi(getter)]
483    pub fn chunks(&self) -> Vec<EncryptedChunk> {
484        self.chunks
485            .iter()
486            .map(|chunk| EncryptedChunk(chunk.clone()))
487            .collect()
488    }
489}