UNPKG

3box

Version:
343 lines (314 loc) 11.5 kB
const { mnemonicToSeed, entropyToMnemonic } = require('@ethersproject/hdnode') const EventEmitter = require('events') const didJWT = require('did-jwt') const { Resolver } = require('did-resolver') const get3IdResolver = require('3id-resolver').getResolver const getMuportResolver = require('muport-did-resolver').getResolver const DidDocument = require('ipfs-did-document') const localstorage = require('store') const Identities = require('orbit-db-identity-provider') const { OdbIdentityProvider } = require('3box-orbitdb-plugins') Identities.addIdentityProvider(OdbIdentityProvider) const utils = require('../utils/index') const Keyring = require('./keyring') const config = require('../config.js') const nacl = require('tweetnacl') const { randomNonce } = require('./utils') const DID_METHOD_NAME = '3' const STORAGE_KEY = 'serialized3id_' const MUPORT_IPFS = { host: config.muport_ipfs_host, port: config.muport_ipfs_port, protocol: config.muport_ipfs_protocol} const POLL_INTERVAL = 500 class ThreeId { constructor (provider, ipfs, keystore, opts = {}) { this.events = new EventEmitter() this._provider = provider this._has3idProv = Boolean(opts.has3idProv) this._ipfs = ipfs this._muportIpfs = opts.muportIpfs || MUPORT_IPFS this._pubkeys = { spaces: {} } this._keystore = keystore const threeIdResolver = get3IdResolver(ipfs, { pin: true }) const muportResolver = getMuportResolver(ipfs) const resolver = new Resolver({...threeIdResolver, ...muportResolver}) OdbIdentityProvider.setDidResolver(resolver) } startUpdatePolling () { if (this._has3idProv) { const poll = async (method, event) => { const result = await utils.callRpc(this._provider, method) result.map(data => { this.events.emit(event, data) }) } this._pollInterval = setInterval(() => { poll('3id_newAuthMethodPoll', 'new-auth-method') poll('3id_newLinkPoll', 'new-link-proof') }, POLL_INTERVAL) } } async signJWT (payload, { space, expiresIn } = {}) { let issuer = this.DID if (space) { issuer = this._subDIDs[space] } if (this._has3idProv) { return utils.callRpc(this._provider, '3id_signClaim', { payload, did: issuer, space, expiresIn }) } else { const keyring = this._keyringBySpace(space) const settings = { signer: keyring.getJWTSigner(), issuer, expiresIn } return didJWT.createJWT(payload, settings) } } get DID () { return this._rootDID } get muportDID () { return this._muportDID } getSubDID (space) { return this._subDIDs[space] } async getOdbId (space) { return Identities.createIdentity({ type: '3ID', threeId: this, space, keystore: this._keystore }) } serializeState () { if (this._has3idProv) throw new Error('Can not serializeState of IdentityWallet') let stateObj = { managementAddress: this.managementAddress, seed: this._mainKeyring.serialize(), spaceSeeds: {}, } Object.keys(this._keyrings).map(name => { stateObj.spaceSeeds[name] = this._keyrings[name].serialize() }) return JSON.stringify(stateObj) } _initKeys (serializedState) { if (this._has3idProv) throw new Error('Can not initKeys of IdentityWallet') this._keyrings = {} const state = JSON.parse(serializedState) // TODO remove toLowerCase() in future, should be sanitized elsewhere // this forces existing state to correct state so that address <-> // rootstore relation holds this.managementAddress = state.managementAddress.toLowerCase() this._mainKeyring = new Keyring(state.seed) Object.keys(state.spaceSeeds).map(name => { this._keyrings[name] = new Keyring(state.spaceSeeds[name]) }) localstorage.set(STORAGE_KEY + this.managementAddress, this.serializeState()) } async _initDID () { const muportPromise = this._initMuport() this._rootDID = await this._init3ID() let spaces if (this._has3idProv) { spaces = Object.keys(this._pubkeys.spaces) } else { spaces = Object.keys(this._keyrings) } const subDIDs = await Promise.all( spaces.map(space => { return this._init3ID(space) }) ) this._subDIDs = {} spaces.map((space, i) => { this._subDIDs[space] = subDIDs[i] }) await muportPromise } async _init3ID (spaceName) { const doc = new DidDocument(this._ipfs, DID_METHOD_NAME) const pubkeys = await this.getPublicKeys(spaceName, true) if (!spaceName) { doc.addPublicKey('signingKey', 'Secp256k1VerificationKey2018', 'publicKeyHex', pubkeys.signingKey) doc.addPublicKey('encryptionKey', 'Curve25519EncryptionPublicKey', 'publicKeyBase64', pubkeys.asymEncryptionKey) doc.addPublicKey('managementKey', 'Secp256k1VerificationKey2018', 'ethereumAddress', pubkeys.managementKey) doc.addAuthentication('Secp256k1SignatureAuthentication2018', 'signingKey') } else { doc.addPublicKey('subSigningKey', 'Secp256k1VerificationKey2018', 'publicKeyHex', pubkeys.signingKey) doc.addPublicKey('subEncryptionKey', 'Curve25519EncryptionPublicKey', 'publicKeyBase64', pubkeys.asymEncryptionKey) doc.addAuthentication('Secp256k1SignatureAuthentication2018', 'subSigningKey') doc.addCustomProperty('space', spaceName) doc.addCustomProperty('root', this.DID) const payload = { subSigningKey: pubkeys.signingKey, subEncryptionKey: pubkeys.asymEncryptionKey, space: spaceName, iat: null } const signature = (await this.signJWT(payload, { use3ID: true })).split('.')[2] doc.addCustomProperty('proof', { alg: 'ES256K', signature }) } await doc.commit({ noTimestamp: true }) return doc.DID } async _initMuport () { const keys = await this.getPublicKeys(null) const doc = createMuportDocument(keys.signingKey, keys.managementKey, keys.asymEncryptionKey) let docHash = (await this._ipfs.add(Buffer.from(JSON.stringify(doc))))[0].hash this._muportDID = 'did:muport:' + docHash this.muportFingerprint = utils.sha256Multihash(this.muportDID) } async getAddress () { if (this._has3idProv) { return utils.callRpc(this._provider, '3id_getLink') } else { return this.managementAddress } } async linkManagementAddress () { if (this._has3idProv) { return utils.callRpc(this._provider, '3id_linkManagementKey') } } async authenticate (spaces, opts = {}) { spaces = spaces || [] if (this._has3idProv) { const pubkeys = await utils.callRpc(this._provider, '3id_authenticate', { spaces, authData: opts.authData, address: opts.address }) this._pubkeys.main = pubkeys.main this._pubkeys.spaces = Object.assign(this._pubkeys.spaces, pubkeys.spaces) if (!this.DID) { await this._initDID() } else { for (const space of spaces) { if (!this._subDIDs[space]) { this._subDIDs[space] = await this._init3ID(space) } } } } else { for (const space of spaces) { await this._initKeyringByName(space) } } } async isAuthenticated (spaces = []) { return spaces.reduce((acc, space) => acc && Object.keys(this._subDIDs).includes(space), true) } async _initKeyringByName (name) { if (this._has3idProv) throw new Error('Can not initKeyringByName of IdentityWallet') if (!this._keyrings[name]) { const sig = await utils.openSpaceConsent(this.managementAddress, this._provider, name) const entropy = '0x' + utils.sha256(sig.slice(2)) const seed = mnemonicToSeed(entropyToMnemonic(entropy)) this._keyrings[name] = new Keyring(seed) this._subDIDs[name] = await this._init3ID(name) localstorage.set(STORAGE_KEY + this.managementAddress, this.serializeState()) return true } else { return false } } async getPublicKeys (space, uncompressed) { let pubkeys if (this._has3idProv) { pubkeys = Object.assign({}, space ? this._pubkeys.spaces[space] : this._pubkeys.main) if (uncompressed) { pubkeys.signingKey = Keyring.uncompress(pubkeys.signingKey) } } else { pubkeys = this._keyringBySpace(space).getPublicKeys(uncompressed) pubkeys.managementKey = this.managementAddress } return pubkeys } async encrypt (message, space, to) { if (this._has3idProv) { return utils.callRpc(this._provider, '3id_encrypt', { message, space, to }) } else { const keyring = this._keyringBySpace(space) let paddedMsg = typeof message === 'string' ? utils.pad(message) : message if (to) { return keyring.asymEncrypt(paddedMsg, to) } else { return keyring.symEncrypt(paddedMsg) } } } async decrypt (encObj, space, toBuffer) { if (this._has3idProv) { const res = await utils.callRpc(this._provider, '3id_decrypt', { ...encObj, space, buffer: toBuffer }) return toBuffer ? Buffer.from(res) : res } else { const keyring = this._keyringBySpace(space) let paddedMsg if (encObj.ephemeralFrom) { paddedMsg = keyring.asymDecrypt(encObj.ciphertext, encObj.ephemeralFrom, encObj.nonce, toBuffer) } else { paddedMsg = keyring.symDecrypt(encObj.ciphertext, encObj.nonce, toBuffer) } return toBuffer ? paddedMsg : utils.unpad(paddedMsg) } } async hashDBKey (key, space) { if (this._has3idProv) { return utils.callRpc(this._provider, '3id_hashEntryKey', { key, space }) } else { const salt = this._keyringBySpace(space).getDBSalt() return utils.sha256Multihash(salt + key) } } _keyringBySpace (space) { return space ? this._keyrings[space] : this._mainKeyring } logout () { localstorage.remove(STORAGE_KEY + this.managementAddress) if (this._pollInterval) { clearInterval(this._pollInterval) } } static isLoggedIn (address) { return Boolean(localstorage.get(STORAGE_KEY + address.toLowerCase())) } static async getIdFromEthAddress (address, provider, ipfs, keystore, opts = {}) { opts.has3idProv = Boolean(provider.is3idProvider) if (opts.has3idProv) { return new ThreeId(provider, ipfs, keystore, opts) } else { const normalizedAddress = address.toLowerCase() let serialized3id = localstorage.get(STORAGE_KEY + normalizedAddress) if (serialized3id) { if (opts.consentCallback) opts.consentCallback(false) } else { let sig if (opts.contentSignature) { sig = opts.contentSignature } else { sig = await utils.openBoxConsent(normalizedAddress, provider) } if (opts.consentCallback) opts.consentCallback(true) const entropy = '0x' + utils.sha256(sig.slice(2)) const mnemonic = entropyToMnemonic(entropy) const seed = mnemonicToSeed(mnemonic) serialized3id = JSON.stringify({ managementAddress: normalizedAddress, seed, spaceSeeds: {} }) } const threeId = new ThreeId(provider, ipfs, keystore, opts) threeId._initKeys(serialized3id) await threeId._initDID() return threeId } } } const createMuportDocument = (signingKey, managementKey, asymEncryptionKey) => { return { version: 1, signingKey, managementKey, asymEncryptionKey } } module.exports = ThreeId