0
0
mirror of https://github.com/signalapp/libsignal.git synced 2024-09-20 20:03:07 +02:00

Merge pull request #124 from signalapp/jack/node-binding-curve

Add PublicKey and PrivateKey for Node
This commit is contained in:
Jack Lloyd 2021-01-04 17:10:48 -05:00 committed by GitHub
commit 60c135f1b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 265 additions and 21 deletions

View File

@ -8,14 +8,67 @@ import * as SignalClient from './libsignal_client';
const SC = bindings('libsignal_client') as typeof SignalClient;
export class PublicKey {
private readonly nativeHandle: SignalClient.PublicKey;
private constructor(handle: SignalClient.PublicKey) {
this.nativeHandle = handle;
}
static fromNativeHandle(handle: SignalClient.PublicKey): PublicKey {
return new PublicKey(handle);
}
static deserialize(buf: Buffer): PublicKey {
return new PublicKey(SC.PublicKey_deserialize(buf));
}
serialize(): Buffer {
return SC.PublicKey_serialize(this.nativeHandle);
}
verify(msg: Buffer, sig: Buffer): boolean {
return SC.PublicKey_verify(this.nativeHandle, msg, sig);
}
_unsafeGetNativeHandle(): SignalClient.PublicKey {
return this.nativeHandle;
}
}
export class PrivateKey {
private readonly nativeHandle: SignalClient.PrivateKey;
constructor() {
this.nativeHandle = SC.PrivateKey_generate();
private constructor(handle: SignalClient.PrivateKey) {
this.nativeHandle = handle;
}
static generate(): PrivateKey {
return new PrivateKey(SC.PrivateKey_generate());
}
static deserialize(buf: Buffer): PrivateKey {
return new PrivateKey(SC.PrivateKey_deserialize(buf));
}
serialize(): Buffer {
return SC.PrivateKey_serialize(this.nativeHandle);
}
sign(msg: Buffer): Buffer {
return SC.PrivateKey_sign(this.nativeHandle, msg);
}
agree(other_key: PublicKey): Buffer {
return SC.PrivateKey_agree(
this.nativeHandle,
other_key._unsafeGetNativeHandle()
);
}
getPublicKey(): PublicKey {
return PublicKey.fromNativeHandle(
SC.PrivateKey_getPublicKey(this.nativeHandle)
);
}
}

View File

@ -8,9 +8,25 @@
// FIXME: Eventually we should be able to autogenerate this.
// Newtype pattern from https://kubyshkin.name/posts/newtype-in-typescript/
interface PublicKey {
readonly __type: unique symbol;
}
export function PublicKey_deserialize(buf: Buffer): PublicKey;
export function PublicKey_serialize(key: PublicKey): Buffer;
export function PublicKey_verify(
key: PublicKey,
msg: Buffer,
signature: Buffer
): boolean;
interface PrivateKey {
readonly __type: unique symbol;
}
export function PrivateKey_generate(): PrivateKey;
export function PrivateKey_deserialize(buf: Buffer): PrivateKey;
export function PrivateKey_serialize(key: PrivateKey): Buffer;
export function PrivateKey_sign(key: PrivateKey, msg: Buffer): Buffer;
export function PrivateKey_agree(key: PrivateKey, other_key: PublicKey): Buffer;
export function PrivateKey_getPublicKey(key: PrivateKey): PublicKey;

View File

@ -7,14 +7,71 @@ import { assert } from 'chai';
import * as SignalClient from '../index';
describe('SignalClient', () => {
it('can generate and serialize PrivateKeys', () => {
const a = new SignalClient.PrivateKey();
const b = new SignalClient.PrivateKey();
assert.equal(a.serialize().length, 32, 'correct length');
assert(a.serialize().equals(a.serialize()), 'repeatable');
assert(
!a.serialize().equals(b.serialize()),
it('ECC signatures work', () => {
const priv_a = SignalClient.PrivateKey.generate();
const priv_b = SignalClient.PrivateKey.generate();
assert.lengthOf(priv_a.serialize(), 32, 'private key serialization length');
assert.deepEqual(priv_a.serialize(), priv_a.serialize(), 'repeatable');
assert.notDeepEqual(
priv_a.serialize(),
priv_b.serialize(),
'different for different keys'
);
const pub_a = priv_a.getPublicKey();
const pub_b = priv_b.getPublicKey();
const msg = Buffer.from([1, 2, 3]);
const sig_a = priv_a.sign(msg);
assert.lengthOf(sig_a, 64, 'signature length');
assert(pub_a.verify(msg, sig_a));
assert(!pub_b.verify(msg, sig_a));
const sig_b = priv_b.sign(msg);
assert.lengthOf(sig_b, 64, 'signature length');
assert(pub_b.verify(msg, sig_b));
assert(!pub_a.verify(msg, sig_b));
});
it('ECC key agreement work', () => {
const priv_a = SignalClient.PrivateKey.generate();
const priv_b = SignalClient.PrivateKey.generate();
const pub_a = priv_a.getPublicKey();
const pub_b = priv_b.getPublicKey();
const shared_a = priv_a.agree(pub_b);
const shared_b = priv_b.agree(pub_a);
assert.deepEqual(shared_a, shared_b, 'key agreement works');
});
it('ECC keys roundtrip through serialization', () => {
const key = Buffer.alloc(32, 0xab);
const priv = SignalClient.PrivateKey.deserialize(key);
assert(key.equals(priv.serialize()));
const pub = priv.getPublicKey();
const pub_bytes = pub.serialize();
assert.lengthOf(pub_bytes, 32 + 1);
const pub2 = SignalClient.PublicKey.deserialize(pub_bytes);
assert.deepEqual(pub.serialize(), pub2.serialize());
});
it('decoding invalid ECC key throws an error', () => {
const invalid_key = Buffer.alloc(33, 0xab);
assert.throws(() => {
SignalClient.PrivateKey.deserialize(invalid_key);
}, 'bad key length <33> for key with type <Djb>');
assert.throws(() => {
SignalClient.PublicKey.deserialize(invalid_key);
}, 'bad key type <0xab>');
});
});

View File

@ -6,6 +6,7 @@
use libsignal_protocol_rust::*;
use neon::context::Context;
use neon::prelude::*;
use std::convert::TryFrom;
use std::ops::Deref;
struct DefaultFinalize<T>(T);
@ -22,32 +23,149 @@ impl<T> Deref for DefaultFinalize<T> {
type DefaultJsBox<T> = JsBox<DefaultFinalize<T>>;
fn boxed<'a, T: Send>(cx: &mut FunctionContext<'a>, value: T) -> Handle<'a, DefaultJsBox<T>> {
cx.boxed(DefaultFinalize(value))
fn return_boxed_object<'a, T: 'static + Send>(
cx: &mut FunctionContext<'a>,
value: Result<T, SignalProtocolError>,
) -> JsResult<'a, JsValue> {
match value {
Ok(v) => Ok(cx.boxed(DefaultFinalize(v)).upcast()),
Err(e) => cx.throw_error(e.to_string()),
}
}
fn return_boolean<'a>(
cx: &mut FunctionContext<'a>,
value: Result<bool, SignalProtocolError>,
) -> JsResult<'a, JsValue> {
match value {
Ok(v) => Ok(cx.boolean(v).upcast()),
Err(e) => cx.throw_error(e.to_string()),
}
}
fn return_binary_data<'a, T: AsRef<[u8]>>(
cx: &mut FunctionContext<'a>,
bytes: Result<T, SignalProtocolError>,
) -> JsResult<'a, JsValue> {
match bytes {
Ok(bytes) => {
let bytes = bytes.as_ref();
let bytes_len = match u32::try_from(bytes.len()) {
Ok(l) => l,
Err(_) => {
return cx.throw_error("Cannot return very large object to JS environment")
}
};
let mut buffer = cx.buffer(bytes_len)?;
cx.borrow_mut(&mut buffer, |raw_buffer| {
raw_buffer.as_mut_slice().copy_from_slice(&bytes);
});
Ok(buffer.upcast())
}
Err(e) => cx.throw_error(e.to_string()),
}
}
macro_rules! node_bridge_deserialize {
( $typ:ident::$fn:ident is $node_name:ident ) => {
#[allow(non_snake_case)]
fn $node_name(mut cx: FunctionContext) -> JsResult<JsValue> {
let buffer = cx.argument::<JsBuffer>(0)?;
let obj: Result<$typ, SignalProtocolError> = {
let guard = cx.lock();
let slice = buffer.borrow(&guard).as_slice::<u8>();
$typ::$fn(slice)
};
return_boxed_object(&mut cx, obj)
}
};
}
macro_rules! node_bridge_serialize {
( $typ:ident::$fn:ident is $node_name:ident ) => {
#[allow(non_snake_case)]
fn $node_name(mut cx: FunctionContext) -> JsResult<JsValue> {
let obj = cx.argument::<DefaultJsBox<$typ>>(0)?;
let bytes = obj.$fn();
return_binary_data(&mut cx, Ok(bytes))
}
};
}
#[allow(non_snake_case)]
fn PrivateKey_generate(mut cx: FunctionContext) -> JsResult<JsValue> {
let cx = &mut cx;
let mut rng = rand::rngs::OsRng;
let keypair = KeyPair::generate(&mut rng);
Ok(boxed(cx, keypair.private_key).upcast())
return_boxed_object(&mut cx, Ok(keypair.private_key))
}
node_bridge_deserialize!(PrivateKey::deserialize is PrivateKey_deserialize);
node_bridge_serialize!(PrivateKey::serialize is PrivateKey_serialize);
#[allow(non_snake_case)]
fn PrivateKey_getPublicKey(mut cx: FunctionContext) -> JsResult<JsValue> {
let obj = cx.argument::<DefaultJsBox<PrivateKey>>(0)?;
let new_obj = obj.public_key();
return_boxed_object(&mut cx, new_obj)
}
#[allow(non_snake_case)]
fn PrivateKey_serialize(mut cx: FunctionContext) -> JsResult<JsValue> {
let value = cx.argument::<DefaultJsBox<PrivateKey>>(0)?;
let bytes = value.serialize();
let mut buffer = cx.buffer(bytes.len() as u32)?;
cx.borrow_mut(&mut buffer, |raw_buffer| {
raw_buffer.as_mut_slice().copy_from_slice(&bytes);
});
Ok(buffer.upcast())
fn PrivateKey_sign(mut cx: FunctionContext) -> JsResult<JsValue> {
let key = cx.argument::<DefaultJsBox<PrivateKey>>(0)?;
let message = cx.argument::<JsBuffer>(1)?;
let mut rng = rand::rngs::OsRng;
let signature = {
let guard = cx.lock();
let message = message.borrow(&guard);
key.calculate_signature(message.as_slice::<u8>(), &mut rng)
};
return_binary_data(&mut cx, signature)
}
#[allow(non_snake_case)]
fn PrivateKey_agree(mut cx: FunctionContext) -> JsResult<JsValue> {
let key = cx.argument::<DefaultJsBox<PrivateKey>>(0)?;
let other_key = cx.argument::<DefaultJsBox<PublicKey>>(1)?;
let shared_secret = key.calculate_agreement(&other_key);
return_binary_data(&mut cx, shared_secret)
}
node_bridge_deserialize!(PublicKey::deserialize is PublicKey_deserialize);
node_bridge_serialize!(PublicKey::serialize is PublicKey_serialize);
#[allow(non_snake_case)]
fn PublicKey_verify(mut cx: FunctionContext) -> JsResult<JsValue> {
let key = cx.argument::<DefaultJsBox<PublicKey>>(0)?;
let message = cx.argument::<JsBuffer>(1)?;
let signature = cx.argument::<JsBuffer>(2)?;
let ok = {
let guard = cx.lock();
let message = message.borrow(&guard);
let signature = signature.borrow(&guard);
key.verify_signature(message.as_slice::<u8>(), signature.as_slice::<u8>())
};
return_boolean(&mut cx, ok)
}
#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
cx.export_function("PrivateKey_generate", PrivateKey_generate)?;
cx.export_function("PrivateKey_deserialize", PrivateKey_deserialize)?;
cx.export_function("PrivateKey_serialize", PrivateKey_serialize)?;
cx.export_function("PrivateKey_sign", PrivateKey_sign)?;
cx.export_function("PrivateKey_agree", PrivateKey_agree)?;
cx.export_function("PrivateKey_getPublicKey", PrivateKey_getPublicKey)?;
cx.export_function("PublicKey_verify", PublicKey_verify)?;
cx.export_function("PublicKey_deserialize", PublicKey_deserialize)?;
cx.export_function("PublicKey_serialize", PublicKey_serialize)?;
Ok(())
}