Skip to content

Commit e0d31ea

Browse files
KiNgMaRindutny
authored andcommitted
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.
1 parent f9f9239 commit e0d31ea

File tree

5 files changed

+260
-1
lines changed

5 files changed

+260
-1
lines changed

doc/api/crypto.markdown

+16
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,13 @@ multiple of the cipher's block size or `final` will fail. Useful for
218218
non-standard padding, e.g. using `0x0` instead of PKCS padding. You
219219
must call this before `cipher.final`.
220220

221+
### cipher.getAuthTag()
222+
223+
For authenticated encryption modes (currently supported: GCM), this
224+
method returns a `Buffer` that represents the _authentication tag_ that
225+
has been computed from the given data. Should be called after
226+
encryption has been completed using the `final` method!
227+
221228

222229
## crypto.createDecipher(algorithm, password)
223230

@@ -268,6 +275,15 @@ removing it. Can only work if the input data's length is a multiple of
268275
the ciphers block size. You must call this before streaming data to
269276
`decipher.update`.
270277

278+
### decipher.setAuthTag(buffer)
279+
280+
For authenticated encryption modes (currently supported: GCM), this
281+
method must be used to pass in the received _authentication tag_.
282+
If no tag is provided or if the ciphertext has been tampered with,
283+
`final` will throw, thus indicating that the ciphertext should
284+
be discarded due to failed authentication.
285+
286+
271287
## crypto.createSign(algorithm)
272288

273289
Creates and returns a signing object, with the given algorithm. On

lib/crypto.js

+11
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,15 @@ Cipheriv.prototype.update = Cipher.prototype.update;
322322
Cipheriv.prototype.final = Cipher.prototype.final;
323323
Cipheriv.prototype.setAutoPadding = Cipher.prototype.setAutoPadding;
324324

325+
Cipheriv.prototype.getAuthTag = function() {
326+
return this._binding.getAuthTag();
327+
};
328+
329+
330+
Cipheriv.prototype.setAuthTag = function(tagbuf) {
331+
this._binding.setAuthTag(tagbuf);
332+
};
333+
325334

326335

327336
exports.createDecipher = exports.Decipher = Decipher;
@@ -367,6 +376,8 @@ Decipheriv.prototype.update = Cipher.prototype.update;
367376
Decipheriv.prototype.final = Cipher.prototype.final;
368377
Decipheriv.prototype.finaltol = Cipher.prototype.final;
369378
Decipheriv.prototype.setAutoPadding = Cipher.prototype.setAutoPadding;
379+
Decipheriv.prototype.getAuthTag = Cipheriv.prototype.getAuthTag;
380+
Decipheriv.prototype.setAuthTag = Cipheriv.prototype.setAuthTag;
370381

371382

372383

src/node_crypto.cc

