/* * This file is part of the storage node for the Joystream project. * Copyright (C) 2019 Joystream Contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ 'use strict' const path = require('path') const fs = require('fs') const debug = require('debug')('joystream:runtime:identities') const { Keyring } = require('@polkadot/keyring') const utilCrypto = require('@polkadot/util-crypto') /* * Add identity management to the substrate API. * * This loosely groups: accounts, key management, and membership. */ class IdentitiesApi { static async create(base, { accountFile, passphrase, canPromptForPassphrase }) { const ret = new IdentitiesApi() ret.base = base await ret.init(accountFile, passphrase, canPromptForPassphrase) return ret } async init(accountFile, passphrase, canPromptForPassphrase) { debug('Init') // Creatre keyring this.keyring = new Keyring() this.canPromptForPassphrase = canPromptForPassphrase || false // Load account file, if possible. try { this.key = await this.loadUnlock(accountFile, passphrase) } catch (err) { debug('Error loading account file:', err.message) } } /* * Load a key file and unlock it if necessary. */ async loadUnlock(accountFile, passphrase) { const fullname = path.resolve(accountFile) debug('Initializing key from', fullname) const key = this.keyring.addFromJson(require(fullname)) await this.tryUnlock(key, passphrase) debug('Successfully initialized with address', key.address) return key } /* * Try to unlock a key if it isn't already unlocked. * passphrase should be supplied as argument. */ async tryUnlock(key, passphrase) { if (!key.isLocked) { debug('Key is not locked, not attempting to unlock') return } // First try with an empty passphrase - for convenience try { key.decodePkcs8('') if (passphrase) { debug('Key was not encrypted, supplied passphrase was ignored') } return } catch (err) { // pass } // Then with supplied passphrase try { debug('Decrypting with supplied passphrase') key.decodePkcs8(passphrase) return } catch (err) { // pass } // If that didn't work, ask for a passphrase if appropriate if (this.canPromptForPassphrase) { passphrase = await this.askForPassphrase(key.address) key.decodePkcs8(passphrase) return } throw new Error('invalid passphrase supplied') } /* * Ask for a passphrase */ /* eslint-disable class-methods-use-this */ // Disable lint because the method used by a mocking library. askForPassphrase(address) { // Query for passphrase const prompt = require('password-prompt') return prompt(`Enter passphrase for ${address}: `, { required: false }) } /* * Return true if the account is a root account of a member */ async isMember(accountId) { const memberIds = await this.memberIdsOf(accountId) // return array of member ids return memberIds.length > 0 // true if at least one member id exists for the acccount } /* * Return all the member IDs of an account by the root account id */ async memberIdsOf(accountId) { const decoded = this.keyring.decodeAddress(accountId) return this.base.api.query.members.memberIdsByRootAccountId(decoded) } /* * Return all the member IDs of an account by the controller account id */ async memberIdsOfController(accountId) { const decoded = this.keyring.decodeAddress(accountId) return this.base.api.query.members.memberIdsByControllerAccountId(decoded) } /* * Return the first member ID of an account, or undefined if not a member root account. */ async firstMemberIdOf(accountId) { const decoded = this.keyring.decodeAddress(accountId) const ids = await this.base.api.query.members.memberIdsByRootAccountId(decoded) return ids[0] } /* * Export a key pair to JSON. Will ask for a passphrase. */ async exportKeyPair(accountId) { const passphrase = await this.askForPassphrase(accountId) // Produce JSON output return this.keyring.toJson(accountId, passphrase) } /* * Export a key pair and write it to a JSON file with the account ID as the * name. */ async writeKeyPairExport(accountId, prefix) { // Generate JSON const data = await this.exportKeyPair(accountId) // Write JSON let filename = `${data.address}.json` if (prefix) { const path = require('path') filename = path.resolve(prefix, filename) } fs.writeFileSync(filename, JSON.stringify(data), { encoding: 'utf8', mode: 0o600, }) return filename } /* * Register account id with userInfo as a new member * using default policy 0, returns new member id */ async registerMember(accountId, userInfo) { const tx = this.base.api.tx.members.buyMembership(0, userInfo.handle, userInfo.avatarUri, userInfo.about) return this.base.signAndSendThenGetEventResult(accountId, tx, { module: 'members', event: 'MemberRegistered', type: 'MemberId', index: 0, }) } /* * Injects a keypair and sets it as the default identity */ useKeyPair(keyPair) { this.key = this.keyring.addPair(keyPair) } /* * Create a new role key. If no name is given, * default to 'storage'. */ async createNewRoleKey(name) { name = name || 'storage-provider' // Generate new key pair const keyPair = utilCrypto.naclKeypairFromRandom() // Encode to an address. const addr = this.keyring.encodeAddress(keyPair.publicKey) debug('Generated new key pair with address', addr) // Add to key wring. We set the meta to identify the account as // a role key. const meta = { name: `${name} role account`, } const createPair = require('@polkadot/keyring/pair').default const pair = createPair('ed25519', keyPair, meta) this.keyring.addPair(pair) return pair } getSudoAccount() { return this.base.api.query.sudo.key() } } module.exports = { IdentitiesApi, }