Skip to content

Commit ea85c88

Browse files
authored
Merge pull request #19 from cipherstash/chore/clean-up-interface
Refactor `encrypt` interface and various TS types
2 parents b924d95 + ee65108 commit ea85c88

File tree

11 files changed

+319
-182
lines changed

11 files changed

+319
-182
lines changed

crates/protect-ffi/src/lib.rs

Lines changed: 155 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use cipherstash_client::{
99
ScopedCipher, SteVec, TypeParseError,
1010
},
1111
schema::ColumnConfig,
12-
zerokms::{self, encrypted_record, EncryptedRecord, WithContext, ZeroKMSWithClientKey},
12+
zerokms::{self, EncryptedRecord, WithContext, ZeroKMSWithClientKey},
1313
};
1414
use encrypt_config::{EncryptConfig, Identifier};
1515
use neon::prelude::*;
@@ -43,8 +43,8 @@ impl Finalize for Client {}
4343
pub enum Encrypted {
4444
#[serde(rename = "ct")]
4545
Ciphertext {
46-
#[serde(rename = "c", with = "encrypted_record::formats::mp_base85")]
47-
ciphertext: EncryptedRecord,
46+
#[serde(rename = "c")]
47+
ciphertext: String,
4848
#[serde(rename = "o")]
4949
ore_index: Option<Vec<String>>,
5050
#[serde(rename = "m")]
@@ -139,11 +139,12 @@ async fn new_client_inner(encrypt_config: EncryptConfig) -> Result<Client, Error
139139

140140
fn encrypt(mut cx: FunctionContext) -> JsResult<JsPromise> {
141141
let client = (**cx.argument::<JsBox<Client>>(0)?).clone();
142-
let plaintext = cx.argument::<JsString>(1)?.value(&mut cx);
143-
let column_name = cx.argument::<JsString>(2)?.value(&mut cx);
144-
let table_name = cx.argument::<JsString>(3)?.value(&mut cx);
145-
let lock_context = encryption_context_from_js_value(cx.argument_opt(4), &mut cx)?;
146-
let service_token = service_token_from_js_value(cx.argument_opt(5), &mut cx)?;
142+
let (plaintext_target, ident) = plaintext_target_from_js_object(
143+
cx.argument::<JsObject>(1)?,
144+
&client.encrypt_config,
145+
&mut cx,
146+
)?;
147+
let service_token = service_token_from_js_value(cx.argument_opt(2), &mut cx)?;
147148

148149
let rt = runtime(&mut cx)?;
149150
let channel = cx.channel();
@@ -159,15 +160,7 @@ fn encrypt(mut cx: FunctionContext) -> JsResult<JsPromise> {
159160
//
160161
// This task will _not_ block the JavaScript main thread.
161162
rt.spawn(async move {
162-
let ciphertext_result = encrypt_inner(
163-
client,
164-
plaintext,
165-
column_name,
166-
table_name,
167-
lock_context,
168-
service_token,
169-
)
170-
.await;
163+
let ciphertext_result = encrypt_inner(client, plaintext_target, ident, service_token).await;
171164

172165
// Settle the promise from the result of a closure. JavaScript exceptions
173166
// will be converted to a Promise rejection.
@@ -177,8 +170,7 @@ fn encrypt(mut cx: FunctionContext) -> JsResult<JsPromise> {
177170
// should be performed outside of it.
178171
deferred.settle_with(&channel, move |mut cx| {
179172
let ciphertext = ciphertext_result.or_else(|err| cx.throw_error(err.to_string()))?;
180-
181-
Ok(cx.string(ciphertext))
173+
eql_encrypted_to_js(ciphertext, &mut cx)
182174
});
183175
});
184176

@@ -187,24 +179,13 @@ fn encrypt(mut cx: FunctionContext) -> JsResult<JsPromise> {
187179

188180
async fn encrypt_inner(
189181
client: Client,
190-
plaintext: String,
191-
column_name: String,
192-
table_name: String,
193-
encryption_context: Vec<zerokms::Context>,
182+
plaintext_target: PlaintextTarget,
183+
ident: Identifier,
194184
service_token: Option<ServiceToken>,
195-
) -> Result<String, Error> {
196-
let ident = Identifier::new(table_name, column_name);
197-
198-
let column_config = client
199-
.encrypt_config
200-
.get(&ident)
201-
.ok_or_else(|| Error::UnknownColumn(ident.clone()))?;
202-
185+
) -> Result<Encrypted, Error> {
203186
let mut pipeline = ReferencedPendingPipeline::new(client.cipher);
204-
let mut encryptable = PlaintextTarget::new(plaintext, column_config.clone());
205-
encryptable.context = encryption_context;
206187

207-
pipeline.add_with_ref::<PlaintextTarget>(encryptable, 0)?;
188+
pipeline.add_with_ref::<PlaintextTarget>(plaintext_target, 0)?;
208189

209190
let mut source_encrypted = pipeline.encrypt(service_token).await?;
210191

@@ -214,9 +195,7 @@ async fn encrypt_inner(
214195
)
215196
})?;
216197

217-
let eql_payload = to_eql_encrypted(encrypted, &ident)?;
218-
219-
eql_encrypted_to_json_string(&eql_payload)
198+
to_eql_encrypted(encrypted, &ident)
220199
}
221200

222201
fn encrypt_bulk(mut cx: FunctionContext) -> JsResult<JsPromise> {
@@ -238,7 +217,7 @@ fn encrypt_bulk(mut cx: FunctionContext) -> JsResult<JsPromise> {
238217

239218
deferred.settle_with(&channel, move |mut cx| {
240219
let ciphertexts = ciphertexts_result.or_else(|err| cx.throw_error(err.to_string()))?;
241-
js_array_from_string_vec(ciphertexts, &mut cx)
220+
js_array_from_eql_encrypted_vec(ciphertexts, &mut cx)
242221
});
243222
});
244223

@@ -249,7 +228,7 @@ async fn encrypt_bulk_inner(
249228
client: Client,
250229
plaintext_targets: Vec<(PlaintextTarget, Identifier)>,
251230
service_token: Option<ServiceToken>,
252-
) -> Result<Vec<String>, Error> {
231+
) -> Result<Vec<Encrypted>, Error> {
253232
let len = plaintext_targets.len();
254233
let mut pipeline = ReferencedPendingPipeline::new(client.cipher);
255234
let (plaintext_targets, identifiers): (Vec<PlaintextTarget>, Vec<Identifier>) =
@@ -261,7 +240,7 @@ async fn encrypt_bulk_inner(
261240

262241
let mut source_encrypted = pipeline.encrypt(service_token).await?;
263242

264-
let mut results: Vec<String> = Vec::with_capacity(len);
243+
let mut results: Vec<Encrypted> = Vec::with_capacity(len);
265244

266245
for i in 0..len {
267246
let encrypted = source_encrypted.remove(i).ok_or_else(|| {
@@ -278,7 +257,7 @@ async fn encrypt_bulk_inner(
278257

279258
let eql_payload = to_eql_encrypted(encrypted, ident)?;
280259

281-
results.push(eql_encrypted_to_json_string(&eql_payload)?);
260+
results.push(eql_payload);
282261
}
283262

284263
Ok(results)
@@ -404,18 +383,19 @@ fn service_token_from_js_value(
404383
value: Option<Handle<JsValue>>,
405384
cx: &mut FunctionContext,
406385
) -> NeonResult<Option<ServiceToken>> {
407-
if let Some(service_token) = value {
408-
let service_token: Handle<JsObject> = service_token.downcast_or_throw(cx)?;
386+
match value {
387+
Some(service_token) if is_defined(service_token, cx) => {
388+
let service_token: Handle<JsObject> = service_token.downcast_or_throw(cx)?;
409389

410-
let token = service_token
411-
.get::<JsString, _, _>(cx, "accessToken")?
412-
.value(cx);
390+
let token = service_token
391+
.get::<JsString, _, _>(cx, "accessToken")?
392+
.value(cx);
413393

414-
let expiry = service_token.get::<JsNumber, _, _>(cx, "expiry")?.value(cx);
394+
let expiry = service_token.get::<JsNumber, _, _>(cx, "expiry")?.value(cx);
415395

416-
Ok(Some(ServiceToken::new(token, expiry as u64)))
417-
} else {
418-
Ok(None)
396+
Ok(Some(ServiceToken::new(token, expiry as u64)))
397+
}
398+
_ => Ok(None),
419399
}
420400
}
421401

@@ -430,29 +410,38 @@ fn plaintext_targets_from_js_array(
430410

431411
for js_value in js_values {
432412
let obj: Handle<JsObject> = js_value.downcast_or_throw(cx)?;
413+
let (plaintext_target, ident) = plaintext_target_from_js_object(obj, &encrypt_config, cx)?;
414+
415+
plaintext_targets.push((plaintext_target, ident));
416+
}
433417

434-
let plaintext = obj.get::<JsString, _, _>(cx, "plaintext")?.value(cx);
418+
Ok(plaintext_targets)
419+
}
435420

436-
let column = obj.get::<JsString, _, _>(cx, "column")?.value(cx);
437-
let table = obj.get::<JsString, _, _>(cx, "table")?.value(cx);
421+
fn plaintext_target_from_js_object(
422+
value: Handle<'_, JsObject>,
423+
encrypt_config: &Arc<HashMap<Identifier, ColumnConfig>>,
424+
cx: &mut FunctionContext,
425+
) -> NeonResult<(PlaintextTarget, Identifier)> {
426+
let plaintext = value.get::<JsString, _, _>(cx, "plaintext")?.value(cx);
438427

439-
let lock_context = obj.get_opt::<JsValue, _, _>(cx, "lockContext")?;
440-
let lock_context = encryption_context_from_js_value(lock_context, cx)?;
428+
let column = value.get::<JsString, _, _>(cx, "column")?.value(cx);
429+
let table = value.get::<JsString, _, _>(cx, "table")?.value(cx);
441430

442-
let ident = Identifier::new(table, column);
431+
let lock_context = value.get_opt::<JsValue, _, _>(cx, "lockContext")?;
432+
let lock_context = encryption_context_from_js_value(lock_context, cx)?;
443433

444-
let column_config = encrypt_config
445-
.get(&ident)
446-
.ok_or_else(|| Error::UnknownColumn(ident.clone()))
447-
.or_else(|err| cx.throw_error(err.to_string()))?;
434+
let ident = Identifier::new(table, column);
448435

449-
let mut plaintext_target = PlaintextTarget::new(plaintext, column_config.clone());
450-
plaintext_target.context = lock_context;
436+
let column_config = encrypt_config
437+
.get(&ident)
438+
.ok_or_else(|| Error::UnknownColumn(ident.clone()))
439+
.or_else(|err| cx.throw_error(err.to_string()))?;
451440

452-
plaintext_targets.push((plaintext_target, ident));
453-
}
441+
let mut plaintext_target = PlaintextTarget::new(plaintext, column_config.clone());
442+
plaintext_target.context = lock_context;
454443

455-
Ok(plaintext_targets)
444+
Ok((plaintext_target, ident))
456445
}
457446

458447
fn ciphertexts_from_js_array(
@@ -490,6 +479,34 @@ fn js_array_from_string_vec<'a, C: Context<'a>>(
490479
Ok(js_array)
491480
}
492481

482+
fn js_array_from_u16_vec<'a, C: Context<'a>>(
483+
vec: Vec<u16>,
484+
cx: &mut C,
485+
) -> NeonResult<Handle<'a, JsArray>> {
486+
let js_array = JsArray::new(cx, vec.len());
487+
488+
for (i, value) in vec.iter().enumerate() {
489+
let js_number = cx.number(*value);
490+
js_array.set(cx, i as u32, js_number)?;
491+
}
492+
493+
Ok(js_array)
494+
}
495+
496+
fn js_array_from_eql_encrypted_vec<'a, C: Context<'a>>(
497+
vec: Vec<Encrypted>,
498+
cx: &mut C,
499+
) -> NeonResult<Handle<'a, JsArray>> {
500+
let js_array = JsArray::new(cx, vec.len());
501+
502+
for (i, value) in vec.into_iter().enumerate() {
503+
let js_obj = eql_encrypted_to_js(value, cx)?;
504+
js_array.set(cx, i as u32, js_obj)?;
505+
}
506+
507+
Ok(js_array)
508+
}
509+
493510
fn encrypted_record_from_mp_base85(
494511
base85str: &str,
495512
encryption_context: Vec<zerokms::Context>,
@@ -554,6 +571,12 @@ fn to_eql_encrypted(
554571
};
555572
}
556573

574+
let ciphertext = ciphertext
575+
.to_mp_base85()
576+
// The error type from `to_mp_base85` isn't public, so we don't derive an error for this one.
577+
// Instead, we use `map_err`.
578+
.map_err(|err| Error::Base85(err.to_string()))?;
579+
557580
Ok(Encrypted::Ciphertext {
558581
ciphertext,
559582
identifier: identifier.to_owned(),
@@ -571,6 +594,71 @@ fn to_eql_encrypted(
571594
}
572595
}
573596

597+
fn eql_encrypted_to_js<'cx, C: Context<'cx>>(
598+
encrypted: Encrypted,
599+
cx: &mut C,
600+
) -> NeonResult<Handle<'cx, JsObject>> {
601+
let obj: Handle<JsObject> = cx.empty_object();
602+
603+
let Encrypted::Ciphertext {
604+
ciphertext,
605+
ore_index,
606+
match_index,
607+
unique_index,
608+
identifier,
609+
version,
610+
} = encrypted
611+
else {
612+
return cx
613+
.throw_error(Error::Unimplemented("encrypted JSON columns".to_string()).to_string());
614+
};
615+
616+
let k = cx.string("ct");
617+
obj.set(cx, "k", k)?;
618+
619+
let c = cx.string(ciphertext);
620+
obj.set(cx, "c", c)?;
621+
622+
if let Some(ore_index) = ore_index {
623+
let o = js_array_from_string_vec(ore_index, cx)?;
624+
obj.set(cx, "o", o)?;
625+
} else {
626+
let o = cx.null();
627+
obj.set(cx, "o", o)?;
628+
}
629+
630+
if let Some(match_index) = match_index {
631+
let m = js_array_from_u16_vec(match_index, cx)?;
632+
obj.set(cx, "m", m)?;
633+
} else {
634+
let m = cx.null();
635+
obj.set(cx, "m", m)?;
636+
}
637+
638+
if let Some(unique_index) = unique_index {
639+
let u = cx.string(unique_index);
640+
obj.set(cx, "u", u)?;
641+
} else {
642+
let u = cx.null();
643+
obj.set(cx, "u", u)?;
644+
}
645+
646+
let i = cx.empty_object();
647+
648+
let col = cx.string(identifier.column);
649+
i.set(cx, "c", col)?;
650+
651+
let t = cx.string(identifier.table);
652+
i.set(cx, "t", t)?;
653+
654+
obj.set(cx, "i", i)?;
655+
656+
let v = cx.number(version);
657+
obj.set(cx, "v", v)?;
658+
659+
Ok(obj)
660+
}
661+
574662
fn format_index_term_binary(bytes: &Vec<u8>) -> String {
575663
hex::encode(bytes)
576664
}
@@ -596,13 +684,8 @@ fn format_index_term_ore(bytes: &Vec<u8>) -> Vec<String> {
596684
vec![format_index_term_ore_bytea(bytes)]
597685
}
598686

599-
fn eql_encrypted_to_json_string(encrypted: &Encrypted) -> Result<String, Error> {
600-
serde_json::to_string(encrypted).map_err(|_| {
601-
Error::InvariantViolation(
602-
"expected EQL payload to be serialiable as JSON, but it could not be serialized"
603-
.to_string(),
604-
)
605-
})
687+
fn is_defined(js_value: Handle<'_, JsValue>, cx: &mut FunctionContext) -> bool {
688+
!js_value.is_a::<JsUndefined, _>(cx)
606689
}
607690

608691
#[neon::main]

0 commit comments

Comments
 (0)