From 96f52adbf79f3817e3e099217f9e308fbc654613 Mon Sep 17 00:00:00 2001 From: Ryan Day Date: Fri, 7 Apr 2017 11:38:31 -0400 Subject: [PATCH] feat: adding ssri.create for a crypto style interface (#2) * feat: adding ssri.create for a crypto style interface adding crypto.create adding toJSON to Integrity * chore: standard * fix: update nits and fix documentation for create. create was misdocumented as returning Integrity but it return a Hash --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ index.js | 41 +++++++++++++++++++++++++++++++++++++++++ test/create.js | 24 ++++++++++++++++++++++++ test/integrity.js | 15 +++++++++++++++ 4 files changed, 122 insertions(+) create mode 100644 test/create.js diff --git a/README.md b/README.md index e3024f9..f2fc035 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,14 @@ Integrity](https://w3c.github.io/webappsec/specs/subresourceintegrity/) hashes. * [`stringify`](#stringify) * [`Integrity#concat`](#integrity-concat) * [`Integrity#toString`](#integrity-to-string) + * [`Integrity#toJSON`](#integrity-to-json) * [`Integrity#pickAlgorithm`](#integrity-pick-algorithm) * [`Integrity#hexDigest`](#integrity-hex-digest) * Integrity Generation * [`fromHex`](#from-hex) * [`fromData`](#from-data) * [`fromStream`](#from-stream) + * [`create`](#create) * Integrity Verification * [`checkData`](#check-data) * [`checkStream`](#check-stream) @@ -200,6 +202,22 @@ const integrity = 'sha512-9KhgCRIx/AmzC8xqYJTZRrnO8OW2Pxyl2DIMZSBOr0oDvtEFyht3xp ssri.parse(integrity).toString() === integrity ``` +#### `> Integrity#toJSON() -> String` + +Returns the string representation of an `Integrity` object. All hash entries +will be concatenated in the string by `' '`. + +This is a convenience method so you can pass an `Integrity` object directly to `JSON.stringify`. +For more info check out [toJSON() behavior on mdn](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON%28%29_behavior). + +##### Example + +```javascript +const integrity = '"sha512-9KhgCRIx/AmzC8xqYJTZRrnO8OW2Pxyl2DIMZSBOr0oDvtEFyht3xpp71j/r/pAe1DM+JI/A+line3jUBgzQ7A==?foo"' + +JSON.stringify(ssri.parse(integrity)) === integrity +``` + #### `> Integrity#pickAlgorithm([opts]) -> String` Returns the "best" algorithm from those available in the integrity object. @@ -312,6 +330,30 @@ ssri.fromStream(fs.createReadStream('index.js'), { }) // succeeds ``` +#### `> ssri.create([opts]) -> ` + +Returns a Hash object with `update([,enc])` and `digest()` methods. + + +The Hash object provides the same methods as [crypto class Hash](https://nodejs.org/dist/latest-v6.x/docs/api/crypto.html#crypto_class_hash). +`digest()` accepts no arguments and returns an Integrity object calculated by reading data from +calls to update. + +It accepts both `opts.algorithms` and `opts.options`, which are documented as +part of [`ssri.fromData`](#from-data). + +If `opts.strict` is true, the integrity object will be created using strict +parsing rules. See [`ssri.parse`](#parse). + +##### Example + +```javascript +const integrity = ssri.create().update('foobarbaz').digest() +integrity.toString() +// -> +// sha512-yzd8ELD1piyANiWnmdnpCL5F52f10UfUdEkHywVZeqTt0ymgrxR63Qz0GB7TKPoeeZQmWCaz7T1+9vBnypkYWg== +``` + #### `> ssri.checkData(data, sri, [opts]) -> Hash|false` Verifies `data` integrity against an `sri` argument. `data` may be either a diff --git a/index.js b/index.js index 8a3d3f3..8e88057 100644 --- a/index.js +++ b/index.js @@ -33,6 +33,9 @@ class Hash { hexDigest () { return this.digest && bufFrom(this.digest, 'base64').toString('hex') } + toJSON () { + return this.toString() + } toString (opts) { if (opts && opts.strict) { // Strict mode enforces the standard as close to the foot of the @@ -63,6 +66,9 @@ class Hash { class Integrity { get isIntegrity () { return true } + toJSON () { + return this.toString() + } toString (opts) { opts = opts || {} let sep = opts.sep || ' ' @@ -275,6 +281,41 @@ function integrityStream (opts) { return stream } +module.exports.create = createIntegrity +function createIntegrity (opts) { + opts = opts || {} + const algorithms = opts.algorithms || ['sha512'] + const optString = opts.options && opts.options.length + ? `?${opts.options.join('?')}` + : '' + + const hashes = algorithms.map(crypto.createHash) + + return { + update: function (chunk, enc) { + hashes.forEach(h => h.update(chunk, enc)) + return this + }, + digest: function (enc) { + const integrity = algorithms.reduce((acc, algo) => { + const digest = hashes.shift().digest('base64') + const hash = new Hash( + `${algo}-${digest}${optString}`, + opts + ) + if (hash.algorithm && hash.digest) { + const algo = hash.algorithm + if (!acc[algo]) { acc[algo] = [] } + acc[algo].push(hash) + } + return acc + }, new Integrity()) + + return integrity + } + } +} + // This is a Best Effortâ„¢ at a reasonable priority for hash algos const DEFAULT_PRIORITY = [ 'md5', 'whirlpool', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512' diff --git a/test/create.js b/test/create.js new file mode 100644 index 0000000..1c68391 --- /dev/null +++ b/test/create.js @@ -0,0 +1,24 @@ +const test = require('tap').test + +const ssri = require('..') + +test('works just like from', function (t) { + const integrity = ssri.fromData('hi') + const integrityCreate = ssri.create().update('hi').digest() + + t.ok(integrityCreate instanceof integrity.constructor, 'should be same Integrity that fromData returns') + t.equals(integrity + '', integrityCreate + '', 'should be the sam as fromData') + t.end() +}) + +test('can pass options', function (t) { + const integrity = ssri.create({algorithms: ['sha256', 'sha384']}).update('hi').digest() + + t.equals( + integrity + '', + 'sha256-j0NDRmSPa5bfid2pAcUXaxCm2Dlh3TwayItZstwyeqQ= ' + + 'sha384-B5EAbfgShHckT1PQ/c4hDbgfVXV1EOJqzuNcGKa86qKNzbv9bcBBubTcextU439S', + 'should be expected value' + ) + t.end() +}) diff --git a/test/integrity.js b/test/integrity.js index 661a942..526bbfa 100644 --- a/test/integrity.js +++ b/test/integrity.js @@ -24,6 +24,21 @@ test('toString()', t => { t.done() }) +test('toJSON()', t => { + const sri = ssri.parse('sha512-foo sha256-bar!') + t.equal( + sri.toJSON(), + 'sha512-foo sha256-bar!', + 'integrity objects from ssri.parse() can use toJSON()' + ) + t.equal( + sri.sha512[0].toJSON(), + 'sha512-foo', + 'hash objects should toJSON also' + ) + t.done() +}) + test('concat()', t => { const sri = ssri.parse('sha512-foo') t.equal(