From 2d790594fe155b736de941ee9a4527ea7a8263e1 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Thu, 21 Jun 2018 17:45:36 +0100 Subject: [PATCH 1/4] feat: initial implementation --- .gitignore | 64 ++++++++++++++++ README.md | 129 +++++++++++++++++++++++++++++++ ci/Jenkinsfile | 2 + package.json | 53 +++++++++++++ src/errors.js | 5 ++ src/index.js | 176 +++++++++++++++++++++++++++++++++++++++++++ src/pb/ipns.proto.js | 30 ++++++++ test/index.spec.js | 103 +++++++++++++++++++++++++ 8 files changed, 562 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 ci/Jenkinsfile create mode 100644 package.json create mode 100644 src/errors.js create mode 100644 src/index.js create mode 100644 src/pb/ipns.proto.js create mode 100644 test/index.spec.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2e81726 --- /dev/null +++ b/.gitignore @@ -0,0 +1,64 @@ +dist/ + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# while testing npm5 +package-lock.json +yarn.lock diff --git a/README.md b/README.md new file mode 100644 index 0000000..29d8a68 --- /dev/null +++ b/README.md @@ -0,0 +1,129 @@ +# ipns + +[![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://protocol.ai) +[![](https://img.shields.io/badge/project-IPFS-blue.svg?style=flat-square)](http://ipfs.io/) +[![](https://img.shields.io/badge/freenode-%23ipfs-blue.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23ipfs) +[![standard-readme](https://img.shields.io/badge/standard--readme-OK-green.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme) +[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard) + +> ipns record definitions + +This module contains all the necessary code for creating, understanding and validating IPNS records. + +## Lead Maintainer + +[Vasco Santos](https://github.com/vasco-santos). + +## Table of Contents + +- [Install](#install) +- [Usage](#usage) + - [Create Record](#create-record) + - [Validate Record](#validate-record) + - [Embed public key to record](#embed-public-key-to-record) + - [Extract public key from record](#extract-public-key-from-record) + - [Datastore key](#datastore-key) +- [API](#api) +- [Contribute](#contribute) +- [License](#license) + +### Install + +> npm install ipns + +## Usage + +#### Create record + +```js +const ipns = require('./ipns') + +ipns.create(privateKey, value, seqNumber, eol, (err, entryData) => { + // your code goes here +}); +``` + +#### Validate record + +```js +const ipns = require('./ipns') + +ipns.validate(publicKey, ipnsEntry, (err) => { + // your code goes here + // if no error, the record is valid +}); +``` + +#### Embed public key to record + +> Not available yet + +#### Extract public key from record + +> Not available yet + +#### Datastore key + +```js +const ipns = require('./ipns') + +ipns.getDatastoreKey(peerId); +``` + +Returns a key to be used for storing the ipns entry in the datastore according to the specs, that is: + +``` +/ipns/${base32()} +``` + +## API + +#### Create record + +```js + +ipns.create(privateKey, value, sequenceNumber, eol, callback); +``` + +Create an IPNS record for being stored in a protocol buffer. + +- `privateKey` (`PrivKey` RSA Instance): key to be used for cryptographic operations. +- `value` (string): ipfs path of the object to be published. +- `sequenceNumber` (Number): sequence number of the record. +- `eol` (string): end of life datetime of the record (according to RFC3339). +- `callback` (function): operation result. + +#### Create record + +```js + +ipns.validate(publicKey, ipnsEntry, callback); +``` + +Create an IPNS record for being stored in a protocol buffer. + +- `publicKey` (`PubKey` RSA Instance): key to be used for cryptographic operations. +- `ipnsEntry` (Object): ipns entry record (obtained using the create function). +- `callback` (function): operation result (if no error, validation successful). + +#### Datastore key + +```js +ipns.getDatastoreKey(peerId); +``` + +Get a key for storing the ipns entry in the datastore. + +- `peerId` (`Uint8Array`): peer identifier. + +## Contribute + +Feel free to join in. All welcome. Open an [issue](https://github.com/ipfs/js-ipns/issues)! + +This repository falls under the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). + +[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/contributing.md) + +## License + +[MIT](LICENSE) diff --git a/ci/Jenkinsfile b/ci/Jenkinsfile new file mode 100644 index 0000000..a7da2e5 --- /dev/null +++ b/ci/Jenkinsfile @@ -0,0 +1,2 @@ +// Warning: This file is automatically synced from https://github.com/ipfs/ci-sync so if you want to change it, please change it there and ask someone to sync all repositories. +javascript() diff --git a/package.json b/package.json new file mode 100644 index 0000000..a8eda03 --- /dev/null +++ b/package.json @@ -0,0 +1,53 @@ +{ + "name": "ipns", + "version": "0.1.0", + "description": "ipns record definitions", + "leadMaintainer": "Vasco Santos ", + "main": "src/index.js", + "scripts": { + "build": "aegir build", + "lint": "aegir lint", + "release": "aegir release", + "release-minor": "aegir release --type minor", + "release-major": "aegir release --type major", + "test": "aegir test", + "test:browser": "aegir test -t browser -t webworker", + "test:node": "aegir test -t node" + }, + "pre-push": [ + "lint", + "test" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/ipfs/js-ipns.git" + }, + "keywords": [ + "ipfs", + "ipns" + ], + "author": "Vasco Santos ", + "license": "MIT", + "bugs": { + "url": "https://github.com/ipfs/js-ipns/issues" + }, + "homepage": "https://github.com/ipfs/js-ipns#readme", + "dependencies": { + "base32-encode": "^1.0.0", + "debug": "^3.1.0", + "protons": "^1.0.1" + }, + "devDependencies": { + "aegir": "^13.1.0", + "chai": "^4.1.2", + "chai-string": "^1.4.0", + "dirty-chai": "^2.0.1", + "ipfs": "^0.28.2", + "ipfsd-ctl": "^0.36.0", + "libp2p-crypto": "^0.13.0", + "multihashes": "^0.4.13" + }, + "contributors": [ + "Vasco Santos " + ] +} diff --git a/src/errors.js b/src/errors.js new file mode 100644 index 0000000..0b0675d --- /dev/null +++ b/src/errors.js @@ -0,0 +1,5 @@ +'use strict' + +exports.ERR_IPNS_EXPIRED_RECORD = 'ERR_IPNS_EXPIRED_RECORD' +exports.ERR_UNRECOGNIZED_VALIDITY = 'ERR_UNRECOGNIZED_VALIDITY' +exports.ERR_SIGNATURE_VERIFICATION = 'ERR_SIGNATURE_VERIFICATION' diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..ffc5d14 --- /dev/null +++ b/src/index.js @@ -0,0 +1,176 @@ +'use strict' + +const base32Encode = require('base32-encode') +const debug = require('debug') +const log = debug('jsipns') +log.error = debug('jsipns:error') + +const ipnsEntryProto = require('./pb/ipns.proto') +const ERRORS = require('./errors') + +/** + * Create creates a new ipns entry and signs it with the given private key. + * Note: This function does not embed the public key. If you want to do that, use `EmbedPublicKey`. + * + * @param {Object} privateKey private key for signing the record. + * @param {string} value value to be stored in the record. + * @param {number} seq sequence number of the record. + * @param {string} eol end of life datetime of the record. + * @param {function(Error)} [callback] + * @returns {Promise|void} + */ +const create = (privateKey, value, seq, eol, callback) => { + const validity = eol.toISOString() + const validityType = ipnsEntryProto.ValidityType.EOL + + sign(privateKey, value, validityType, validity, (error, signature) => { + if (error) { + log.error(error) + return callback(error) + } + + const entry = { + value: value, + signature: signature, // TODO confirm format compliance with go-ipfs + validityType: validityType, + validity: validity, + sequence: seq + } + + log(`ipns entry for ${value} created`) + return callback(null, entry) + }) +} + +/** + * Validates the given ipns entry against the given public key. + * + * @param {Object} publicKey public key for validating the record. + * @param {Object} entry ipns entry record. + * @param {function(Error)} [callback] + * @returns {Promise|void} + */ +const validate = (publicKey, entry, callback) => { + const { value, validityType, validity } = entry + const dataForSignature = ipnsEntryDataForSig(value, validityType, validity) + + // Validate Signature + publicKey.verify(dataForSignature, entry.signature, (err, result) => { + if (err) { + log.error('record signature verification failed') + return callback(Object.assign(new Error('record signature verification failed'), { code: ERRORS.ERR_SIGNATURE_VERIFICATION })) + } + + // Validate according to the validity type + if (validityType === ipnsEntryProto.ValidityType.EOL) { + const validityDate = Date.parse(validity.toString()) + + if (validityDate < Date.now()) { + log.error('record has expired') + return callback(Object.assign(new Error('record has expired'), { code: ERRORS.ERR_IPNS_EXPIRED_RECORD })) + } + } else if (validityType) { + log.error('unrecognized validity type') + return callback(Object.assign(new Error('unrecognized validity type'), { code: ERRORS.ERR_UNRECOGNIZED_VALIDITY })) + } + + log(`ipns entry for ${value} is valid`) + return callback(null, null) + }) +} + +/** + * Validates the given ipns entry against the given public key. + * + * @param {Object} publicKey public key for validating the record. + * @param {Object} entry ipns entry record. + * @param {function(Error)} [callback] + * @returns {Promise|void} + */ +const embedPublicKey = (publicKey, entry, callback) => { + return callback(new Error('not implemented yet')) +} + +/** + * Extracts a public key matching `pid` from the ipns record. + * + * @param {Object} peerId peer identifier object. + * @param {Object} entry ipns entry record. + * @param {function(Error)} [callback] + * @returns {Promise|void} + */ +const extractPublicKey = (peerId, entry, callback) => { + return callback(new Error('not implemented yet')) +} + +// rawStdEncoding as go +// TODO Remove once resolved +// Created PR for allowing this inside base32-encode https://github.com/LinusU/base32-encode/issues/2 +const regex = new RegExp('=', 'g') +const rawStdEncoding = (key) => base32Encode(key, 'RFC4648').replace(regex, '') + +/** + * Get key for storing the record in the datastore. + * Format: /ipns/${base32()} + * + * @param {Buffer} key peer identifier object. + * @returns {string} + */ +const getDatastoreKey = (key) => `/ipns/${rawStdEncoding(key)}` + +/** + * Get key for sharing the record in the routing mechanism. + * Format: ${base32(/ipns/)}, ${base32(/pk/)} + * + * @param {Buffer} key peer identifier object. + * @returns {string} + */ +const getIdKeys = (key) => { + const pkBuffer = Buffer.from('/pk/') + const ipnsBuffer = Buffer.from('/ipns/') + + return { + nameKey: rawStdEncoding(Buffer.concat([pkBuffer, key])), + ipnsKey: rawStdEncoding(Buffer.concat([ipnsBuffer, key])) + } +} + +// Sign ipns record data +const sign = (privateKey, value, validityType, validity, callback) => { + const dataForSignature = ipnsEntryDataForSig(value, validityType, validity) + + privateKey.sign(dataForSignature, (err, signature) => { + if (err) { + return callback(err) + } + return callback(null, signature) + }) +} + +// Create record data for being signed +const ipnsEntryDataForSig = (value, validityType, eol) => { + const valueBuffer = Buffer.from(value) + const validityTypeBuffer = Buffer.from(validityType.toString()) + const eolBuffer = Buffer.from(eol) + + return Buffer.concat([valueBuffer, validityTypeBuffer, eolBuffer]) +} + +module.exports = { + // create ipns entry record + create, + // validate ipns entry record + validate, + // embed public key in the record + embedPublicKey, + // extract public key from the record + extractPublicKey, + // get key for datastore + getDatastoreKey, + // get keys for routing + getIdKeys, + // marshal + marshal: ipnsEntryProto.encode, + // unmarshal + unmarshal: ipnsEntryProto.decode +} diff --git a/src/pb/ipns.proto.js b/src/pb/ipns.proto.js new file mode 100644 index 0000000..4d12b43 --- /dev/null +++ b/src/pb/ipns.proto.js @@ -0,0 +1,30 @@ +'use strict' + +const protons = require('protons') + +/* eslint-disable no-tabs */ +const message = ` +message IpnsEntry { + enum ValidityType { + EOL = 0; // setting an EOL says "this record is valid until..." + } + + required bytes value = 1; + required bytes signature = 2; + + optional ValidityType validityType = 3; + optional bytes validity = 4; + + optional uint64 sequence = 5; + + optional uint64 ttl = 6; + + // in order for nodes to properly validate a record upon receipt, they need the public + // key associated with it. For old RSA keys, its easiest if we just send this as part of + // the record itself. For newer ed25519 keys, the public key can be embedded in the + // peerID, making this field unnecessary. + optional bytes pubKey = 7; +} +` + +module.exports = protons(message).IpnsEntry diff --git a/test/index.spec.js b/test/index.spec.js new file mode 100644 index 0000000..e88f824 --- /dev/null +++ b/test/index.spec.js @@ -0,0 +1,103 @@ +/* eslint-env mocha */ +'use strict' + +const chai = require('chai') +const dirtyChai = require('dirty-chai') +const chaiString = require('chai-string') +const expect = chai.expect +chai.use(dirtyChai) +chai.use(chaiString) + +const ipfs = require('ipfs') +const DaemonFactory = require('ipfsd-ctl') +const crypto = require('libp2p-crypto') +const { fromB58String } = require('multihashes') + +const ipns = require('../src') + +const df = DaemonFactory.create({ type: 'proc', exec: ipfs }) + +describe('ipns', function () { + const cid = 'QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq' + + let ipfs = null + let ipfsd = null + let ipfsId = null + let rsa = null + + const spawnDaemon = (cb) => { + df.spawn({ initOptions: { bits: 512 } }, (err, _ipfsd) => { + expect(err).to.not.exist() + ipfsd = _ipfsd + ipfs = ipfsd.api + + ipfs.id((err, id) => { + if (err) { + throw err + } + + ipfsId = id + cb() + }) + }) + } + + before(function (done) { + crypto.keys.generateKeyPair('RSA', 2048, (err, keypair) => { + expect(err).to.not.exist() + rsa = keypair + + spawnDaemon(done) + }) + }) + + after(function (done) { + ipfsd.stop(() => done()) + }) + + it('should create an ipns record correctly', () => { + const sequence = 0 + const eol = new Date(Date.now()) + + ipns.create(rsa, cid, sequence, eol, (err, entry) => { + expect(err).to.not.exist() + expect(entry).to.deep.include({ + value: cid, + sequence: sequence, + validity: eol + }) + expect(entry).to.have.a.property('signature') + expect(entry).to.have.a.property('validityType') + }) + }) + + it('should create an ipns record and validate it correctly', () => { + const sequence = 0 + const eol = new Date(Date.now()) + + ipns.create(rsa, cid, sequence, eol, (err, entry) => { + expect(err).to.not.exist() + + ipns.validate(rsa.public, entry, (err, res) => { + expect(err).to.not.exist() + }) + }) + }) + + it('should get datastore key correctly', () => { + const datastoreKey = ipns.getDatastoreKey(fromB58String(ipfsId.id)) + + expect(datastoreKey).to.exist() + expect(datastoreKey).to.startsWith('/ipns/') + }) + + it('should get id keys correctly', () => { + const idKeys = ipns.getIdKeys(fromB58String(ipfsId.id)) + + expect(idKeys).to.exist() + expect(idKeys).to.have.a.property('nameKey') + expect(idKeys).to.have.a.property('ipnsKey') + expect(idKeys.nameKey).to.not.startsWith('/pk/') + expect(idKeys.ipnsKey).to.not.startsWith('/ipns/') + }) +}) From 3afa02c73398fa60c3b3ce59e72e5238a34c8761 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Thu, 21 Jun 2018 18:20:21 +0100 Subject: [PATCH 2/4] fix: code review --- README.md | 8 ++++---- package.json | 2 +- src/index.js | 2 +- test/index.spec.js | 2 ++ 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 29d8a68..17588da 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ipns +# IPNS [![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://protocol.ai) [![](https://img.shields.io/badge/project-IPFS-blue.svg?style=flat-square)](http://ipfs.io/) @@ -36,7 +36,7 @@ This module contains all the necessary code for creating, understanding and vali #### Create record ```js -const ipns = require('./ipns') +const ipns = require('ipns') ipns.create(privateKey, value, seqNumber, eol, (err, entryData) => { // your code goes here @@ -46,7 +46,7 @@ ipns.create(privateKey, value, seqNumber, eol, (err, entryData) => { #### Validate record ```js -const ipns = require('./ipns') +const ipns = require('ipns') ipns.validate(publicKey, ipnsEntry, (err) => { // your code goes here @@ -65,7 +65,7 @@ ipns.validate(publicKey, ipnsEntry, (err) => { #### Datastore key ```js -const ipns = require('./ipns') +const ipns = require('ipns') ipns.getDatastoreKey(peerId); ``` diff --git a/package.json b/package.json index a8eda03..3f0325d 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "chai": "^4.1.2", "chai-string": "^1.4.0", "dirty-chai": "^2.0.1", - "ipfs": "^0.28.2", + "ipfs": "^0.29.3", "ipfsd-ctl": "^0.36.0", "libp2p-crypto": "^0.13.0", "multihashes": "^0.4.13" diff --git a/src/index.js b/src/index.js index ffc5d14..f8be22f 100644 --- a/src/index.js +++ b/src/index.js @@ -9,7 +9,7 @@ const ipnsEntryProto = require('./pb/ipns.proto') const ERRORS = require('./errors') /** - * Create creates a new ipns entry and signs it with the given private key. + * Creates a new ipns entry and signs it with the given private key. * Note: This function does not embed the public key. If you want to do that, use `EmbedPublicKey`. * * @param {Object} privateKey private key for signing the record. diff --git a/test/index.spec.js b/test/index.spec.js index e88f824..e373c32 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -18,6 +18,8 @@ const ipns = require('../src') const df = DaemonFactory.create({ type: 'proc', exec: ipfs }) describe('ipns', function () { + this.timeout(20 * 1000) + const cid = 'QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq' let ipfs = null From 6295fb614ad714b654743d39e5355f71a2fda71e Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Tue, 26 Jun 2018 17:30:29 +0100 Subject: [PATCH 3/4] added nanoseconds precision to the validity --- LICENSE | 12 +++---- README.md | 85 +++++++++++++++++++++++++++++++++++++++------- package.json | 5 ++- src/errors.js | 2 ++ src/index.js | 68 +++++++++++++++++++++++-------------- src/utils.js | 56 ++++++++++++++++++++++++++++++ test/index.spec.js | 46 +++++++++++++++++++------ 7 files changed, 217 insertions(+), 57 deletions(-) create mode 100644 src/utils.js diff --git a/LICENSE b/LICENSE index e4224df..7d37874 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ -MIT License +The MIT License (MIT) -Copyright (c) 2018 IPFS +Copyright (c) 2018 Protocol Labs, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index 17588da..46a32b4 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ This module contains all the necessary code for creating, understanding and vali ```js const ipns = require('ipns') -ipns.create(privateKey, value, seqNumber, eol, (err, entryData) => { +ipns.create(privateKey, value, sequenceNumber, lifetime, (err, entryData) => { // your code goes here }); ``` @@ -67,44 +67,82 @@ ipns.validate(publicKey, ipnsEntry, (err) => { ```js const ipns = require('ipns') -ipns.getDatastoreKey(peerId); +ipns.getLocalKey(peerId); ``` -Returns a key to be used for storing the ipns entry in the datastore according to the specs, that is: +Returns a key to be used for storing the ipns entry locally, that is: ``` /ipns/${base32()} ``` +#### Marshal data with proto buffer + +```js +const ipns = require('ipns') + +ipns.create(privateKey, value, sequenceNumber, lifetime, (err, entryData) => { + // ... + const marshalledData = ipns.marshal(entryData) + // ... +}); +``` + +Returns the entry data serialized. + +#### Unmarshal data from proto buffer + +```js +const ipns = require('ipns') + +const data = ipns.unmarshal(storedData) +``` + +Returns the entry data structure after being serialized. + ## API #### Create record ```js -ipns.create(privateKey, value, sequenceNumber, eol, callback); +ipns.create(privateKey, value, sequenceNumber, lifetime, [callback]); ``` Create an IPNS record for being stored in a protocol buffer. -- `privateKey` (`PrivKey` RSA Instance): key to be used for cryptographic operations. +- `privateKey` (`PrivKey` [RSA Instance](https://github.com/libp2p/js-libp2p-crypto/blob/master/src/keys/rsa-class.js)): key to be used for cryptographic operations. - `value` (string): ipfs path of the object to be published. -- `sequenceNumber` (Number): sequence number of the record. -- `eol` (string): end of life datetime of the record (according to RFC3339). +- `sequenceNumber` (Number): number representing the current version of the record. +- `lifetime` (string): lifetime of the record (in milliseconds). - `callback` (function): operation result. -#### Create record +`callback` must follow `function (err, ipnsEntry) {}` signature, where `err` is an error if the operation was not successful. `ipnsEntry` is an object that contains the entry's properties, such as: + +```js +{ + value: '/ipfs/QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq', + signature: Buffer, + validityType: 0, + validity: '2018-06-27T14:49:14.074000000Z', + sequence: 2 +} +``` + +#### Validate record ```js -ipns.validate(publicKey, ipnsEntry, callback); +ipns.validate(publicKey, ipnsEntry, [callback]); ``` -Create an IPNS record for being stored in a protocol buffer. +Validate an IPNS record previously stored in a protocol buffer. -- `publicKey` (`PubKey` RSA Instance): key to be used for cryptographic operations. +- `publicKey` (`PubKey` [RSA Instance](https://github.com/libp2p/js-libp2p-crypto/blob/master/src/keys/rsa-class.js)): key to be used for cryptographic operations. - `ipnsEntry` (Object): ipns entry record (obtained using the create function). -- `callback` (function): operation result (if no error, validation successful). +- `callback` (function): operation result. + +`callback` must follow `function (err) {}` signature, where `err` is an error if the operation was not successful. This way, if no error, the validation was successful. #### Datastore key @@ -116,6 +154,27 @@ Get a key for storing the ipns entry in the datastore. - `peerId` (`Uint8Array`): peer identifier. +#### Marshal data with proto buffer + +```js +const marshalledData = ipns.marshal(entryData) +}); +``` + +Returns the entry data serialized. + +- `entryData` (Object): ipns entry record (obtained using the create function). + +#### Unmarshal data from proto buffer + +```js +const data = ipns.unmarshal(storedData) +``` + +Returns the entry data structure after being serialized. + +- `storedData` (Buffer): ipns entry record serialized. + ## Contribute Feel free to join in. All welcome. Open an [issue](https://github.com/ipfs/js-ipns/issues)! @@ -126,4 +185,4 @@ This repository falls under the IPFS [Code of Conduct](https://github.com/ipfs/c ## License -[MIT](LICENSE) +Copyright (c) Protocol Labs, Inc. under the **MIT**. See [MIT](./LICENSE) for details. diff --git a/package.json b/package.json index 3f0325d..217e135 100644 --- a/package.json +++ b/package.json @@ -33,8 +33,11 @@ }, "homepage": "https://github.com/ipfs/js-ipns#readme", "dependencies": { - "base32-encode": "^1.0.0", + "base32-encode": "^1.1.0", + "big.js": "^5.1.2", "debug": "^3.1.0", + "left-pad": "^1.3.0", + "nano-date": "^2.1.0", "protons": "^1.0.1" }, "devDependencies": { diff --git a/src/errors.js b/src/errors.js index 0b0675d..ac8e3b1 100644 --- a/src/errors.js +++ b/src/errors.js @@ -2,4 +2,6 @@ exports.ERR_IPNS_EXPIRED_RECORD = 'ERR_IPNS_EXPIRED_RECORD' exports.ERR_UNRECOGNIZED_VALIDITY = 'ERR_UNRECOGNIZED_VALIDITY' +exports.ERR_SIGNATURE_CREATION = 'ERR_SIGNATURE_CREATION' exports.ERR_SIGNATURE_VERIFICATION = 'ERR_SIGNATURE_VERIFICATION' +exports.ERR_UNRECOGNIZED_FORMAT = 'ERR_UNRECOGNIZED_FORMAT' diff --git a/src/index.js b/src/index.js index f8be22f..f5e3260 100644 --- a/src/index.js +++ b/src/index.js @@ -1,39 +1,51 @@ 'use strict' const base32Encode = require('base32-encode') +const Big = require('big.js') +const NanoDate = require('nano-date').default + const debug = require('debug') const log = debug('jsipns') log.error = debug('jsipns:error') const ipnsEntryProto = require('./pb/ipns.proto') +const { parseRFC3339 } = require('./utils') const ERRORS = require('./errors') /** * Creates a new ipns entry and signs it with the given private key. + * The ipns entry validity should follow the [RFC3339]{@link https://www.ietf.org/rfc/rfc3339.txt} with nanoseconds precision. * Note: This function does not embed the public key. If you want to do that, use `EmbedPublicKey`. * * @param {Object} privateKey private key for signing the record. * @param {string} value value to be stored in the record. - * @param {number} seq sequence number of the record. - * @param {string} eol end of life datetime of the record. - * @param {function(Error)} [callback] - * @returns {Promise|void} + * @param {number} seq number representing the current version of the record. + * @param {string} lifetime lifetime of the record (in milliseconds). + * @param {function(Error, entry)} [callback] + * @returns {function(Error, entry)} callback */ -const create = (privateKey, value, seq, eol, callback) => { - const validity = eol.toISOString() +const create = (privateKey, value, seq, lifetime, callback) => { + // Calculate eol with nanoseconds precision + const bnLifetime = new Big(lifetime) + const bnCurrentDate = new Big(new NanoDate()) + const bnEol = bnCurrentDate.plus(bnLifetime).times('10e+6') + const nanoDateEol = new NanoDate(bnEol.toString()) + + // Validity in ISOString with nanoseconds precision and validity type EOL + const isoValidity = nanoDateEol.toISOStringFull() const validityType = ipnsEntryProto.ValidityType.EOL - sign(privateKey, value, validityType, validity, (error, signature) => { + sign(privateKey, value, validityType, isoValidity, (error, signature) => { if (error) { - log.error(error) - return callback(error) + log.error('record signature creation failed') + return callback(Object.assign(new Error('record signature verification failed'), { code: ERRORS.ERR_SIGNATURE_CREATION })) } const entry = { value: value, signature: signature, // TODO confirm format compliance with go-ipfs validityType: validityType, - validity: validity, + validity: isoValidity, sequence: seq } @@ -48,7 +60,7 @@ const create = (privateKey, value, seq, eol, callback) => { * @param {Object} publicKey public key for validating the record. * @param {Object} entry ipns entry record. * @param {function(Error)} [callback] - * @returns {Promise|void} + * @returns {function(Error)} callback */ const validate = (publicKey, entry, callback) => { const { value, validityType, validity } = entry @@ -63,7 +75,14 @@ const validate = (publicKey, entry, callback) => { // Validate according to the validity type if (validityType === ipnsEntryProto.ValidityType.EOL) { - const validityDate = Date.parse(validity.toString()) + let validityDate + + try { + validityDate = parseRFC3339(validity.toString()) + } catch (e) { + log.error('unrecognized validity format (not an rfc3339 format)') + return callback(Object.assign(new Error('unrecognized validity format (not an rfc3339 format)'), { code: ERRORS.ERR_UNRECOGNIZED_FORMAT })) + } if (validityDate < Date.now()) { log.error('record has expired') @@ -85,10 +104,10 @@ const validate = (publicKey, entry, callback) => { * @param {Object} publicKey public key for validating the record. * @param {Object} entry ipns entry record. * @param {function(Error)} [callback] - * @returns {Promise|void} + * @return {Void} */ const embedPublicKey = (publicKey, entry, callback) => { - return callback(new Error('not implemented yet')) + callback(new Error('not implemented yet')) } /** @@ -97,33 +116,30 @@ const embedPublicKey = (publicKey, entry, callback) => { * @param {Object} peerId peer identifier object. * @param {Object} entry ipns entry record. * @param {function(Error)} [callback] - * @returns {Promise|void} + * @return {Void} */ const extractPublicKey = (peerId, entry, callback) => { - return callback(new Error('not implemented yet')) + callback(new Error('not implemented yet')) } -// rawStdEncoding as go -// TODO Remove once resolved -// Created PR for allowing this inside base32-encode https://github.com/LinusU/base32-encode/issues/2 -const regex = new RegExp('=', 'g') -const rawStdEncoding = (key) => base32Encode(key, 'RFC4648').replace(regex, '') +// rawStdEncoding with RFC4648 +const rawStdEncoding = (key) => base32Encode(key, 'RFC4648', { padding: false }) /** - * Get key for storing the record in the datastore. + * Get key for storing the record locally. * Format: /ipns/${base32()} * * @param {Buffer} key peer identifier object. * @returns {string} */ -const getDatastoreKey = (key) => `/ipns/${rawStdEncoding(key)}` +const getLocalKey = (key) => `/ipns/${rawStdEncoding(key)}` /** * Get key for sharing the record in the routing mechanism. * Format: ${base32(/ipns/)}, ${base32(/pk/)} * * @param {Buffer} key peer identifier object. - * @returns {string} + * @returns {Object} containgin the `nameKey` and the `ipnsKey`. */ const getIdKeys = (key) => { const pkBuffer = Buffer.from('/pk/') @@ -165,8 +181,8 @@ module.exports = { embedPublicKey, // extract public key from the record extractPublicKey, - // get key for datastore - getDatastoreKey, + // get key for storing the entry locally + getLocalKey, // get keys for routing getIdKeys, // marshal diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..3b2fb2b --- /dev/null +++ b/src/utils.js @@ -0,0 +1,56 @@ +'use strict' + +const leftPad = require('left-pad') + +/** + * Convert a JavaScript date into an `RFC3339Nano` formatted + * string. + * + * @param {Date} time + * @returns {string} + */ +module.exports.toRFC3339 = (time) => { + const year = time.getUTCFullYear() + const month = leftPad(time.getUTCMonth() + 1, 2, '0') + const day = leftPad(time.getUTCDate(), 2, '0') + const hour = leftPad(time.getUTCHours(), 2, '0') + const minute = leftPad(time.getUTCMinutes(), 2, '0') + const seconds = leftPad(time.getUTCSeconds(), 2, '0') + const milliseconds = time.getUTCMilliseconds() + const nanoseconds = milliseconds * 1000 * 1000 + + return `${year}-${month}-${day}T${hour}:${minute}:${seconds}.${nanoseconds}Z` +} + +/** + * Parses a date string formatted as `RFC3339Nano` into a + * JavaScript Date object. + * + * @param {string} time + * @returns {Date} + */ +module.exports.parseRFC3339 = (time) => { + const rfc3339Matcher = new RegExp( + // 2006-01-02T + '(\\d{4})-(\\d{2})-(\\d{2})T' + + // 15:04:05 + '(\\d{2}):(\\d{2}):(\\d{2})' + + // .999999999Z + '\\.(\\d+)Z' + ) + const m = String(time).trim().match(rfc3339Matcher) + + if (!m) { + throw new Error('Invalid format') + } + + const year = parseInt(m[1], 10) + const month = parseInt(m[2], 10) - 1 + const date = parseInt(m[3], 10) + const hour = parseInt(m[4], 10) + const minute = parseInt(m[5], 10) + const second = parseInt(m[6], 10) + const millisecond = parseInt(m[7].slice(0, -6), 10) + + return new Date(Date.UTC(year, month, date, hour, minute, second, millisecond)) +} diff --git a/test/index.spec.js b/test/index.spec.js index e373c32..aa873e7 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -35,7 +35,7 @@ describe('ipns', function () { ipfs.id((err, id) => { if (err) { - throw err + return cb(err) } ipfsId = id @@ -54,40 +54,64 @@ describe('ipns', function () { }) after(function (done) { - ipfsd.stop(() => done()) + if (ipfsd) { + ipfsd.stop(() => done()) + } else { + done() + } }) - it('should create an ipns record correctly', () => { + it('should create an ipns record correctly', (done) => { const sequence = 0 - const eol = new Date(Date.now()) + const validity = 1000000 - ipns.create(rsa, cid, sequence, eol, (err, entry) => { + ipns.create(rsa, cid, sequence, validity, (err, entry) => { expect(err).to.not.exist() expect(entry).to.deep.include({ value: cid, - sequence: sequence, - validity: eol + sequence: sequence }) + expect(entry).to.have.a.property('validity') expect(entry).to.have.a.property('signature') expect(entry).to.have.a.property('validityType') + + done() }) }) - it('should create an ipns record and validate it correctly', () => { + it('should create an ipns record and validate it correctly', (done) => { const sequence = 0 - const eol = new Date(Date.now()) + const validity = 1000000 - ipns.create(rsa, cid, sequence, eol, (err, entry) => { + ipns.create(rsa, cid, sequence, validity, (err, entry) => { expect(err).to.not.exist() ipns.validate(rsa.public, entry, (err, res) => { expect(err).to.not.exist() + + done() }) }) }) + it('should create an ipns record with a validity of 1 nanosecond correctly and it should not be valid 1ms later', (done) => { + const sequence = 0 + const validity = 0.00001 + + ipns.create(rsa, cid, sequence, validity, (err, entry) => { + expect(err).to.not.exist() + + setTimeout(() => { + ipns.validate(rsa.public, entry, (err, res) => { + expect(err).to.exist() + done() + }) + }, 1) + }) + }) + it('should get datastore key correctly', () => { - const datastoreKey = ipns.getDatastoreKey(fromB58String(ipfsId.id)) + const datastoreKey = ipns.getLocalKey(fromB58String(ipfsId.id)) expect(datastoreKey).to.exist() expect(datastoreKey).to.startsWith('/ipns/') From ccd26057f2d08f70bb862b56d24e1d1fefb80d8b Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Wed, 27 Jun 2018 11:01:33 +0100 Subject: [PATCH 4/4] added test for marshal and unmarshal ipns records --- package.json | 1 + src/index.js | 6 +++--- test/index.spec.js | 26 ++++++++++++++++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 217e135..71dcc4f 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "devDependencies": { "aegir": "^13.1.0", "chai": "^4.1.2", + "chai-bytes": "^0.1.1", "chai-string": "^1.4.0", "dirty-chai": "^2.0.1", "ipfs": "^0.29.3", diff --git a/src/index.js b/src/index.js index f5e3260..909177b 100644 --- a/src/index.js +++ b/src/index.js @@ -22,7 +22,7 @@ const ERRORS = require('./errors') * @param {number} seq number representing the current version of the record. * @param {string} lifetime lifetime of the record (in milliseconds). * @param {function(Error, entry)} [callback] - * @returns {function(Error, entry)} callback + * @return {Void} */ const create = (privateKey, value, seq, lifetime, callback) => { // Calculate eol with nanoseconds precision @@ -60,7 +60,7 @@ const create = (privateKey, value, seq, lifetime, callback) => { * @param {Object} publicKey public key for validating the record. * @param {Object} entry ipns entry record. * @param {function(Error)} [callback] - * @returns {function(Error)} callback + * @return {Void} */ const validate = (publicKey, entry, callback) => { const { value, validityType, validity } = entry @@ -139,7 +139,7 @@ const getLocalKey = (key) => `/ipns/${rawStdEncoding(key)}` * Format: ${base32(/ipns/)}, ${base32(/pk/)} * * @param {Buffer} key peer identifier object. - * @returns {Object} containgin the `nameKey` and the `ipnsKey`. + * @returns {Object} containing the `nameKey` and the `ipnsKey`. */ const getIdKeys = (key) => { const pkBuffer = Buffer.from('/pk/') diff --git a/test/index.spec.js b/test/index.spec.js index aa873e7..d6a961a 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -3,9 +3,11 @@ const chai = require('chai') const dirtyChai = require('dirty-chai') +const chaiBytes = require('chai-bytes') const chaiString = require('chai-string') const expect = chai.expect chai.use(dirtyChai) +chai.use(chaiBytes) chai.use(chaiString) const ipfs = require('ipfs') @@ -110,6 +112,30 @@ describe('ipns', function () { }) }) + it('should create an ipns record, marshal and unmarshal it, as well as validate it correctly', (done) => { + const sequence = 0 + const validity = 1000000 + + ipns.create(rsa, cid, sequence, validity, (err, entryDataCreated) => { + expect(err).to.not.exist() + + const marshalledData = ipns.marshal(entryDataCreated) + const unmarshalledData = ipns.unmarshal(marshalledData) + + expect(entryDataCreated.value).to.equal(unmarshalledData.value.toString()) + expect(entryDataCreated.validity).to.equal(unmarshalledData.validity.toString()) + expect(entryDataCreated.validityType).to.equal(unmarshalledData.validityType) + expect(entryDataCreated.signature).to.equalBytes(unmarshalledData.signature) + expect(entryDataCreated.sequence).to.equal(unmarshalledData.sequence) + + ipns.validate(rsa.public, unmarshalledData, (err, res) => { + expect(err).to.not.exist() + + done() + }) + }) + }) + it('should get datastore key correctly', () => { const datastoreKey = ipns.getLocalKey(fromB58String(ipfsId.id))