Skip to content

Commit ba96c2d

Browse files
committed
feat: make lib Web Crypto compliant
BREAKING CHANGE: Requires Node@>=17.4.0 BREAKING CHANGE: `sign` & `unsign` are now async BREAKING CHANGE: `sign` & `unsign`'s `secret` can only be a string
1 parent 3bb0628 commit ba96c2d

File tree

4 files changed

+93
-62
lines changed

4 files changed

+93
-62
lines changed

Readme.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
## Example
77

88
```js
9-
var cookie = require('cookie-signature');
9+
const cookie = require('cookie-signature');
1010

11-
var val = cookie.sign('hello', 'tobiiscool');
11+
const val = await cookie.sign('hello', 'tobiiscool');
1212
val.should.equal('hello.DGDUkGlIkCzPz+C0B064FNgHdEjox7ch8tOBGslZ5QI');
1313

14-
var val = cookie.sign('hello', 'tobiiscool');
15-
cookie.unsign(val, 'tobiiscool').should.equal('hello');
16-
cookie.unsign(val, 'luna').should.be.false;
14+
const val = await cookie.sign('hello', 'tobiiscool');
15+
(await cookie.unsign(val, 'tobiiscool')).should.equal('hello');
16+
(await cookie.unsign(val, 'luna')).should.be.false;
1717
```
1818

1919
## License

index.js

Lines changed: 70 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,91 @@
11
/**
22
* Module dependencies.
33
*/
4+
const crypto = require('node:crypto');
45

5-
var crypto = require('crypto');
6+
const encoder = new TextEncoder();
67

