UNPKG

ara-identity

Version:

Create and resolve decentralized identity based Ara identifiers.

437 lines (367 loc) 11.4 kB
/* eslint-disable prefer-destructuring */ const { Authentication } = require('did-document') const { PublicKey } = require('did-document/public-key') const { Service } = require('did-document/service') const createContext = require('ara-context') const isBuffer = require('is-buffer') const crypto = require('ara-crypto') const bip39 = require('bip39') const ss = require('ara-secret-storage') const { kEd25519VerificationKey2018, kEd25519SignatureAuthentication2018, kSecp256k1VerificationKey2018, kSecp256k1SignatureAuthentication2018, } = require('ld-cryptosuite-registry') const { toHex } = require('./util') const ethereum = require('./ethereum') const protobuf = require('./protobuf') const ddo = require('./ddo') const did = require('./did') const BEGIN_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\n' const END_PUBLIC_KEY = '\n-----END PUBLIC KEY-----' /** * Creates a new ARA identity. * @public * @param {Object} opts * @return {Object} * @throws TypeError */ async function create(opts) { let mnemonic if (null == opts || 'object' !== typeof opts) { throw new TypeError('Expecting object.') } if (opts.context && 'object' !== typeof opts.context) { throw new TypeError('Expecting context object.') } if (null == opts.password) { throw new TypeError('Expecting password.') } else if (opts.password && 'string' !== typeof opts.password) { throw new TypeError('Expecting password to be a string.') } if (opts.ddo) { if (opts.ddo.authentication && 'object' !== typeof opts.ddo.authentication) { throw new TypeError('Expecting authentication to be an object.') } if (opts.ddo.publicKeys && !Array.isArray(opts.ddo.publicKeys)) { throw new TypeError('Expecting additional publicKey to be an array.') } if (opts.ddo.service && !Array.isArray(opts.ddo.service)) { throw new TypeError('Expecting service endpoints to be an array.') } } if (opts.ddo) { if (!opts.ddo.publicKey && opts.ddo.publicKeys) { opts.ddo.publicKey = opts.ddo.publicKeys } if (opts.ddo.publicKey && !Array.isArray(opts.ddo.publicKey)) { opts.ddo.publicKey = [ opts.ddo.publicKey ] } } const { context = createContext({ provider: false }) } = opts const password = crypto.blake2b(Buffer.from(opts.password)) let encryptedAraKeystore = null let encryptedEthKeystore = null let encryptionKey = null let didDocument = null let publicKey = null let secretKey = null let account = null let wallet = null let didUri = null let seed = isBuffer(opts.seed) ? opts.seed : null if (isBuffer(opts.publicKey) && isBuffer(opts.secretKey)) { await modifyIdentity() } else { await createNewIdentity() } if (opts.files && Array.isArray(opts.files)) { encryptedEthKeystore = opts.files.find((f) => 'keystore/eth' === f.path) encryptedAraKeystore = opts.files.find((f) => 'keystore/ara' === f.path) } if (opts.keystore && opts.keystore.eth) { encryptedEthKeystore = opts.keystore.eth if ('buffer' in encryptedEthKeystore) { encryptedEthKeystore = encryptedEthKeystore.buffer } if (isBuffer(encryptedEthKeystore)) { encryptedEthKeystore = JSON.parse(encryptedEthKeystore) } } if (opts.keystore && opts.keystore.ara) { encryptedAraKeystore = opts.keystore.ara if ('buffer' in encryptedAraKeystore) { encryptedAraKeystore = encryptedAraKeystore.buffer } if (isBuffer(encryptedAraKeystore)) { encryptedAraKeystore = JSON.parse(encryptedAraKeystore) } } else { encryptedAraKeystore = ss.encrypt(secretKey, { iv: crypto.randomBytes(16), key: password.slice(0, 16) }) } if (opts.ddo) { // add default authentication to ddo if available if (opts.ddo.authentication) { if (!Array.isArray(opts.ddo.authentication)) { // eslint-disable-next-line no-param-reassign opts.ddo.authentication = [ opts.ddo.authentication ] } for (const auth of opts.ddo.authentication) { // eslint-disable-next-line no-shadow didDocument.addAuthentication(new Authentication( auth.type, { publicKey: auth.publicKey } )) } } // additional keys if (Array.isArray(opts.ddo.publicKey)) { for (const pk of opts.ddo.publicKey) { let { publicKeyBase64, publicKeyBase58 } = pk const { publicKeyHex, publicKeyPem } = pk if (!pk.id.startsWith('did:')) { pk.id = `${didUri.did}#${pk.id}` } const pub = Buffer.from(publicKeyHex, 'hex') if (!publicKeyBase58) { publicKeyBase58 = crypto.base58.encode(pub).toString() } if (!publicKeyBase64) { publicKeyBase64 = crypto.base64.encode(pub).toString() } didDocument.addPublicKey(new PublicKey({ id: pk.id, type: pk.type || kEd25519VerificationKey2018, owner: didUri.did, // public key variants publicKeyHex, publicKeyBase58, publicKeyBase64, publicKeyPem: publicKeyPem || ( BEGIN_PUBLIC_KEY + crypto.base64.encode(pub).toString() + END_PUBLIC_KEY ) })) } } // add service endpoints if (Array.isArray(opts.ddo.service)) { for (const service of opts.ddo.service) { if (!service.id.startsWith('did:')) { service.id = `${didUri.did}#${service.id}` } didDocument.addService(createService({ id: service.id, type: service.type, serviceEndpoint: service.serviceEndpoint, service })) } } } // sign the DDO for the proof const digest = didDocument.digest(crypto.blake2b) didDocument.proof({ // from ld-cryptosuite-registry' type: kEd25519VerificationKey2018, nonce: crypto.randomBytes(32).toString('hex'), domain: 'ara', // ISO timestamp created: (new Date()).toISOString(), creator: `${didUri.did}#owner`, signatureValue: toHex(crypto.sign(digest, secretKey)) }) if (!encryptedEthKeystore) { throw new TypeError("Unable to determine file: 'keystore/eth'") } const files = [ { path: 'ddo.json', buffer: Buffer.from(JSON.stringify(didDocument)) }, { path: 'keystore/eth', buffer: Buffer.from(JSON.stringify(encryptedEthKeystore)) }, { path: 'keystore/ara', buffer: Buffer.from(JSON.stringify(encryptedAraKeystore)) }, { path: 'schema.proto', buffer: protobuf.kProtocolBufferSchema, } ] // The intermediate value are the identity fields with // the proof field missing const intermediate = protobuf.messages.Identity.encode({ did: didUri.did, key: publicKey, files, }) files.push({ path: 'identity', buffer: protobuf.messages.Identity.encode({ did: didUri.did, key: publicKey, files, // sign intermediate to get identity signature proof: { signature: crypto.sign(intermediate, secretKey) } }), }) if (null !== encryptionKey) { encryptionKey.fill(0) encryptionKey = null } if (null !== seed) { seed.fill(0) seed = null } if (context) { context.close() } return { account, mnemonic, publicKey, secretKey, wallet, files, ddo: didDocument, did: didUri, } async function createNewIdentity() { if (opts.context && 'object' !== typeof opts.context.web3) { throw new TypeError('Expecting web3 to be in context.') } if (null == opts.mnemonic) { mnemonic = bip39.generateMnemonic() } else { if (!bip39.validateMnemonic(opts.mnemonic)) { throw new TypeError('Expecting a valid bip39 mnemonic') } // eslint-disable-next-line prefer-destructuring mnemonic = opts.mnemonic } if (isBuffer(opts.seed)) { seed = opts.seed } else { seed = crypto.blake2b(await bip39.mnemonicToSeed(mnemonic)) } const kp = crypto.keyPair(seed) publicKey = kp.publicKey secretKey = kp.secretKey const { web3 } = context const { salt, iv } = await ethereum.keystore.create() encryptionKey = createEncryptionKey() wallet = await ethereum.wallet.load({ seed: await bip39.mnemonicToSeed(mnemonic) }) account = await ethereum.account.create({ web3, privateKey: wallet.getPrivateKey() }) const kstore = await ethereum.keystore.dump({ password: opts.password, salt, iv, privateKey: wallet.getPrivateKey(), }) const encodedKeystore = protobuf.messages.KeyStore.encode(kstore) encryptedEthKeystore = ss.encrypt(encodedKeystore, { key: encryptionKey, iv: crypto.randomBytes(16), }) didUri = did.create(publicKey) didDocument = createDIDDocument() addOwnerPublicKey() addEthPublicKey() } async function modifyIdentity() { publicKey = opts.publicKey secretKey = opts.secretKey encryptionKey = createEncryptionKey() didUri = did.create(publicKey) didDocument = createDIDDocument() addOwnerPublicKey() } function addOwnerPublicKey() { didDocument.addPublicKey(new PublicKey({ id: `${didUri.did}#owner`, type: kEd25519VerificationKey2018, owner: didUri.did, // public key variants publicKeyHex: toHex(publicKey), publicKeyBase64: crypto.base64.encode(publicKey).toString(), publicKeyBase58: crypto.base58.encode(publicKey).toString(), publicKeyPem: ( BEGIN_PUBLIC_KEY + crypto.base64.encode(publicKey).toString() + END_PUBLIC_KEY ) })) didDocument.addAuthentication(new Authentication( kEd25519SignatureAuthentication2018, { publicKey: `${didUri.did}#owner` } )) } function addEthPublicKey() { didDocument.addPublicKey(new PublicKey({ id: `${didUri.did}#eth`, type: kSecp256k1VerificationKey2018, owner: didUri.did, // public key variants publicKeyHex: toHex(wallet.getPublicKey()), publicKeyBase64: crypto.base64.encode(wallet.getPublicKey()).toString(), publicKeyBase58: crypto.base58.encode(wallet.getPublicKey()).toString(), publicKeyPem: ( BEGIN_PUBLIC_KEY + crypto.base64.encode(wallet.getPublicKey()).toString() + END_PUBLIC_KEY ) })) didDocument.addAuthentication(new Authentication( kSecp256k1SignatureAuthentication2018, { publicKey: `${didUri.did}#eth` } )) } function createEncryptionKey() { return Buffer.allocUnsafe(16).fill(secretKey.slice(0, 16)) } function createDIDDocument() { const conf = { id: didUri } // opts is DDO if ('id' in opts && 'publicKey' in opts) { conf.created = opts.created conf.updated = opts.updated } else if (opts.ddo) { conf.created = opts.ddo.created conf.updated = opts.ddo.updated } conf.revoked = opts.revoked if (conf.updated) { conf.updated = new Date() } return ddo.create(conf) } } /** * Creates a new service endpoint to be added to the service array. * @param {Object} opts * @return {Object} */ function createService(opts = {}) { const { id, type, serviceEndpoint, service } = opts return new Service( id, type, serviceEndpoint, service ) } module.exports = { create }