@buckless/signed-data
Advanced tools
Comparing version 2.0.0 to 2.1.1
{ | ||
"name": "@buckless/signed-data", | ||
"version": "2.0.0", | ||
"description": "Sign data using HMAC-SHA1", | ||
"version": "2.1.1", | ||
"description": "Sign unsigned numbers using HMAC-SHA1", | ||
"main": "src/index.js", | ||
"repository": "https://github.com/buckless/signed-data.git", | ||
"repository": "https://github.com/buckless/signed-number.git", | ||
"author": "Gabriel Juchault <gabriel.juchault@gmail.com>", | ||
@@ -8,0 +8,0 @@ "license": "MIT", |
@@ -26,3 +26,3 @@ # signed-data | ||
const inst = new SignedData({ | ||
const inst = new SignedData( | ||
'secret', // signature key | ||
@@ -41,3 +41,3 @@ null, // max bytes (defaults to: auto) | ||
] | ||
}) | ||
) | ||
@@ -55,3 +55,3 @@ const cipher = inst.encode(value) | ||
const inst = new SignedData({ | ||
const inst = new SignedData( | ||
'secret', // signature key | ||
@@ -70,3 +70,3 @@ null, // max bytes (defaults to: auto) | ||
] | ||
}) | ||
) | ||
@@ -84,3 +84,3 @@ const cipher = inst.encode(value).toString('hex') | ||
const inst = new SignedData({ | ||
const inst = new SignedData( | ||
'secret', // signature key | ||
@@ -99,3 +99,3 @@ 23, // (3 bytes of data + 20 bytes of signature) | ||
] | ||
}) | ||
) | ||
@@ -115,3 +115,3 @@ const cipher = inst.encode(value) | ||
const inst = new SignedData({ | ||
const inst = new SignedData( | ||
'secret', // signature key | ||
@@ -130,3 +130,3 @@ null, // max bytes (defaults to: auto) | ||
] | ||
}) | ||
) | ||
@@ -146,2 +146,7 @@ const cipher = inst.encode(value) | ||
``` | ||
# Override secret from constructor | ||
key(secret: String) | ||
``` | ||
``` | ||
encode(data: Object) | ||
@@ -148,0 +153,0 @@ ``` |
107
src/index.js
@@ -1,106 +0,5 @@ | ||
const nativeSha1 = require('./nativeSha1') | ||
const timingSafeEqual = require('./timingSafeEqual') | ||
const MIN_HASH_BYTES = 4 | ||
const MAX_HASH_BYTES = 20 | ||
const SignedData = require('./signedData') | ||
class SignedData { | ||
constructor(secret, maxBytes, hashAlgorithm, scheme = []) { | ||
if (typeof hashAlgorithm !== 'function') { | ||
hashAlgorithm = nativeSha1 | ||
} | ||
if (typeof secret !== 'string' || secret.length === 0) { | ||
throw new Error('[signed-data] missing secret') | ||
} | ||
this.scheme = scheme | ||
this.secret = secret | ||
this.hashAlgorithm = hashAlgorithm | ||
this.dataSize = this.scheme.map(p => p.size).reduce((a, b) => a + b, 0) | ||
this.createHmac = require('hmac').bind(null, hashAlgorithm, 64) | ||
this.maxBytes = maxBytes | ||
? Math.min(this.dataSize + MAX_HASH_BYTES, Math.max(maxBytes, this.dataSize + MIN_HASH_BYTES)) | ||
: this.dataSize + MIN_HASH_BYTES | ||
} | ||
hmac() { | ||
return this.createHmac(this.secret) | ||
} | ||
encode(data) { | ||
let value = '' | ||
for (let i = 0; i < this.scheme.length; i++) { | ||
const part = this.scheme[i] | ||
let result = data.hasOwnProperty(part.name) | ||
? part.encode(data[part.name]) | ||
: part.default | ||
if (result.length > part.size * 2) { | ||
throw new Error(`[signed-data] encoder ${part.name} output a too large result`) | ||
} else { | ||
// left-pad | ||
result = ('0'.repeat(part.size * 2) + result).slice(-1 * part.size * 2) | ||
} | ||
value += result | ||
} | ||
return this.sign(value) | ||
} | ||
sign(result) { | ||
const hash = this.hmac().update(result, 'utf8').digest('hex') | ||
return Buffer.from(`${result}${hash}`.slice(0, this.maxBytes * 2), 'hex') | ||
} | ||
decode(raw) { | ||
let value = {} | ||
let startIndex = 0 | ||
if (Buffer.isBuffer(raw)) { | ||
raw = raw.toString('hex') | ||
} | ||
if (raw.length < this.dataSize * 2) { | ||
throw new Error('[signed-data] data not found') | ||
} | ||
if (raw.length < this.maxBytes * 2) { | ||
throw new Error('[signed-data] signature not found') | ||
} | ||
raw = raw.slice(0, this.maxBytes * 2) | ||
if (!this.checkSignature(Buffer.from(raw, 'hex'))) { | ||
throw new Error('[signed-data] signature does not match') | ||
} | ||
for (let i = 0; i < this.scheme.length; i++) { | ||
const part = this.scheme[i] | ||
let result = null | ||
const data = part.decode(raw.slice(startIndex, startIndex + part.size * 2)) | ||
value[part.name] = data | ||
startIndex += part.size * 2 | ||
} | ||
return value | ||
} | ||
checkSignature(raw) { | ||
const data = raw.slice(0, this.dataSize).toString('hex') | ||
const signed = this.sign(data) | ||
return timingSafeEqual(signed, raw) | ||
} | ||
module.exports = { | ||
SignedData | ||
} | ||
module.exports = SignedData |
@@ -6,11 +6,11 @@ // timingSafeEqual | ||
if (!Buffer.isBuffer(a)) { | ||
throw new Error('[signed-data] first argument must be a buffer') | ||
throw new TypeError('[signed-data] first argument must be a buffer') | ||
} | ||
if (!Buffer.isBuffer(b)) { | ||
throw new Error('[signed-data] second argument must be a buffer') | ||
throw new TypeError('[signed-data] second argument must be a buffer') | ||
} | ||
if (a.length !== b.length) { | ||
throw new Error('[signed-data] input buffers must have the same length') | ||
throw new TypeError('[signed-data] input buffers must have the same length') | ||
} | ||
@@ -17,0 +17,0 @@ |
const test = require('tape') | ||
const Rusha = require('rusha') | ||
const mock = require('./mock') | ||
const mockData = require('./mock.data') | ||
@@ -10,10 +10,11 @@ test('Custom hash method using rusha', (t) => { | ||
credit: 1500, | ||
bools: { food: false, drink1: true, drink2: false } | ||
catering: { food: false, drink1: true, drink2: false } | ||
} | ||
const inst = mock('secret', 12, Rusha.createHash) | ||
const inst = mockData('mysecret', 12, Rusha.createHash) | ||
const cipher = inst.encode(value) | ||
const decipher = inst.decode(cipher) | ||
t.deepEqual(decipher, value, 'decipher = value') | ||
t.deepEqual(decipher, value, 'value = decipher') | ||
}) |
const test = require('tape') | ||
const mock = require('./mock') | ||
const mockData = require('./mock.data') | ||
@@ -7,68 +7,29 @@ test('Failing key', (t) => { | ||
const value = { | ||
const inst = mockData('mysecret') | ||
const otherInst = mockData('othersecret') | ||
const cipher = inst.encode({ | ||
credit: 1500, | ||
bools: { food: false, drink1: true, drink2: false } | ||
} | ||
catering: { food: false, drink1: false, drink2: false } | ||
}) | ||
const inst = mock('secret') | ||
const otherInst = mock('othersecret') | ||
const cipher = inst.encode(value) | ||
try { | ||
otherInst.decode(cipher) | ||
} catch (e) { | ||
t.equal(e.message, '[signed-data] signature does not match', 'error') | ||
t.equal(e.message, '[signed-data] signature does not match') | ||
} | ||
}) | ||
test('No Data', (t) => { | ||
test('Failing signature', (t) => { | ||
t.plan(1) | ||
const value = { | ||
credit: 1500, | ||
bools: { food: false, drink1: true, drink2: false } | ||
} | ||
const inst = mock('secret') | ||
const cipher = inst.encode(value).slice(0, 2) | ||
try { | ||
inst.decode(cipher) | ||
} catch (e) { | ||
t.equal(e.message, '[signed-data] data not found', 'error') | ||
} | ||
}) | ||
test('No signature', (t) => { | ||
t.plan(1) | ||
const value = { | ||
credit: 1500, | ||
bools: { food: false, drink1: true, drink2: false } | ||
} | ||
const inst = mock('secret') | ||
const cipher = inst.encode(value).slice(0, inst.dataSize) | ||
try { | ||
inst.decode(cipher) | ||
} catch (e) { | ||
t.equal(e.message, '[signed-data] signature not found', 'error') | ||
} | ||
}) | ||
test('Modified signature', (t) => { | ||
t.plan(1) | ||
const value = { | ||
credit: 1500, | ||
bools: { food: false, drink1: true, drink2: false } | ||
} | ||
const inst = mock('secret') | ||
const cipher = inst | ||
.encode(value) | ||
const inst = mockData('mysecret') | ||
let cipher = inst | ||
.encode({ | ||
credit: 1500, | ||
catering: { food: false, drink1: false, drink2: false } | ||
}) | ||
.toString('hex') | ||
.replace(/.$/, 'f') // 0005dc024be23567 -> 0005dc024be2356f | ||
// replace last char (n) with (n + 1) to ensure the signature fails | ||
cipher = cipher.replace(/.$/, ((parseInt(cipher[cipher.length - 1], 16) + 1) % 16).toString(16)) | ||
@@ -78,4 +39,4 @@ try { | ||
} catch (e) { | ||
t.equal(e.message, '[signed-data] signature does not match', 'error') | ||
t.equal(e.message, '[signed-data] signature does not match') | ||
} | ||
}) |
const test = require('tape') | ||
const mock = require('./mock') | ||
const mockData = require('./mock.data') | ||
test('Missing secret (empty string)', (t) => { | ||
t.plan(1) | ||
test('Missing or invalid secret', (t) => { | ||
t.plan(4) | ||
const value = { | ||
credit: 123131313123, | ||
bools: { food: false, drink1: false, drink2: false } | ||
try { | ||
const inst = mockData() | ||
inst.encode(1500) | ||
} catch (e) { | ||
t.equal(e.message, '[signed-data] Missing argument secret, or secret is not a string', 'throw error for a missing key') | ||
} | ||
try { | ||
mock('') | ||
const inst = mockData(null) | ||
inst.encode(1500) | ||
} catch (e) { | ||
t.equal(e.message, '[signed-data] missing secret', 'error') | ||
t.equal(e.message, '[signed-data] Missing argument secret, or secret is not a string', 'throw error for a missing key') | ||
} | ||
}) | ||
test('Missing secret (non-string)', (t) => { | ||
t.plan(1) | ||
const value = { | ||
credit: 123131313123, | ||
bools: { food: false, drink1: false, drink2: false } | ||
} | ||
try { | ||
mock(0) | ||
const inst = mockData(123) | ||
inst.encode(1500) | ||
} catch (e) { | ||
t.equal(e.message, '[signed-data] missing secret', 'error') | ||
t.equal(e.message, '[signed-data] Missing argument secret, or secret is not a string', 'throw error for a missing key') | ||
} | ||
}) | ||
test('Missing secret (undefined)', (t) => { | ||
t.plan(1) | ||
const value = { | ||
credit: 123131313123, | ||
bools: { food: false, drink1: false, drink2: false } | ||
} | ||
try { | ||
mock() | ||
const inst = mockData('') | ||
inst.encode(1500) | ||
} catch (e) { | ||
t.equal(e.message, '[signed-data] missing secret', 'error') | ||
t.equal(e.message, '[signed-data] Missing argument secret, or secret is not a string', 'throw error for a missing key') | ||
} | ||
}) |
const test = require('tape') | ||
const mock = require('./mock') | ||
const mockData = require('./mock.data') | ||
@@ -8,12 +8,13 @@ test('Usage', (t) => { | ||
const value = { | ||
credit: 1500, | ||
bools: { food: false, drink1: true, drink2: false } | ||
credit: 0, | ||
catering: { food: false, drink1: true, drink2: true } | ||
} | ||
const inst = mock('secret') | ||
const inst = mockData('mysecret') | ||
const cipher = inst.encode(value) | ||
const decipher = inst.decode(cipher) | ||
t.equal(8, cipher.length, 'length = 8') | ||
t.deepEqual(decipher, value, 'decipher = value') | ||
t.equal(8, cipher.length, 'cipher length === 8') | ||
t.deepEqual(decipher, value, 'decipher === value') | ||
}) | ||
@@ -25,12 +26,13 @@ | ||
const value = { | ||
credit: 1500, | ||
bools: { food: false, drink1: true, drink2: false } | ||
credit: 0, | ||
catering: { food: false, drink1: true, drink2: true } | ||
} | ||
const inst = mock('secret') | ||
const inst = mockData('mysecret') | ||
const cipher = inst.encode(value).toString('hex') | ||
const decipher = inst.decode(cipher) | ||
t.equal(8, cipher.length / 2, 'length = 8') | ||
t.deepEqual(decipher, value, 'decipher = value') | ||
t.equal(8, cipher.length / 2, 'cipher length === 8') | ||
t.deepEqual(decipher, value, 'decipher === value') | ||
}) | ||
@@ -42,27 +44,29 @@ | ||
const value = { | ||
credit: 1500, | ||
bools: { food: false, drink1: true, drink2: false } | ||
credit: 0, | ||
catering: { food: false, drink1: true, drink2: true } | ||
} | ||
const inst = mock('secret', 20) | ||
const inst = mockData('mysecret', 20) | ||
const cipher = inst.encode(value) | ||
const decipher = inst.decode(cipher) | ||
t.equal(20, cipher.length, 'length = 20') | ||
t.deepEqual(decipher, value, 'decipher = value') | ||
t.equal(20, cipher.length, 'cipher length === 20') | ||
t.deepEqual(decipher, value, 'decipher === value') | ||
}) | ||
test('Usage — With random data at the end', (t) => { | ||
test('Usage — More signature', (t) => { | ||
t.plan(1) | ||
const value = { | ||
credit: 1500, | ||
bools: { food: false, drink1: true, drink2: false } | ||
credit: 0, | ||
catering: { food: false, drink1: true, drink2: true } | ||
} | ||
const inst = mock('secret', 20) | ||
const inst = mockData('mysecret', 14) | ||
const cipher = inst.encode(value).toString('hex') + Math.ceil(Math.random() * 1000000) | ||
const decipher = inst.decode(cipher) | ||
t.deepEqual(decipher, value, 'decipher = value') | ||
t.deepEqual(decipher, value, 'decipher === value') | ||
}) |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
15500
16
149
364
1