Skip to content

feat: make lib Web Crypto compliant #50

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
## Example

```js
var cookie = require('cookie-signature');
const cookie = require('cookie-signature');

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

var val = cookie.sign('hello', 'tobiiscool');
cookie.unsign(val, 'tobiiscool').should.equal('hello');
cookie.unsign(val, 'luna').should.be.false;
const val = await cookie.sign('hello', 'tobiiscool');
(await cookie.unsign(val, 'tobiiscool')).should.equal('hello');
(await cookie.unsign(val, 'luna')).should.be.false;
```

## License
Expand Down
98 changes: 71 additions & 27 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,91 @@
/**
* Module dependencies.
*/
const crypto = require('node:crypto');

var crypto = require('crypto');
const encoder = new TextEncoder();

/**
* Sign the given `val` with `secret`.
*
* @param {String} val
* @param {String|NodeJS.ArrayBufferView|crypto.KeyObject} secret
* @return {String}
* @api private
* @param {String} value
* @param {String} secret
* @return {Promise<String>}
* @api public
*/
exports.sign = async (value, secret) => {
if (typeof value !== "string") {
throw new TypeError("Cookie value must be provided as a string.");
}
if (typeof secret !== "string") {
throw new TypeError("Secret key must be provided as a string.");
}

const data = encoder.encode(value);
const key = await createKey(secret, ["sign"]);
const signature = await crypto.webcrypto.subtle.sign("HMAC", key, data);
const hash = btoa(String.fromCharCode(...new Uint8Array(signature))).replace(
/=+$/,
""
);

exports.sign = function(val, secret){
if ('string' != typeof val) throw new TypeError("Cookie value must be provided as a string.");
if (null == secret) throw new TypeError("Secret key must be provided.");
return val + '.' + crypto
.createHmac('sha256', secret)
.update(val)
.digest('base64')
.replace(/\=+$/, '');
return `${value}.${hash}`;
};

/**
* Unsign and decode the given `input` with `secret`,
* returning `false` if the signature is invalid.
*
* @param {String} input
* @param {String|NodeJS.ArrayBufferView|crypto.KeyObject} secret
* @return {String|Boolean}
* @param {String} cookie
* @param {String} secret
* @return {Promise<String|false>}
* @api public
*/
exports.unsign = async (cookie, secret) => {
if (typeof cookie !== "string") {
throw new TypeError("Signed cookie string must be provided.");
}
if (typeof secret !== "string") {
throw new TypeError("Secret key must be provided.");
}

const value = cookie.slice(0, cookie.lastIndexOf("."));
const hash = cookie.slice(cookie.lastIndexOf(".") + 1);

const data = encoder.encode(value);
const key = await createKey(secret, ["verify"]);
const signature = byteStringToUint8Array(atob(hash));
const valid = await crypto.webcrypto.subtle.verify("HMAC", key, signature, data);

return valid ? value : false;
};

/**
* @param {String} secret
* @param {ReadonlyArray<KeyUsage>} usages
* @return {Promise<CryptoKey>}
* @api private
*/
const createKey = async (secret, usages) =>
crypto.webcrypto.subtle.importKey(
"raw",
encoder.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
usages
);

/**
* @param {String} byteString
* @return {Uint8Array}
* @api private
*/
const byteStringToUint8Array = (byteString) => {
const array = new Uint8Array(byteString.length);

for (let i = 0; i < byteString.length; i++) {
array[i] = byteString.charCodeAt(i);
}

exports.unsign = function(input, secret){
if ('string' != typeof input) throw new TypeError("Signed cookie string must be provided.");
if (null == secret) throw new TypeError("Secret key must be provided.");
var tentativeValue = input.slice(0, input.lastIndexOf('.')),
expectedInput = exports.sign(tentativeValue, secret),
expectedBuffer = Buffer.from(expectedInput),
inputBuffer = Buffer.from(input);
return (
expectedBuffer.length === inputBuffer.length &&
crypto.timingSafeEqual(expectedBuffer, inputBuffer)
) ? tentativeValue : false;
return array;
};
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/visionmedia/node-cookie-signature.git"
"url": "https://github.com/tj/node-cookie-signature"
},
"dependencies": {},
"engines": {
"node": ">=6.6.0"
"node": ">=17.4.0"
},
"devDependencies": {
"mocha": "*",
Expand Down
45 changes: 16 additions & 29 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,32 @@
* Module dependencies.
*/

var cookie = require('..');
const cookie = require('..');

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

var val = cookie.sign('hello', 'luna');
val = await cookie.sign('hello', 'luna');
val.should.not.equal('hello.DGDUkGlIkCzPz+C0B064FNgHdEjox7ch8tOBGslZ5QI');
})
it('should accept appropriately non-string secrets', function(){
var key = Buffer.from("A0ABBC0C", 'hex'),
val = cookie.sign('hello', key);
val.should.equal('hello.hIvljrKw5oOZtHHSq5u+MlL27cgnPKX77y7F+x5r1to');
(function () {
cookie.sign('unsupported', new Date());
}).should.throw();
})
})

describe('.unsign(val, secret)', function(){
it('should unsign the cookie', function(){
var val = cookie.sign('hello', 'tobiiscool');
cookie.unsign(val, 'tobiiscool').should.equal('hello');
cookie.unsign(val, 'luna').should.be.false();
it('should unsign the cookie', async function(){
const val = await cookie.sign('hello', 'tobiiscool');
(await cookie.unsign(val, 'tobiiscool')).should.equal('hello');
(await cookie.unsign(val, 'luna')).should.be.false();
})
it('should reject malformed cookies', function(){
var pwd = 'actual sekrit password';
cookie.unsign('fake unsigned data', pwd).should.be.false();
it('should reject malformed cookies', async function(){
const pwd = 'actual sekrit password';
(await cookie.unsign('fake unsigned data', pwd)).should.be.false();

var val = cookie.sign('real data', pwd);
cookie.unsign('garbage'+val, pwd).should.be.false();
cookie.unsign('garbage.'+val, pwd).should.be.false();
cookie.unsign(val+'.garbage', pwd).should.be.false();
cookie.unsign(val+'garbage', pwd).should.be.false();
})
it('should accept non-string secrets', function(){
var key = Uint8Array.from([0xA0, 0xAB, 0xBC, 0x0C]),
val = cookie.unsign('hello.hIvljrKw5oOZtHHSq5u+MlL27cgnPKX77y7F+x5r1to', key);
val.should.equal('hello');
const val = await cookie.sign('real data', pwd);
(await cookie.unsign('garbage'+val, pwd)).should.be.false();
(await cookie.unsign('garbage.'+val, pwd)).should.be.false();
(await cookie.unsign(val+'.garbage', pwd)).should.be.false();
(await cookie.unsign(val+'garbage', pwd)).should.be.false();
})
})