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:
commit
60c135f1b4
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
16
node/libsignal_client.d.ts
vendored
16
node/libsignal_client.d.ts
vendored
@ -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;
|
||||
|
@ -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>');
|
||||
});
|
||||
});
|
||||
|
@ -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(())
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user