+90
Original file line numberDiff line numberDiff line change
@@ -2122,6 +2122,8 @@ void CipherBase::Initialize(Environment* env, Handle<Object> target) {
21222122
NODE_SET_PROTOTYPE_METHOD(t, "update", Update);
21232123
NODE_SET_PROTOTYPE_METHOD(t, "final", Final);
21242124
NODE_SET_PROTOTYPE_METHOD(t, "setAutoPadding", SetAutoPadding);
2125+
NODE_SET_PROTOTYPE_METHOD(t, "getAuthTag", GetAuthTag);
2126+
NODE_SET_PROTOTYPE_METHOD(t, "setAuthTag", SetAuthTag);
21252127

21262128
target->Set(FIXED_ONE_BYTE_STRING(node_isolate, "CipherBase"),
21272129
t->GetFunction());
@@ -2250,12 +2252,85 @@ void CipherBase::InitIv(const FunctionCallbackInfo<Value>& args) {
22502252
}
22512253

22522254

2255+
bool CipherBase::IsAuthenticatedMode() const {
2256+
// check if this cipher operates in an AEAD mode that we support.
2257+
if (!cipher_)
2258+
return false;
2259+
int mode = EVP_CIPHER_mode(cipher_);
2260+
return mode == EVP_CIPH_GCM_MODE;
2261+
}
2262+
2263+
2264+
bool CipherBase::GetAuthTag(char** out, unsigned int* out_len) const {
2265+
// only callable after Final and if encrypting.
2266+
if (initialised_ || kind_ != kCipher || !auth_tag_)
2267+
return false;
2268+
*out_len = auth_tag_len_;
2269+
*out = new char[auth_tag_len_];
2270+
memcpy(*out, auth_tag_, auth_tag_len_);
2271+
return true;
2272+
}
2273+
2274+
2275+
void CipherBase::GetAuthTag(const FunctionCallbackInfo<Value>& args) {
2276+
Environment* env = Environment::GetCurrent(args.GetIsolate());
2277+
HandleScope handle_scope(args.GetIsolate());
2278+
CipherBase* cipher = Unwrap<CipherBase>(args.This());
2279+
2280+
char* out = NULL;
2281+
unsigned int out_len = 0;
2282+
2283+
if (cipher->GetAuthTag(&out, &out_len)) {
2284+
Local<Object> buf = Buffer::Use(env, out, out_len);
2285+
args.GetReturnValue().Set(buf);
2286+
} else {
2287+
ThrowError("Attempting to get auth tag in unsupported state");
2288+
}
2289+
}
2290+
2291+
2292+
bool CipherBase::SetAuthTag(const char* data, unsigned int len) {
2293+
if (!initialised_ || !IsAuthenticatedMode() || kind_ != kDecipher)
2294+
return false;
2295+
delete[] auth_tag_;
2296+
auth_tag_len_ = len;
2297+
auth_tag_ = new char[len];
2298+
memcpy(auth_tag_, data, len);
2299+
return true;
2300+
}
2301+
2302+
2303+
void CipherBase::SetAuthTag(const FunctionCallbackInfo<Value>& args) {
2304+
HandleScope handle_scope(args.GetIsolate());
2305+
2306+
Local<Object> buf = args[0].As<Object>();
2307+
if (!buf->IsObject() || !Buffer::HasInstance(buf))
2308+
return ThrowTypeError("Argument must be a Buffer");
2309+
2310+
CipherBase* cipher = Unwrap<CipherBase>(args.This());
2311+
2312+
if (!cipher->SetAuthTag(Buffer::Data(buf), Buffer::Length(buf)))
2313+
ThrowError("Attempting to set auth tag in unsupported state");
2314+
}
2315+
2316+
22532317
bool CipherBase::Update(const char* data,
22542318
int len,
22552319
unsigned char** out,
22562320
int* out_len) {
22572321
if (!initialised_)
22582322
return 0;
2323+
2324+
// on first update:
2325+
if (kind_ == kDecipher && IsAuthenticatedMode() && auth_tag_ != NULL) {
2326+
EVP_CIPHER_CTX_ctrl(&ctx_,
2327+
EVP_CTRL_GCM_SET_TAG,
2328+
auth_tag_len_,
2329+
reinterpret_cast<unsigned char*>(auth_tag_));
2330+
delete[] auth_tag_;
2331+
auth_tag_ = NULL;
2332+
}
2333+
22592334
*out_len = len + EVP_CIPHER_CTX_block_size(&ctx_);
22602335
*out = new unsigned char[*out_len];
22612336
return EVP_CipherUpdate(&ctx_,
@@ -2328,6 +2403,21 @@ bool CipherBase::Final(unsigned char** out, int *out_len) {
23282403

23292404
*out = new unsigned char[EVP_CIPHER_CTX_block_size(&ctx_)];
23302405
bool r = EVP_CipherFinal_ex(&ctx_, *out, out_len);
2406+
2407+
if (r && kind_ == kCipher) {
2408+
delete[] auth_tag_;
2409+
auth_tag_ = NULL;
2410+
if (IsAuthenticatedMode()) {
2411+
auth_tag_len_ = EVP_GCM_TLS_TAG_LEN; // use default tag length
2412+
auth_tag_ = new char[auth_tag_len_];
2413+
memset(auth_tag_, 0, auth_tag_len_);
2414+
EVP_CIPHER_CTX_ctrl(&ctx_,
2415+
EVP_CTRL_GCM_GET_TAG,
2416+
auth_tag_len_,
2417+
reinterpret_cast<unsigned char*>(auth_tag_));
2418+
}
2419+
}
2420+
23312421
EVP_CIPHER_CTX_cleanup(&ctx_);
23322422
initialised_ = false;
23332423

src/node_crypto.h

+13-1
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ class CipherBase : public BaseObject {
318318
~CipherBase() {
319319
if (!initialised_)
320320
return;
321+
delete[] auth_tag_;
321322
EVP_CIPHER_CTX_cleanup(&ctx_);
322323
}
323324

@@ -339,20 +340,29 @@ class CipherBase : public BaseObject {
339340
bool Final(unsigned char** out, int *out_len);
340341
bool SetAutoPadding(bool auto_padding);
341342

343+
bool IsAuthenticatedMode() const;
344+
bool GetAuthTag(char** out, unsigned int* out_len) const;
345+
bool SetAuthTag(const char* data, unsigned int len);
346+
342347
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
343348
static void Init(const v8::FunctionCallbackInfo<v8::Value>& args);
344349
static void InitIv(const v8::FunctionCallbackInfo<v8::Value>& args);
345350
static void Update(const v8::FunctionCallbackInfo<v8::Value>& args);
346351
static void Final(const v8::FunctionCallbackInfo<v8::Value>& args);
347352
static void SetAutoPadding(const v8::FunctionCallbackInfo<v8::Value>& args);
348353

354+
static void GetAuthTag(const v8::FunctionCallbackInfo<v8::Value>& args);
355+
static void SetAuthTag(const v8::FunctionCallbackInfo<v8::Value>& args);
356+
349357
CipherBase(Environment* env,
350358
v8::Local<v8::Object> wrap,
351359
CipherKind kind)
352360
: BaseObject(env, wrap),
353361
cipher_(NULL),
354362
initialised_(false),
355-
kind_(kind) {
363+
kind_(kind),
364+
auth_tag_(NULL),
365+
auth_tag_len_(0) {
356366
MakeWeak<CipherBase>(this);
357367
}
358368

@@ -361,6 +371,8 @@ class CipherBase : public BaseObject {
361371
const EVP_CIPHER* cipher_; /* coverity[member_decl] */
362372
bool initialised_;
363373
CipherKind kind_;
374+
char* auth_tag_;
375+
unsigned int auth_tag_len_;
364376
};
365377

366378
class Hmac : public BaseObject {
+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Copyright Joyent, Inc. and other Node contributors.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a
4+
// copy of this software and associated documentation files (the
5+
// "Software"), to deal in the Software without restriction, including
6+
// without limitation the rights to use, copy, modify, merge, publish,
7+
// distribute, sublicense, and/or sell copies of the Software, and to permit
8+
// persons to whom the Software is furnished to do so, subject to the
9+
// following conditions:
10+
//
11+
// The above copyright notice and this permission notice shall be included
12+
// in all copies or substantial portions of the Software.
13+
//
14+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15+
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
17+
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18+
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19+
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
20+
// USE OR OTHER DEALINGS IN THE SOFTWARE.
21+
22+
23+
24+
25+
var common = require('../common');
26+
var assert = require('assert');
27+
28+
try {
29+
var crypto = require('crypto');
30+
} catch (e) {
31+
console.log('Not compiled with OPENSSL support.');
32+
process.exit();
33+
}
34+
35+
crypto.DEFAULT_ENCODING = 'buffer';
36+
37+
//
38+
// Test authenticated encryption modes.
39+
//
40+
// !NEVER USE STATIC IVs IN REAL LIFE!
41+
//
42+
43+
var TEST_CASES = [
44+
{ algo: 'aes-128-gcm', key: 'ipxp9a6i1Mb4USb4', iv: 'X6sIq117H0vR',
45+
plain: 'Hello World!', ct: '4BE13896F64DFA2C2D0F2C76',
46+
tag: '272B422F62EB545EAA15B5FF84092447', tampered: false },
47+
{ algo: 'aes-128-gcm', key: 'ipxp9a6i1Mb4USb4', iv: 'X6sIq117H0vR',
48+
plain: 'Hello World!', ct: '4BE13596F64DFA2C2D0FAC76',
49+
tag: '272B422F62EB545EAA15B5FF84092447', tampered: true },
50+
{ algo: 'aes-256-gcm', key: '3zTvzr3p67VC61jmV54rIYu1545x4TlY',
51+
iv: '60iP0h6vJoEa', plain: 'Hello node.js world!',
52+
ct: '58E62CFE7B1D274111A82267EBB93866E72B6C2A',
53+
tag: '9BB44F663BADABACAE9720881FB1EC7A', tampered: false },
54+
{ algo: 'aes-256-gcm', key: '3zTvzr3p67VC61jmV54rIYu1545x4TlY',
55+
iv: '60iP0h6vJoEa', plain: 'Hello node.js world!',
56+
ct: '58E62CFF7B1D274011A82267EBB93866E72B6C2B',
57+
tag: '9BB44F663BADABACAE9720881FB1EC7A', tampered: true },
58+
];
59+
60+
var ciphers = crypto.getCiphers();
61+
62+
for (var i in TEST_CASES) {
63+
var test = TEST_CASES[i];
64+
65+
if (ciphers.indexOf(test.algo) == -1) {
66+
console.log('skipping unsupported ' + test.algo + ' test');
67+
continue;
68+
}
69+
70+
(function() {
71+
var encrypt = crypto.createCipheriv(test.algo, test.key, test.iv);
72+
var hex = encrypt.update(test.plain, 'ascii', 'hex');
73+
hex += encrypt.final('hex');
74+
var auth_tag = encrypt.getAuthTag();
75+
// only test basic encryption run if output is marked as tampered.
76+
if (!test.tampered) {
77+
assert.equal(hex.toUpperCase(), test.ct);
78+
assert.equal(auth_tag.toString('hex').toUpperCase(), test.tag);
79+
}
80+
})();
81+
82+
(function() {
83+
var decrypt = crypto.createDecipheriv(test.algo, test.key, test.iv);
84+
decrypt.setAuthTag(new Buffer(test.tag, 'hex'));
85+
var msg = decrypt.update(test.ct, 'hex', 'ascii');
86+
if (!test.tampered) {
87+
msg += decrypt.final('ascii');
88+
assert.equal(msg, test.plain);
89+
} else {
90+
// assert that final throws if input data could not be verified!
91+
assert.throws(function() { decrypt.final('ascii'); });
92+
}
93+
})();
94+
95+
// after normal operation, test some incorrect ways of calling the API:
96+
// it's most certainly enough to run these tests with one algorithm only.
97+
98+
if (i > 0) {
99+
continue;
100+
}
101+
102+
(function() {
103+
// non-authenticating mode:
104+
var encrypt = crypto.createCipheriv('aes-128-cbc',
105+
'ipxp9a6i1Mb4USb4', '6fKjEjR3Vl30EUYC');
106+
encrypt.update('blah', 'ascii');
107+
encrypt.final();
108+
assert.throws(function() { encrypt.getAuthTag(); });
109+
})();
110+
111+
(function() {
112+
// trying to get tag before inputting all data:
113+
var encrypt = crypto.createCipheriv(test.algo, test.key, test.iv);
114+
encrypt.update('blah', 'ascii');
115+
assert.throws(function() { encrypt.getAuthTag(); });
116+
})();
117+
118+
(function() {
119+
// trying to set tag on encryption object:
120+
var encrypt = crypto.createCipheriv(test.algo, test.key, test.iv);
121+
assert.throws(function() {
122+
encrypt.setAuthTag(new Buffer(test.tag, 'hex')); });
123+
})();
124+
125+
(function() {
126+
// trying to read tag from decryption object:
127+
var decrypt = crypto.createDecipheriv(test.algo, test.key, test.iv);
128+
assert.throws(function() { decrypt.getAuthTag(); });
129+
})();
130+
}

0 commit comments

Comments
 (0)