diff --git a/package.json b/package.json index 560fdb4..e1276aa 100644 --- a/package.json +++ b/package.json @@ -37,18 +37,19 @@ }, "homepage": "https://github.com/libp2p/js-libp2p-record", "devDependencies": { - "aegir": "^22.0.0", + "aegir": "^25.0.0", "chai": "^4.2.0", "dirty-chai": "^2.0.1", + "ipfs-utils": "^2.3.1", "libp2p-crypto": "~0.17.0", + "multibase": "^2.0.0", "peer-id": "~0.13.6" }, "dependencies": { - "buffer": "^5.6.0", "err-code": "^2.0.0", - "multihashes": "~0.4.15", - "multihashing-async": "^0.8.0", - "protons": "^1.0.1" + "multihashes": "^1.0.1", + "multihashing-async": "^1.0.0", + "protons": "^1.2.1" }, "contributors": [ "Vasco Santos ", diff --git a/src/record.js b/src/record.js index 0db6451..f80f3ff 100644 --- a/src/record.js +++ b/src/record.js @@ -1,23 +1,22 @@ 'use strict' const protons = require('protons') -const { Buffer } = require('buffer') const pb = protons(require('./record.proto')).Record const utils = require('./utils') class Record { /** - * @param {Buffer} [key] - * @param {Buffer} [value] + * @param {Uint8Array} [key] + * @param {Uint8Array} [value] * @param {Date} [recvtime] */ constructor (key, value, recvtime) { - if (key && !Buffer.isBuffer(key)) { - throw new Error('key must be a Buffer') + if (!(key instanceof Uint8Array)) { + throw new Error('key must be a Uint8Array') } - if (value && !Buffer.isBuffer(value)) { - throw new Error('value must be a buffer') + if (!(value instanceof Uint8Array)) { + throw new Error('value must be a Uint8Array') } this.key = key @@ -26,7 +25,7 @@ class Record { } /** - * @returns {Buffer} + * @returns {Uint8Array} */ serialize () { return pb.encode(this.prepareSerialize()) @@ -48,7 +47,7 @@ class Record { /** * Decode a protobuf encoded record. * - * @param {Buffer} raw + * @param {Uint8Array} raw * @returns {Record} */ static deserialize (raw) { diff --git a/src/selection.js b/src/selection.js index e3423b8..26da61d 100644 --- a/src/selection.js +++ b/src/selection.js @@ -1,12 +1,14 @@ 'use strict' const errcode = require('err-code') +const { utf8Decoder } = require('./utils') + /** * Select the best record out of the given records. * * @param {Object} selectors - * @param {Buffer} k - * @param {Array} records + * @param {Uint8Array} k + * @param {Array} records * @returns {number} - The index of the best record. */ const bestRecord = (selectors, k, records) => { @@ -16,7 +18,8 @@ const bestRecord = (selectors, k, records) => { throw errcode(new Error(errMsg), 'ERR_NO_RECORDS_RECEIVED') } - const parts = k.toString().split('/') + const kStr = utf8Decoder.decode(k) + const parts = kStr.split('/') if (parts.length < 3) { const errMsg = 'Record key does not have a selector function' diff --git a/src/selectors/public-key.js b/src/selectors/public-key.js index 1be1353..db0790b 100644 --- a/src/selectors/public-key.js +++ b/src/selectors/public-key.js @@ -5,8 +5,8 @@ * Simply returns the first record, as all valid public key * records are equal. * - * @param {Buffer} k - * @param {Array} records + * @param {Uint8Array} k + * @param {Array} records * @returns {number} */ const publicKeySelector = (k, records) => { diff --git a/src/utils.js b/src/utils.js index 73490fc..746ca76 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,5 +1,10 @@ 'use strict' +const TextDecoder = require('ipfs-utils/src/text-decoder') +const utf8Decoder = new TextDecoder('utf8') + +module.exports.utf8Decoder = utf8Decoder + /** * Convert a JavaScript date into an `RFC3339Nano` formatted * string. @@ -52,3 +57,21 @@ module.exports.parseRFC3339 = (time) => { return new Date(Date.UTC(year, month, date, hour, minute, second, millisecond)) } + +module.exports.uint8ArraysEqual = (arr1, arr2) => { + if (arr1 === arr2) { + return true + } + + if (arr1.byteLength !== arr2.byteLength) { + return false + } + + for (let i = 0; i < arr1.byteLength; i++) { + if (arr1[i] !== arr2[i]) { + return false + } + } + + return true +} diff --git a/src/validator.js b/src/validator.js index 1723964..86092ac 100644 --- a/src/validator.js +++ b/src/validator.js @@ -1,6 +1,7 @@ 'use strict' const errcode = require('err-code') +const { utf8Decoder } = require('./utils') /** * Checks a record and ensures it is still valid. * It runs the needed validators. @@ -12,7 +13,8 @@ const errcode = require('err-code') */ const verifyRecord = (validators, record) => { const key = record.key - const parts = key.toString().split('/') + const keyString = utf8Decoder.decode(key) + const parts = keyString.split('/') if (parts.length < 3) { // No validator available diff --git a/src/validators/public-key.js b/src/validators/public-key.js index 0799e8f..e546553 100644 --- a/src/validators/public-key.js +++ b/src/validators/public-key.js @@ -2,27 +2,28 @@ const multihashing = require('multihashing-async') const errcode = require('err-code') -const { Buffer } = require('buffer') +const { utf8Decoder, uint8ArraysEqual } = require('../utils') + /** - * Validator for publick key records. + * Validator for public key records. * Verifies that the passed in record value is the PublicKey * that matches the passed in key. * If validation fails the returned Promise will reject with the error. * - * @param {Buffer} key - A valid key is of the form `'/pk/'` - * @param {Buffer} publicKey - The public key to validate against (protobuf encoded). + * @param {Uint8Array} key - A valid key is of the form `'/pk/'` + * @param {Uint8Array} publicKey - The public key to validate against (protobuf encoded). * @returns {Promise} */ const validatePublicKeyRecord = async (key, publicKey) => { - if (!Buffer.isBuffer(key)) { - throw errcode(new Error('"key" must be a Buffer'), 'ERR_INVALID_RECORD_KEY_NOT_BUFFER') + if (!(key instanceof Uint8Array)) { + throw errcode(new Error('"key" must be a Uint8Array'), 'ERR_INVALID_RECORD_KEY_NOT_BUFFER') } - if (key.length < 5) { + if (key.byteLength < 5) { throw errcode(new Error('invalid public key record'), 'ERR_INVALID_RECORD_KEY_TOO_SHORT') } - const prefix = key.slice(0, 4).toString() + const prefix = utf8Decoder.decode(key.subarray(0, 4)) if (prefix !== '/pk/') { throw errcode(new Error('key was not prefixed with /pk/'), 'ERR_INVALID_RECORD_KEY_BAD_PREFIX') @@ -32,7 +33,7 @@ const validatePublicKeyRecord = async (key, publicKey) => { const publicKeyHash = await multihashing(publicKey, 'sha2-256') - if (!keyhash.equals(publicKeyHash)) { + if (!uint8ArraysEqual(keyhash, publicKeyHash)) { throw errcode(new Error('public key does not match passed in key'), 'ERR_INVALID_RECORD_HASH_MISMATCH') } } diff --git a/test/fixtures/go-key-records.js b/test/fixtures/go-key-records.js index c483bab..cca9bcf 100644 --- a/test/fixtures/go-key-records.js +++ b/test/fixtures/go-key-records.js @@ -1,8 +1,9 @@ 'use strict' -const { Buffer } = require('buffer') + +const multibase = require('multibase') + module.exports = { - publicKey: Buffer.from( - 'CAASXjBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQDjXAQQMal4SB2tSnX6NJIPmC69/BT8A8jc7/gDUZNkEhdhYHvc7k7S4vntV/c92nJGxNdop9fKJyevuNMuXhhHAgMBAAE=', - 'base64' + publicKey: multibase.decode( + 'MCAASXjBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQDjXAQQMal4SB2tSnX6NJIPmC69/BT8A8jc7/gDUZNkEhdhYHvc7k7S4vntV/c92nJGxNdop9fKJyevuNMuXhhHAgMBAAE=' ) } diff --git a/test/fixtures/go-record.js b/test/fixtures/go-record.js index 188b6c1..ebcc0de 100644 --- a/test/fixtures/go-record.js +++ b/test/fixtures/go-record.js @@ -1,5 +1,5 @@ 'use strict' -const { Buffer } = require('buffer') +const multibase = require('multibase') // Fixtures generated using gore (https://github.com/motemen/gore) // // :import github.com/libp2p/go-libp2p-record @@ -18,12 +18,10 @@ const { Buffer } = require('buffer') // ioutil.WriteFile("js-libp2p-record/test/fixtures/record.bin", enc, 0644) // ioutil.WriteFile("js-libp2p-record/test/fixtures/record-signed.bin", enc2, 0644) module.exports = { - serialized: Buffer.from( - '0a0568656c6c6f1205776f726c641a2212201bd5175b1d4123ee29665348c60ea5cf5ac62e2e05215b97a7b9a9b0cf71d116', - 'hex' + serialized: multibase.decode( + 'f0a0568656c6c6f1205776f726c641a2212201bd5175b1d4123ee29665348c60ea5cf5ac62e2e05215b97a7b9a9b0cf71d116' ), - serializedSigned: Buffer.from( - '0a0568656c6c6f1205776f726c641a2212201bd5175b1d4123ee29665348c60ea5cf5ac62e2e05215b97a7b9a9b0cf71d116228001500fe7505698b8a873ccde6f1d36a2be662d57807490d9a9959540f2645a454bf615215092e10123f6ffc4ed694711bfbb1d5ccb62f3da83cf4528ee577a96b6cf0272eef9a920bd56459993690060353b72c22b8c03ad2a33894522dac338905b201179a85cb5e2fc68ed58be96cf89beec6dc0913887dddc10f202a2a1b117', - 'hex' + serializedSigned: multibase.decode( + 'f0a0568656c6c6f1205776f726c641a2212201bd5175b1d4123ee29665348c60ea5cf5ac62e2e05215b97a7b9a9b0cf71d116228001500fe7505698b8a873ccde6f1d36a2be662d57807490d9a9959540f2645a454bf615215092e10123f6ffc4ed694711bfbb1d5ccb62f3da83cf4528ee577a96b6cf0272eef9a920bd56459993690060353b72c22b8c03ad2a33894522dac338905b201179a85cb5e2fc68ed58be96cf89beec6dc0913887dddc10f202a2a1b117' ) } diff --git a/test/record.spec.js b/test/record.spec.js index a8aa060..1f1851f 100644 --- a/test/record.spec.js +++ b/test/record.spec.js @@ -4,7 +4,7 @@ const chai = require('chai') chai.use(require('dirty-chai')) const expect = chai.expect -const { Buffer } = require('buffer') +const { utf8TextEncoder } = require('./utils') const libp2pRecord = require('../src') const Record = libp2pRecord.Record @@ -15,28 +15,28 @@ const date = new Date(Date.UTC(2012, 1, 25, 10, 10, 10, 10)) describe('record', () => { it('new', () => { const rec = new Record( - Buffer.from('hello'), - Buffer.from('world') + utf8TextEncoder.encode('hello'), + utf8TextEncoder.encode('world') ) - expect(rec).to.have.property('key').eql(Buffer.from('hello')) - expect(rec).to.have.property('value').eql(Buffer.from('world')) + expect(rec).to.have.property('key').eql(utf8TextEncoder.encode('hello')) + expect(rec).to.have.property('value').eql(utf8TextEncoder.encode('world')) }) it('serialize & deserialize', () => { - const rec = new Record(Buffer.from('hello'), Buffer.from('world'), date) + const rec = new Record(utf8TextEncoder.encode('hello'), utf8TextEncoder.encode('world'), date) const dec = Record.deserialize(rec.serialize()) - expect(dec).to.have.property('key').eql(Buffer.from('hello')) - expect(dec).to.have.property('value').eql(Buffer.from('world')) + expect(dec).to.have.property('key').eql(utf8TextEncoder.encode('hello')) + expect(dec).to.have.property('value').eql(utf8TextEncoder.encode('world')) expect(dec.timeReceived).to.be.eql(date) }) describe('go interop', () => { it('no signature', () => { const dec = Record.deserialize(fixture.serialized) - expect(dec).to.have.property('key').eql(Buffer.from('hello')) - expect(dec).to.have.property('value').eql(Buffer.from('world')) + expect(dec).to.have.property('key').eql(utf8TextEncoder.encode('hello')) + expect(dec).to.have.property('value').eql(utf8TextEncoder.encode('world')) }) }) }) diff --git a/test/selection.spec.js b/test/selection.spec.js index b90adf0..7fa90e2 100644 --- a/test/selection.spec.js +++ b/test/selection.spec.js @@ -3,17 +3,17 @@ 'use strict' var expect = require('chai').expect -const { Buffer } = require('buffer') +const { utf8TextEncoder } = require('./utils') const libp2pRecord = require('../src') const selection = libp2pRecord.selection -const records = [Buffer.alloc(0), Buffer.from('hello')] +const records = [new Uint8Array(), utf8TextEncoder.encode('hello')] describe('selection', () => { describe('bestRecord', () => { it('throws no records given when no records received', () => { expect( - () => selection.bestRecord({}, Buffer.from('/'), []) + () => selection.bestRecord({}, utf8TextEncoder.encode('/'), []) ).to.throw( /No records given/ ) @@ -21,7 +21,7 @@ describe('selection', () => { it('throws on missing selector in the record key', () => { expect( - () => selection.bestRecord({}, Buffer.from('/'), records) + () => selection.bestRecord({}, utf8TextEncoder.encode('/'), records) ).to.throw( /Record key does not have a selector function/ ) @@ -29,7 +29,7 @@ describe('selection', () => { it('throws on unknown key prefix', () => { expect( - () => selection.bestRecord({ world () {} }, Buffer.from('/hello/'), records) + () => selection.bestRecord({ world () {} }, utf8TextEncoder.encode('/hello/'), records) ).to.throw( /Unrecognized key prefix: hello/ ) @@ -38,7 +38,7 @@ describe('selection', () => { it('returns the index from the matching selector', () => { const selectors = { hello (k, recs) { - expect(k).to.be.eql(Buffer.from('/hello/world')) + expect(k).to.be.eql(utf8TextEncoder.encode('/hello/world')) expect(recs).to.be.eql(records) return 1 @@ -46,7 +46,7 @@ describe('selection', () => { } expect( - selection.bestRecord(selectors, Buffer.from('/hello/world'), records) + selection.bestRecord(selectors, utf8TextEncoder.encode('/hello/world'), records) ).to.equal( 1 ) @@ -56,7 +56,7 @@ describe('selection', () => { describe('selectors', () => { it('public key', () => { expect( - selection.selectors.pk(Buffer.from('/hello/world'), records) + selection.selectors.pk(utf8TextEncoder.encode('/hello/world'), records) ).to.equal( 0 ) diff --git a/test/utils.js b/test/utils.js new file mode 100644 index 0000000..d14e461 --- /dev/null +++ b/test/utils.js @@ -0,0 +1,6 @@ +'use strict' + +const TextEncoder = require('ipfs-utils/src/text-encoder') +const utf8TextEncoder = new TextEncoder('utf8') + +module.exports.utf8TextEncoder = utf8TextEncoder diff --git a/test/validator.spec.js b/test/validator.spec.js index 7e34da7..13a8991 100644 --- a/test/validator.spec.js +++ b/test/validator.spec.js @@ -7,7 +7,7 @@ chai.use(require('dirty-chai')) const expect = chai.expect const crypto = require('libp2p-crypto') const PeerId = require('peer-id') -const { Buffer } = require('buffer') +const { utf8TextEncoder } = require('./utils') const libp2pRecord = require('../src') const validator = libp2pRecord.validator const Record = libp2pRecord.Record @@ -18,21 +18,20 @@ const generateCases = (hash) => { return { valid: { publicKey: [ - Buffer.concat([ - Buffer.from('/pk/'), - hash - ]) + Uint8Array.of( + ...utf8TextEncoder.encode('/pk/'), + ...hash + ) ] }, invalid: { publicKey: [ // missing hashkey - [Buffer.from('/pk/'), 'ERR_INVALID_RECORD_KEY_TOO_SHORT'], + [utf8TextEncoder.encode('/pk/'), 'ERR_INVALID_RECORD_KEY_TOO_SHORT'], // not the hash of a key - [Buffer.concat([ - Buffer.from('/pk/'), - Buffer.from('random') - ]), 'ERR_INVALID_RECORD_HASH_MISMATCH'], + [Uint8Array.of(...utf8TextEncoder.encode('/pk/'), + ...utf8TextEncoder.encode('random') + ), 'ERR_INVALID_RECORD_HASH_MISMATCH'], // missing prefix [hash, 'ERR_INVALID_RECORD_KEY_BAD_PREFIX'], // not a buffer @@ -55,14 +54,14 @@ describe('validator', () => { describe('verifyRecord', () => { it('calls matching validator', () => { - const k = Buffer.from('/hello/you') - const rec = new Record(k, Buffer.from('world'), new PeerId(hash)) + const k = utf8TextEncoder.encode('/hello/you') + const rec = new Record(k, utf8TextEncoder.encode('world'), new PeerId(hash)) const validators = { hello: { func (key, value) { expect(key).to.eql(k) - expect(value).to.eql(Buffer.from('world')) + expect(value).to.eql(utf8TextEncoder.encode('world')) }, sign: false } @@ -71,14 +70,14 @@ describe('validator', () => { }) it('calls not matching any validator', () => { - const k = Buffer.from('/hallo/you') - const rec = new Record(k, Buffer.from('world'), new PeerId(hash)) + const k = utf8TextEncoder.encode('/hallo/you') + const rec = new Record(k, utf8TextEncoder.encode('world'), new PeerId(hash)) const validators = { hello: { func (key, value) { expect(key).to.eql(k) - expect(value).to.eql(Buffer.from('world')) + expect(value).to.eql(utf8TextEncoder.encode('world')) }, sign: false } @@ -129,7 +128,7 @@ describe('validator', () => { const pubKey = crypto.keys.unmarshalPublicKey(fixture.publicKey) const hash = await pubKey.hash() - const k = Buffer.concat([Buffer.from('/pk/'), hash]) + const k = Uint8Array.of(...utf8TextEncoder.encode('/pk/'), ...hash) return validator.validators.pk.func(k, pubKey.bytes) }) })