crypto: support GCM authenticated encryption mode.
This adds two new member functions getAuthTag and setAuthTag that are useful for AES-GCM encryption modes. Use getAuthTag after Cipheriv.final, transmit the tag along with the data and use Decipheriv.setAuthTag to have the encrypted data verified.
This commit is contained in:
parent
f9f9239fa2
commit
e0d31ea2db
@ -218,6 +218,13 @@ multiple of the cipher's block size or `final` will fail. Useful for
|
||||
non-standard padding, e.g. using `0x0` instead of PKCS padding. You
|
||||
must call this before `cipher.final`.
|
||||
|
||||
### cipher.getAuthTag()
|
||||
|
||||
For authenticated encryption modes (currently supported: GCM), this
|
||||
method returns a `Buffer` that represents the _authentication tag_ that
|
||||
has been computed from the given data. Should be called after
|
||||
encryption has been completed using the `final` method!
|
||||
|
||||
|
||||
## crypto.createDecipher(algorithm, password)
|
||||
|
||||
@ -268,6 +275,15 @@ removing it. Can only work if the input data's length is a multiple of
|
||||
the ciphers block size. You must call this before streaming data to
|
||||
`decipher.update`.
|
||||
|
||||
### decipher.setAuthTag(buffer)
|
||||
|
||||
For authenticated encryption modes (currently supported: GCM), this
|
||||
method must be used to pass in the received _authentication tag_.
|
||||
If no tag is provided or if the ciphertext has been tampered with,
|
||||
`final` will throw, thus indicating that the ciphertext should
|
||||
be discarded due to failed authentication.
|
||||
|
||||
|
||||
## crypto.createSign(algorithm)
|
||||
|
||||
Creates and returns a signing object, with the given algorithm. On
|
||||
|
@ -322,6 +322,15 @@ Cipheriv.prototype.update = Cipher.prototype.update;
|
||||
Cipheriv.prototype.final = Cipher.prototype.final;
|
||||
Cipheriv.prototype.setAutoPadding = Cipher.prototype.setAutoPadding;
|
||||
|
||||
Cipheriv.prototype.getAuthTag = function() {
|
||||
return this._binding.getAuthTag();
|
||||
};
|
||||
|
||||
|
||||
Cipheriv.prototype.setAuthTag = function(tagbuf) {
|
||||
this._binding.setAuthTag(tagbuf);
|
||||
};
|
||||
|
||||
|
||||
|
||||
exports.createDecipher = exports.Decipher = Decipher;
|
||||
@ -367,6 +376,8 @@ Decipheriv.prototype.update = Cipher.prototype.update;
|
||||
Decipheriv.prototype.final = Cipher.prototype.final;
|
||||
Decipheriv.prototype.finaltol = Cipher.prototype.final;
|
||||
Decipheriv.prototype.setAutoPadding = Cipher.prototype.setAutoPadding;
|
||||
Decipheriv.prototype.getAuthTag = Cipheriv.prototype.getAuthTag;
|
||||
Decipheriv.prototype.setAuthTag = Cipheriv.prototype.setAuthTag;
|
||||
|
||||
|
||||
|
||||
|
@ -2122,6 +2122,8 @@ void CipherBase::Initialize(Environment* env, Handle<Object> target) {
|
||||
NODE_SET_PROTOTYPE_METHOD(t, "update", Update);
|
||||
NODE_SET_PROTOTYPE_METHOD(t, "final", Final);
|
||||
NODE_SET_PROTOTYPE_METHOD(t, "setAutoPadding", SetAutoPadding);
|
||||
NODE_SET_PROTOTYPE_METHOD(t, "getAuthTag", GetAuthTag);
|
||||
NODE_SET_PROTOTYPE_METHOD(t, "setAuthTag", SetAuthTag);
|
||||
|
||||
target->Set(FIXED_ONE_BYTE_STRING(node_isolate, "CipherBase"),
|
||||
t->GetFunction());
|
||||
@ -2250,12 +2252,85 @@ void CipherBase::InitIv(const FunctionCallbackInfo<Value>& args) {
|
||||
}
|
||||
|
||||
|
||||
bool CipherBase::IsAuthenticatedMode() const {
|
||||
// check if this cipher operates in an AEAD mode that we support.
|
||||
if (!cipher_)
|
||||
return false;
|
||||
int mode = EVP_CIPHER_mode(cipher_);
|
||||
return mode == EVP_CIPH_GCM_MODE;
|
||||
}
|
||||
|
||||
|
||||
bool CipherBase::GetAuthTag(char** out, unsigned int* out_len) const {
|
||||
// only callable after Final and if encrypting.
|
||||
if (initialised_ || kind_ != kCipher || !auth_tag_)
|
||||
return false;
|
||||
*out_len = auth_tag_len_;
|
||||
*out = new char[auth_tag_len_];
|
||||
memcpy(*out, auth_tag_, auth_tag_len_);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void CipherBase::GetAuthTag(const FunctionCallbackInfo<Value>& args) {
|
||||
Environment* env = Environment::GetCurrent(args.GetIsolate());
|
||||
HandleScope handle_scope(args.GetIsolate());
|
||||
CipherBase* cipher = Unwrap<CipherBase>(args.This());
|
||||
|
||||
char* out = NULL;
|
||||
unsigned int out_len = 0;
|
||||
|
||||
if (cipher->GetAuthTag(&out, &out_len)) {
|
||||
Local<Object> buf = Buffer::Use(env, out, out_len);
|
||||
args.GetReturnValue().Set(buf);
|
||||
} else {
|
||||
ThrowError("Attempting to get auth tag in unsupported state");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool CipherBase::SetAuthTag(const char* data, unsigned int len) {
|
||||
if (!initialised_ || !IsAuthenticatedMode() || kind_ != kDecipher)
|
||||
return false;
|
||||
delete[] auth_tag_;
|
||||
auth_tag_len_ = len;
|
||||
auth_tag_ = new char[len];
|
||||
memcpy(auth_tag_, data, len);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void CipherBase::SetAuthTag(const FunctionCallbackInfo<Value>& args) {
|
||||
HandleScope handle_scope(args.GetIsolate());
|
||||
|
||||
Local<Object> buf = args[0].As<Object>();
|
||||
if (!buf->IsObject() || !Buffer::HasInstance(buf))
|
||||
return ThrowTypeError("Argument must be a Buffer");
|
||||
|
||||
CipherBase* cipher = Unwrap<CipherBase>(args.This());
|
||||
|
||||
if (!cipher->SetAuthTag(Buffer::Data(buf), Buffer::Length(buf)))
|
||||
ThrowError("Attempting to set auth tag in unsupported state");
|
||||
}
|
||||
|
||||
|
||||
bool CipherBase::Update(const char* data,
|
||||
int len,
|
||||
unsigned char** out,
|
||||
int* out_len) {
|
||||
if (!initialised_)
|
||||
return 0;
|
||||
|
||||
// on first update:
|
||||
if (kind_ == kDecipher && IsAuthenticatedMode() && auth_tag_ != NULL) {
|
||||
EVP_CIPHER_CTX_ctrl(&ctx_,
|
||||
EVP_CTRL_GCM_SET_TAG,
|
||||
auth_tag_len_,
|
||||
reinterpret_cast<unsigned char*>(auth_tag_));
|
||||
delete[] auth_tag_;
|
||||
auth_tag_ = NULL;
|
||||
}
|
||||
|
||||
*out_len = len + EVP_CIPHER_CTX_block_size(&ctx_);
|
||||
*out = new unsigned char[*out_len];
|
||||
return EVP_CipherUpdate(&ctx_,
|
||||
@ -2328,6 +2403,21 @@ bool CipherBase::Final(unsigned char** out, int *out_len) {
|
||||
|
||||
*out = new unsigned char[EVP_CIPHER_CTX_block_size(&ctx_)];
|
||||
bool r = EVP_CipherFinal_ex(&ctx_, *out, out_len);
|
||||
|
||||
if (r && kind_ == kCipher) {
|
||||
delete[] auth_tag_;
|
||||
auth_tag_ = NULL;
|
||||
if (IsAuthenticatedMode()) {
|
||||
auth_tag_len_ = EVP_GCM_TLS_TAG_LEN; // use default tag length
|
||||
auth_tag_ = new char[auth_tag_len_];
|
||||
memset(auth_tag_, 0, auth_tag_len_);
|
||||
EVP_CIPHER_CTX_ctrl(&ctx_,
|
||||
EVP_CTRL_GCM_GET_TAG,
|
||||
auth_tag_len_,
|
||||
reinterpret_cast<unsigned char*>(auth_tag_));
|
||||
}
|
||||
}
|
||||
|
||||
EVP_CIPHER_CTX_cleanup(&ctx_);
|
||||
initialised_ = false;
|
||||
|
||||
|
@ -318,6 +318,7 @@ class CipherBase : public BaseObject {
|
||||
~CipherBase() {
|
||||
if (!initialised_)
|
||||
return;
|
||||
delete[] auth_tag_;
|
||||
EVP_CIPHER_CTX_cleanup(&ctx_);
|
||||
}
|
||||
|
||||
@ -339,6 +340,10 @@ class CipherBase : public BaseObject {
|
||||
bool Final(unsigned char** out, int *out_len);
|
||||
bool SetAutoPadding(bool auto_padding);
|
||||
|
||||
bool IsAuthenticatedMode() const;
|
||||
bool GetAuthTag(char** out, unsigned int* out_len) const;
|
||||
bool SetAuthTag(const char* data, unsigned int len);
|
||||
|
||||
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
static void Init(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
static void InitIv(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
@ -346,13 +351,18 @@ class CipherBase : public BaseObject {
|
||||
static void Final(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
static void SetAutoPadding(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
|
||||
static void GetAuthTag(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
static void SetAuthTag(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
|
||||
CipherBase(Environment* env,
|
||||
v8::Local<v8::Object> wrap,
|
||||
CipherKind kind)
|
||||
: BaseObject(env, wrap),
|
||||
cipher_(NULL),
|
||||
initialised_(false),
|
||||
kind_(kind) {
|
||||
kind_(kind),
|
||||
auth_tag_(NULL),
|
||||
auth_tag_len_(0) {
|
||||
MakeWeak<CipherBase>(this);
|
||||
}
|
||||
|
||||
@ -361,6 +371,8 @@ class CipherBase : public BaseObject {
|
||||
const EVP_CIPHER* cipher_; /* coverity[member_decl] */
|
||||
bool initialised_;
|
||||
CipherKind kind_;
|
||||
char* auth_tag_;
|
||||
unsigned int auth_tag_len_;
|
||||
};
|
||||
|
||||
class Hmac : public BaseObject {
|
||||
|
130
test/simple/test-crypto-authenticated.js
Normal file
130
test/simple/test-crypto-authenticated.js
Normal file
@ -0,0 +1,130 @@
|
||||
// Copyright Joyent, Inc. and other Node contributors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
// persons to whom the Software is furnished to do so, subject to the
|
||||
// following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included
|
||||
// in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
// USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
|
||||
|
||||
var common = require('../common');
|
||||
var assert = require('assert');
|
||||
|
||||
try {
|
||||
var crypto = require('crypto');
|
||||
} catch (e) {
|
||||
console.log('Not compiled with OPENSSL support.');
|
||||
process.exit();
|
||||
}
|
||||
|
||||
crypto.DEFAULT_ENCODING = 'buffer';
|
||||
|
||||
//
|
||||
// Test authenticated encryption modes.
|
||||
//
|
||||
// !NEVER USE STATIC IVs IN REAL LIFE!
|
||||
//
|
||||
|
||||
var TEST_CASES = [
|
||||
{ algo: 'aes-128-gcm', key: 'ipxp9a6i1Mb4USb4', iv: 'X6sIq117H0vR',
|
||||
plain: 'Hello World!', ct: '4BE13896F64DFA2C2D0F2C76',
|
||||
tag: '272B422F62EB545EAA15B5FF84092447', tampered: false },
|
||||
{ algo: 'aes-128-gcm', key: 'ipxp9a6i1Mb4USb4', iv: 'X6sIq117H0vR',
|
||||
plain: 'Hello World!', ct: '4BE13596F64DFA2C2D0FAC76',
|
||||
tag: '272B422F62EB545EAA15B5FF84092447', tampered: true },
|
||||
{ algo: 'aes-256-gcm', key: '3zTvzr3p67VC61jmV54rIYu1545x4TlY',
|
||||
iv: '60iP0h6vJoEa', plain: 'Hello node.js world!',
|
||||
ct: '58E62CFE7B1D274111A82267EBB93866E72B6C2A',
|
||||
tag: '9BB44F663BADABACAE9720881FB1EC7A', tampered: false },
|
||||
{ algo: 'aes-256-gcm', key: '3zTvzr3p67VC61jmV54rIYu1545x4TlY',
|
||||
iv: '60iP0h6vJoEa', plain: 'Hello node.js world!',
|
||||
ct: '58E62CFF7B1D274011A82267EBB93866E72B6C2B',
|
||||
tag: '9BB44F663BADABACAE9720881FB1EC7A', tampered: true },
|
||||
];
|
||||
|
||||
var ciphers = crypto.getCiphers();
|
||||
|
||||
for (var i in TEST_CASES) {
|
||||
var test = TEST_CASES[i];
|
||||
|
||||
if (ciphers.indexOf(test.algo) == -1) {
|
||||
console.log('skipping unsupported ' + test.algo + ' test');
|
||||
continue;
|
||||
}
|
||||
|
||||
(function() {
|
||||
var encrypt = crypto.createCipheriv(test.algo, test.key, test.iv);
|
||||
var hex = encrypt.update(test.plain, 'ascii', 'hex');
|
||||
hex += encrypt.final('hex');
|
||||
var auth_tag = encrypt.getAuthTag();
|
||||
// only test basic encryption run if output is marked as tampered.
|
||||
if (!test.tampered) {
|
||||
assert.equal(hex.toUpperCase(), test.ct);
|
||||
assert.equal(auth_tag.toString('hex').toUpperCase(), test.tag);
|
||||
}
|
||||
})();
|
||||
|
||||
(function() {
|
||||
var decrypt = crypto.createDecipheriv(test.algo, test.key, test.iv);
|
||||
decrypt.setAuthTag(new Buffer(test.tag, 'hex'));
|
||||
var msg = decrypt.update(test.ct, 'hex', 'ascii');
|
||||
if (!test.tampered) {
|
||||
msg += decrypt.final('ascii');
|
||||
assert.equal(msg, test.plain);
|
||||
} else {
|
||||
// assert that final throws if input data could not be verified!
|
||||
assert.throws(function() { decrypt.final('ascii'); });
|
||||
}
|
||||
})();
|
||||
|
||||
// after normal operation, test some incorrect ways of calling the API:
|
||||
// it's most certainly enough to run these tests with one algorithm only.
|
||||
|
||||
if (i > 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
(function() {
|
||||
// non-authenticating mode:
|
||||
var encrypt = crypto.createCipheriv('aes-128-cbc',
|
||||
'ipxp9a6i1Mb4USb4', '6fKjEjR3Vl30EUYC');
|
||||
encrypt.update('blah', 'ascii');
|
||||
encrypt.final();
|
||||
assert.throws(function() { encrypt.getAuthTag(); });
|
||||
})();
|
||||
|
||||
(function() {
|
||||
// trying to get tag before inputting all data:
|
||||
var encrypt = crypto.createCipheriv(test.algo, test.key, test.iv);
|
||||
encrypt.update('blah', 'ascii');
|
||||
assert.throws(function() { encrypt.getAuthTag(); });
|
||||
})();
|
||||
|
||||
(function() {
|
||||
// trying to set tag on encryption object:
|
||||
var encrypt = crypto.createCipheriv(test.algo, test.key, test.iv);
|
||||
assert.throws(function() {
|
||||
encrypt.setAuthTag(new Buffer(test.tag, 'hex')); });
|
||||
})();
|
||||
|
||||
(function() {
|
||||
// trying to read tag from decryption object:
|
||||
var decrypt = crypto.createDecipheriv(test.algo, test.key, test.iv);
|
||||
assert.throws(function() { decrypt.getAuthTag(); });
|
||||
})();
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user