identities.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. /*
  2. * This file is part of the storage node for the Joystream project.
  3. * Copyright (C) 2019 Joystream Contributors
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation, either version 3 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. */
  18. 'use strict'
  19. const path = require('path')
  20. const fs = require('fs')
  21. // const readline = require('readline')
  22. const debug = require('debug')('joystream:runtime:identities')
  23. const { Keyring } = require('@polkadot/keyring')
  24. const util_crypto = require('@polkadot/util-crypto')
  25. /*
  26. * Add identity management to the substrate API.
  27. *
  28. * This loosely groups: accounts, key management, and membership.
  29. */
  30. class IdentitiesApi {
  31. static async create (base, {account_file, passphrase, canPromptForPassphrase}) {
  32. const ret = new IdentitiesApi()
  33. ret.base = base
  34. await ret.init(account_file, passphrase, canPromptForPassphrase)
  35. return ret
  36. }
  37. async init (account_file, passphrase, canPromptForPassphrase) {
  38. debug('Init')
  39. // Creatre keyring
  40. this.keyring = new Keyring()
  41. this.canPromptForPassphrase = canPromptForPassphrase || false
  42. // Load account file, if possible.
  43. try {
  44. this.key = await this.loadUnlock(account_file, passphrase)
  45. } catch (err) {
  46. debug('Error loading account file:', err.message)
  47. }
  48. }
  49. /*
  50. * Load a key file and unlock it if necessary.
  51. */
  52. async loadUnlock (account_file, passphrase) {
  53. const fullname = path.resolve(account_file)
  54. debug('Initializing key from', fullname)
  55. const key = this.keyring.addFromJson(require(fullname))
  56. await this.tryUnlock(key, passphrase)
  57. debug('Successfully initialized with address', key.address)
  58. return key
  59. }
  60. /*
  61. * Try to unlock a key if it isn't already unlocked.
  62. * passphrase should be supplied as argument.
  63. */
  64. async tryUnlock (key, passphrase) {
  65. if (!key.isLocked) {
  66. debug('Key is not locked, not attempting to unlock')
  67. return
  68. }
  69. // First try with an empty passphrase - for convenience
  70. try {
  71. key.decodePkcs8('')
  72. if (passphrase) {
  73. debug('Key was not encrypted, supplied passphrase was ignored')
  74. }
  75. return
  76. } catch (err) {
  77. // pass
  78. }
  79. // Then with supplied passphrase
  80. try {
  81. debug('Decrypting with supplied passphrase')
  82. key.decodePkcs8(passphrase)
  83. return
  84. } catch (err) {
  85. // pass
  86. }
  87. // If that didn't work, ask for a passphrase if appropriate
  88. if (this.canPromptForPassphrase) {
  89. passphrase = await this.askForPassphrase(key.address)
  90. key.decodePkcs8(passphrase)
  91. return
  92. }
  93. throw new Error('invalid passphrase supplied')
  94. }
  95. /*
  96. * Ask for a passphrase
  97. */
  98. askForPassphrase (address) {
  99. // Query for passphrase
  100. const prompt = require('password-prompt')
  101. return prompt(`Enter passphrase for ${address}: `, { required: false })
  102. }
  103. /*
  104. * Return true if the account is a root account of a member
  105. */
  106. async isMember (accountId) {
  107. const memberIds = await this.memberIdsOf(accountId) // return array of member ids
  108. return memberIds.length > 0 // true if at least one member id exists for the acccount
  109. }
  110. /*
  111. * Return all the member IDs of an account by the root account id
  112. */
  113. async memberIdsOf (accountId) {
  114. const decoded = this.keyring.decodeAddress(accountId)
  115. return this.base.api.query.members.memberIdsByRootAccountId(decoded)
  116. }
  117. /*
  118. * Return the first member ID of an account, or undefined if not a member root account.
  119. */
  120. async firstMemberIdOf (accountId) {
  121. const decoded = this.keyring.decodeAddress(accountId)
  122. let ids = await this.base.api.query.members.memberIdsByRootAccountId(decoded)
  123. return ids[0]
  124. }
  125. /*
  126. * Export a key pair to JSON. Will ask for a passphrase.
  127. */
  128. async exportKeyPair (accountId) {
  129. const passphrase = await this.askForPassphrase(accountId)
  130. // Produce JSON output
  131. return this.keyring.toJson(accountId, passphrase)
  132. }
  133. /*
  134. * Export a key pair and write it to a JSON file with the account ID as the
  135. * name.
  136. */
  137. async writeKeyPairExport (accountId, prefix) {
  138. // Generate JSON
  139. const data = await this.exportKeyPair(accountId)
  140. // Write JSON
  141. var filename = `${data.address}.json`
  142. if (prefix) {
  143. const path = require('path')
  144. filename = path.resolve(prefix, filename)
  145. }
  146. fs.writeFileSync(filename, JSON.stringify(data), {
  147. encoding: 'utf8',
  148. mode: 0o600
  149. })
  150. return filename
  151. }
  152. /*
  153. * Register account id with userInfo as a new member
  154. * using default policy 0, returns new member id
  155. */
  156. async registerMember (accountId, userInfo) {
  157. const tx = this.base.api.tx.members.buyMembership(0, userInfo)
  158. return this.base.signAndSendThenGetEventResult(accountId, tx, {
  159. eventModule: 'members',
  160. eventName: 'MemberRegistered',
  161. eventProperty: 'MemberId'
  162. })
  163. }
  164. /*
  165. * Injects a keypair and sets it as the default identity
  166. */
  167. useKeyPair (keyPair) {
  168. this.key = this.keyring.addPair(keyPair)
  169. }
  170. /*
  171. * Create a new role key. If no name is given,
  172. * default to 'storage'.
  173. */
  174. async createNewRoleKey (name) {
  175. name = name || 'storage-provider'
  176. // Generate new key pair
  177. const keyPair = util_crypto.naclKeypairFromRandom()
  178. // Encode to an address.
  179. const addr = this.keyring.encodeAddress(keyPair.publicKey)
  180. debug('Generated new key pair with address', addr)
  181. // Add to key wring. We set the meta to identify the account as
  182. // a role key.
  183. const meta = {
  184. name: `${name} role account`
  185. }
  186. const createPair = require('@polkadot/keyring/pair').default
  187. const pair = createPair('ed25519', keyPair, meta)
  188. this.keyring.addPair(pair)
  189. return pair
  190. }
  191. getSudoAccount() {
  192. return this.base.api.query.sudo.key()
  193. }
  194. }
  195. module.exports = {
  196. IdentitiesApi
  197. }