Skip to content

Commit

Permalink
Symmetric keygen: JsThemis (#562)
Browse files Browse the repository at this point in the history
Node.js native extensions and V8 are hard.

Initially it was expected that JsThemis has an interface similar to
WasmThemis. Something like this:

    class SymmetricKey extends Buffer {
        constructor(array) {
            if (array) {
                super(array)
            } else {
                super(generateNewKey())
            }
        }
    }

Note SymmetricKey is a class which inherits from Buffer. Now consider
the fact that JavaScript does not *really* have classes, it has objects,
functions, and prototypes, with EcmaScript 6 classes being a nice syntax
sugar on top of that.

It turned out to be not obvious how to implement JavaScript inheritance
from native JavaScript classes given the API provided by V8 (and I'm not
even talking about the outstanding quality of V8 documentation /s).

This is further complicated by the fact that all of the Buffer's
constructors are considered deprecated, with Buffer.alloc() and
Buffer.from() being the recommended way of constructing Buffers.

I'm in a bit of a loss here so effectively SymmetricKey is now

    function SymmetricKey(array) {
        if (array) {
            return Buffer.from(array)
        } else {
            return generateKeyBuffer()
        }
    }

This kinda works on the API level because classes are functions in
JavaScript, but it's not strictly correct as "new SymmetricKey()"
expression returns an instance of Buffer (not SymmetricKey). It's
fine since that's all API that we need at the moment, but it's not
clean. However, after spending around 4 hours trying to understand
how to do inheritance, I kinda gave up.

This should be enough for practical purposes for now. We will have
to get back to this issue if we would like to provide our own wrapper
over Buffer instances for all other keys, but for now it's good enough.

*fingers crossed*
  • Loading branch information
ilammy authored Dec 11, 2019
1 parent 5f886e1 commit e1196ea
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 3 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ _Code:_
- Fixed a NullPointerException bug in `SecureSocket` initialisation ([#557](https://github.com/cossacklabs/themis/pull/557)).
- Some Themis exceptions have been converted from checked `Exception` to _unchecked_ `RuntimeException`, relaxing requirements for `throws` specifiers ([#563](https://github.com/cossacklabs/themis/pull/563)).

- **Node.js**

- New class `SymmetricKey` can be used to generate symmetric keys for Secure Cell ([#562](https://github.com/cossacklabs/themis/pull/562)).

- **Python**

- Fixed compatibility issues on 32-bit platforms ([#555](https://github.com/cossacklabs/themis/pull/555)).
Expand Down
1 change: 1 addition & 0 deletions src/wrappers/themis/jsthemis/addon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ void InitAll(v8::Local<v8::Object> exports)
jsthemis::Errors::Init(exports);
jsthemis::SecureMessage::Init(exports);
jsthemis::KeyPair::Init(exports);
jsthemis::SymmetricKey::Init(exports);
jsthemis::SecureSession::Init(exports);
jsthemis::SecureCellSeal::Init(exports);
jsthemis::SecureCellContextImprint::Init(exports);
Expand Down
96 changes: 96 additions & 0 deletions src/wrappers/themis/jsthemis/secure_keygen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -173,4 +173,100 @@ bool IsPublicKey(const std::vector<uint8_t>& key)
return false;
}

Nan::Persistent<v8::Function> SymmetricKey::constructor;

void SymmetricKey::Init(v8::Local<v8::Object> exports)
{
v8::Local<v8::String> className = Nan::New("SymmetricKey").ToLocalChecked();

// Prepare constructor template
v8::Local<v8::FunctionTemplate> thisTemplate = Nan::New<v8::FunctionTemplate>(SymmetricKey::New);
thisTemplate->SetClassName(className);

// Export constructor
v8::Local<v8::Function> function = Nan::GetFunction(thisTemplate).ToLocalChecked();
constructor.Reset(function);
Nan::Set(exports, className, function);
}

void SymmetricKey::New(const Nan::FunctionCallbackInfo<v8::Value>& args)
{
// If not invoked as "new themis.SymmetricKey(...)" then reinvoke.
if (!args.IsConstructCall()) {
// We support at most one argument, pass it and ignore others.
v8::Local<v8::Value> argv[1] = {args[0]};
v8::Local<v8::Function> cons = Nan::New<v8::Function>(constructor);
args.GetReturnValue().Set(Nan::NewInstance(cons, args.Length(), argv).ToLocalChecked());
return;
}

// If invoked as "new themis.SymmetricKey()" then generate a new key.
if (args.Length() == 0) {
std::vector<uint8_t> buffer;

size_t length = 0;
themis_status_t status = themis_gen_sym_key(NULL, &length);
if (status != THEMIS_BUFFER_TOO_SMALL) {
ThrowError("Themis SymmetricKey", status);
args.GetReturnValue().SetUndefined();
return;
}

buffer.resize(length);
status = themis_gen_sym_key(&buffer.front(), &length);
if (status != THEMIS_SUCCESS) {
ThrowError("Themis SymmetricKey", status);
args.GetReturnValue().SetUndefined();
return;
}

args.GetReturnValue().Set(CopyIntoBuffer(buffer));
return;
}

// If invoked as "new themis.SymmetricKey(value)" then value must be
// a byte buffer that we copy.
v8::Local<v8::Value> value = args[0];
if (!value->IsUint8Array()) {
ThrowParameterError("Themis SymmetricKey",
"key is not a byte buffer (use Buffer or Uint8Array)");
args.GetReturnValue().SetUndefined();
return;
}
if (node::Buffer::Length(value) == 0) {
ThrowParameterError("Themis SymmetricKey", "key is empty");
args.GetReturnValue().SetUndefined();
return;
}

args.GetReturnValue().Set(CopyIntoBuffer(value));
return;
}

// TODO: return properly inherited instances of SymmetricKey
//
// Currently "new themis.SymmetricKey()" produces instances of Buffer.
// This works in practice (because JavaScript), but it may be unexpected
// as "key instanceof themis.SymmetricKey" returns false.
//
// Unfortunately, V8 does not make JavaScript prototype inheritance easier
// and I was not able to implement it correctly. It would be nice is someone
// made SymmetricKey constructor return SymmetricKey instances that inherit
// from Buffer and get all Node.js utities for free.

v8::Local<v8::Object> SymmetricKey::CopyIntoBuffer(const std::vector<uint8_t>& buffer)
{
const char* data = NULL;
if (!buffer.empty()) {
data = reinterpret_cast<const char*>(&buffer.front());
}
uint32_t length = buffer.size();
return Nan::CopyBuffer(data, length).ToLocalChecked();
}

v8::Local<v8::Object> SymmetricKey::CopyIntoBuffer(v8::Local<v8::Value> buffer)
{
return Nan::CopyBuffer(node::Buffer::Data(buffer), node::Buffer::Length(buffer)).ToLocalChecked();
}

} // namespace jsthemis
14 changes: 14 additions & 0 deletions src/wrappers/themis/jsthemis/secure_keygen.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@ bool IsValidKey(const std::vector<uint8_t>& key);
bool IsPrivateKey(const std::vector<uint8_t>& key);
bool IsPublicKey(const std::vector<uint8_t>& key);

class SymmetricKey : public Nan::ObjectWrap
{
public:
static void Init(v8::Local<v8::Object> exports);

private:
static void New(const Nan::FunctionCallbackInfo<v8::Value>& args);

static v8::Local<v8::Object> CopyIntoBuffer(const std::vector<uint8_t>& buffer);
static v8::Local<v8::Object> CopyIntoBuffer(v8::Local<v8::Value> buffer);

static Nan::Persistent<v8::Function> constructor;
};

} // namespace jsthemis

#endif /* JSTHEMIS_KEY_PAIR_HPP_ */
42 changes: 39 additions & 3 deletions tests/jsthemis/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe("jsthemis", function(){
decrypter = new addon.SecureMessage(peer_keypair.private(), keypair.public());
intruder_decrypter = new addon.SecureMessage(intruder_keypair.private(), keypair.public());
message = new Buffer("Test Message");
it("encrypt/decrypt", function(){
it("encrypt/decrypt", function(){
encrypted_message = encrypter.encrypt(message);
assert.equal(message.toString(), decrypter.decrypt(encrypted_message).toString());
assert.throws(function(){intruder_decrypter.decrypt(encrypted_message);}, expect_code(addon.FAIL));
Expand Down Expand Up @@ -86,7 +86,7 @@ describe("jsthemis", function(){
server_keypair = new addon.KeyPair();
client_id = new Buffer("client");
client_keypair = new addon.KeyPair();

server_session = new addon.SecureSession(server_id, server_keypair.private(), function(id){
if(id.toString()=="server")
return server_keypair.public();
Expand Down Expand Up @@ -175,6 +175,42 @@ describe("jsthemis", function(){

describe("jsthemis", function(){
describe("secure cell", function(){
describe("key generation", function(){
const defaultLength = 32
it("generates new key buffer", function(){
var masterKey = new addon.SymmetricKey()
assert.equal(masterKey.length, defaultLength)
})
it("generates new instances", function(){
// Check that we don't reuse the same object
var key1 = new addon.SymmetricKey()
var key2 = new addon.SymmetricKey()
assert.notDeepEqual(key1, key2)
assert.notEqual(key1, key2)
// A copy should have the same content,
// but it's a distint object
var key3 = new addon.SymmetricKey(key2)
assert.deepEqual(key3, key2)
assert.notEqual(key3, key2)
assert.notDeepEqual(key3, key1)
assert.notEqual(key3, key1)
})
it("is able to restore SymmetricKey from bytes", function(){
var bytes = Buffer.from("MDRwUzB0NG1aN2pvTEEwdVljRFJ5", "base64")
var masterKey = new addon.SymmetricKey(bytes)
assert.equal(masterKey.length, bytes.length)
})
it("throws on empty buffer", function(){
assert.throws(() => new addon.SymmetricKey(Buffer.from("")),
expect_code(addon.INVALID_PARAMETER))
assert.throws(() => new addon.SymmetricKey(""),
expect_code(addon.INVALID_PARAMETER))
assert.throws(() => new addon.SymmetricKey(null),
expect_code(addon.INVALID_PARAMETER))
assert.throws(() => new addon.SymmetricKey(undefined),
expect_code(addon.INVALID_PARAMETER))
})
})
message=new Buffer("This is test message");
password=new Buffer("This is test password");
context=new Buffer("This is test context");
Expand Down Expand Up @@ -217,7 +253,7 @@ describe("jsthemis", function(){
context_imprint_intruder_decrypter = new addon.SecureCellContextImprint(new Buffer("This is test password1"));
assert.throws(function(){new addon.SecureCellContextImprint(empty_message)});
context_imprint_enc_data = context_imprint_encrypter.encrypt(message, context);
assert.equal(message.length, context_imprint_enc_data.length);
assert.equal(message.length, context_imprint_enc_data.length);
context_imprint_dec_data = context_imprint_decrypter.decrypt(context_imprint_enc_data, context);
assert.equal(message.toString(), context_imprint_dec_data.toString());
context_imprint_dec_data = context_imprint_intruder_decrypter.decrypt(context_imprint_enc_data, context);
Expand Down

0 comments on commit e1196ea

Please sign in to comment.