78
/**
89
* Sign the given `val` with `secret`.
910
*
10-
* @param {String} val
11-
* @param {String|NodeJS.ArrayBufferView|crypto.KeyObject} secret
11+
* @param {String} value
12+
* @param {String} secret
1213
* @return {String}
13-
* @api private
14+
* @api public
1415
*/
16+
exports.sign = async (value, secret) => {
17+
if (typeof value !== "string") {
18+
throw new TypeError("Cookie value must be provided as a string.");
19+
}
20+
if (typeof secret !== "string") {
21+
throw new TypeError("Secret key must be provided as a string.");
22+
}
23+
24+
const data = encoder.encode(value);
25+
const key = await createKey(secret, ["sign"]);
26+
const signature = await crypto.webcrypto.subtle.sign("HMAC", key, data);
27+
const hash = btoa(String.fromCharCode(...new Uint8Array(signature))).replace(
28+
/=+$/,
29+
""
30+
);
1531

16-
exports.sign = function(val, secret){
17-
if ('string' != typeof val) throw new TypeError("Cookie value must be provided as a string.");
18-
if (null == secret) throw new TypeError("Secret key must be provided.");
19-
return val + '.' + crypto
20-
.createHmac('sha256', secret)
21-
.update(val)
22-
.digest('base64')
23-
.replace(/\=+$/, '');
32+
return `${value}.${hash}`;
2433
};
2534

2635
/**
2736
* Unsign and decode the given `input` with `secret`,
2837
* returning `false` if the signature is invalid.
2938
*
30-
* @param {String} input
31-
* @param {String|NodeJS.ArrayBufferView|crypto.KeyObject} secret
32-
* @return {String|Boolean}
39+
* @param {String} cookie
40+
* @param {String} secret
41+
* @return {Promise<String|false>}
42+
* @api public
43+
*/
44+
exports.unsign = async (cookie, secret) => {
45+
if (typeof cookie !== "string") {
46+
throw new TypeError("Signed cookie string must be provided.");
47+
}
48+
if (typeof secret !== "string") {
49+
throw new TypeError("Secret key must be provided.");
50+
}
51+
52+
const value = cookie.slice(0, cookie.lastIndexOf("."));
53+
const hash = cookie.slice(cookie.lastIndexOf(".") + 1);
54+
55+
const data = encoder.encode(value);
56+
const key = await createKey(secret, ["verify"]);
57+
const signature = byteStringToUint8Array(atob(hash));
58+
const valid = await crypto.webcrypto.subtle.verify("HMAC", key, signature, data);
59+
60+
return valid ? value : false;
61+
};
62+
63+
/**
64+
* @param {String} secret
65+
* @param {ReadonlyArray<KeyUsage>} usages
66+
* @return {Promise<CryptoKey>}
3367
* @api private
3468
*/
69+
const createKey = async (secret, usages) =>
70+
crypto.webcrypto.subtle.importKey(
71+
"raw",
72+
encoder.encode(secret),
73+
{ name: "HMAC", hash: "SHA-256" },
74+
false,
75+
usages
76+
);
77+
78+
/**
79+
* @param {String} byteString
80+
* @return {Uint8Array}
81+
* @api private
82+
*/
83+
const byteStringToUint8Array = (byteString) => {
84+
const array = new Uint8Array(byteString.length);
85+
86+
for (let i = 0; i < byteString.length; i++) {
87+
array[i] = byteString.charCodeAt(i);
88+
}
3589

36-
exports.unsign = function(input, secret){
37-
if ('string' != typeof input) throw new TypeError("Signed cookie string must be provided.");
38-
if (null == secret) throw new TypeError("Secret key must be provided.");
39-
var tentativeValue = input.slice(0, input.lastIndexOf('.')),
40-
expectedInput = exports.sign(tentativeValue, secret),
41-
expectedBuffer = Buffer.from(expectedInput),
42-
inputBuffer = Buffer.from(input);
43-
return (
44-
expectedBuffer.length === inputBuffer.length &&
45-
crypto.timingSafeEqual(expectedBuffer, inputBuffer)
46-
) ? tentativeValue : false;
90+
return array;
4791
};

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
"license": "MIT",
88
"repository": {
99
"type": "git",
10-
"url": "https://github.com/visionmedia/node-cookie-signature.git"
10+
"url": "https://github.com/tj/node-cookie-signature"
1111
},
1212
"dependencies": {},
1313
"engines": {
14-
"node": ">=6.6.0"
14+
"node": ">=17.4.0"
1515
},
1616
"devDependencies": {
1717
"mocha": "*",

test/index.js

Lines changed: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,45 +2,32 @@
22
* Module dependencies.
33
*/
44

5-
var cookie = require('..');
5+
const cookie = require('..');
66

77
describe('.sign(val, secret)', function(){
8-
it('should sign the cookie', function(){
9-
var val = cookie.sign('hello', 'tobiiscool');
8+
it('should sign the cookie', async function(){
9+
let val = await cookie.sign('hello', 'tobiiscool');
1010
val.should.equal('hello.DGDUkGlIkCzPz+C0B064FNgHdEjox7ch8tOBGslZ5QI');
1111

12-
var val = cookie.sign('hello', 'luna');
12+
val = await cookie.sign('hello', 'luna');
1313
val.should.not.equal('hello.DGDUkGlIkCzPz+C0B064FNgHdEjox7ch8tOBGslZ5QI');
1414
})
15-
it('should accept appropriately non-string secrets', function(){
16-
var key = Buffer.from("A0ABBC0C", 'hex'),
17-
val = cookie.sign('hello', key);
18-
val.should.equal('hello.hIvljrKw5oOZtHHSq5u+MlL27cgnPKX77y7F+x5r1to');
19-
(function () {
20-
cookie.sign('unsupported', new Date());
21-
}).should.throw();
22-
})
2315
})
2416

2517
describe('.unsign(val, secret)', function(){
26-
it('should unsign the cookie', function(){
27-
var val = cookie.sign('hello', 'tobiiscool');
28-
cookie.unsign(val, 'tobiiscool').should.equal('hello');
29-
cookie.unsign(val, 'luna').should.be.false();
18+
it('should unsign the cookie', async function(){
19+
const val = await cookie.sign('hello', 'tobiiscool');
20+
(await cookie.unsign(val, 'tobiiscool')).should.equal('hello');
21+
(await cookie.unsign(val, 'luna')).should.be.false();
3022
})
31-
it('should reject malformed cookies', function(){
32-
var pwd = 'actual sekrit password';
33-
cookie.unsign('fake unsigned data', pwd).should.be.false();
23+
it('should reject malformed cookies', async function(){
24+
const pwd = 'actual sekrit password';
25+
(await cookie.unsign('fake unsigned data', pwd)).should.be.false();
3426

35-
var val = cookie.sign('real data', pwd);
36-
cookie.unsign('garbage'+val, pwd).should.be.false();
37-
cookie.unsign('garbage.'+val, pwd).should.be.false();
38-
cookie.unsign(val+'.garbage', pwd).should.be.false();
39-
cookie.unsign(val+'garbage', pwd).should.be.false();
40-
})
41-
it('should accept non-string secrets', function(){
42-
var key = Uint8Array.from([0xA0, 0xAB, 0xBC, 0x0C]),
43-
val = cookie.unsign('hello.hIvljrKw5oOZtHHSq5u+MlL27cgnPKX77y7F+x5r1to', key);
44-
val.should.equal('hello');
27+
const val = await cookie.sign('real data', pwd);
28+
(await cookie.unsign('garbage'+val, pwd)).should.be.false();
29+
(await cookie.unsign('garbage.'+val, pwd)).should.be.false();
30+
(await cookie.unsign(val+'.garbage', pwd)).should.be.false();
31+
(await cookie.unsign(val+'garbage', pwd)).should.be.false();
4532
})
4633
})

0 commit comments

Comments
 (0)