From b58c0ce820a0cd209e751072605b2b9e565badc2 Mon Sep 17 00:00:00 2001 From: Matthew Aaron Raymer Date: Fri, 9 Dec 2022 18:23:53 +0800 Subject: [PATCH] First iteration of account creation. More refactoring to come --- README.md | 101 +++++++++++++++++++++++ package-lock.json | 6 ++ package.json | 1 + src/libs/crypto/index.ts | 65 +++++++++++++++ src/libs/veramo/appSlice.ts | 100 ++++++++++++++++++++++ src/libs/veramo/setup.ts | 151 ++++++++++++++++++++++++++++++++++ src/views/AccountViewView.vue | 105 ++++++++++------------- 7 files changed, 469 insertions(+), 60 deletions(-) create mode 100644 src/libs/crypto/index.ts create mode 100644 src/libs/veramo/appSlice.ts create mode 100644 src/libs/veramo/setup.ts diff --git a/README.md b/README.md index ad1d5fb..ca6bf7e 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,104 @@ npm run lint ### Customize configuration See [Configuration Reference](https://cli.vuejs.org/config/). + +``` +// Import an existing ID +export const importAndStoreIdentifier = async (mnemonic: string, mnemonicPassword: string, toLowercase: boolean, previousIdentifiers: Array) => { + + // just to get rid of variability that might cause an error + mnemonic = mnemonic.trim().toLowerCase() + + /** + // an approach I pieced together + // requires: yarn add elliptic + // ... plus: + // const EC = require('elliptic').ec + // const secp256k1 = new EC('secp256k1') + // + const keyHex: string = bip39.mnemonicToEntropy(mnemonic) + // returns a KeyPair from the elliptic.ec library + const keyPair = secp256k1.keyFromPrivate(keyHex, 'hex') + // this code is from did-provider-eth createIdentifier + const privateHex = keyPair.getPrivate('hex') + const publicHex = keyPair.getPublic('hex') + const address = didJwt.toEthereumAddress(publicHex) + **/ + + /** + // from https://github.com/uport-project/veramo/discussions/346#discussioncomment-302234 + // ... which almost works but the didJwt.toEthereumAddress is wrong + // requires: yarn add bip32 + // ... plus: import * as bip32 from 'bip32' + // + const seed: Buffer = await bip39.mnemonicToSeed(mnemonic) + const root = bip32.fromSeed(seed) + const node = root.derivePath(UPORT_ROOT_DERIVATION_PATH) + const privateHex = node.privateKey.toString("hex") + const publicHex = node.publicKey.toString("hex") + const address = didJwt.toEthereumAddress('0x' + publicHex) + **/ + + /** + // from https://github.com/uport-project/veramo/discussions/346#discussioncomment-302234 + // requires: yarn add @ethersproject/hdnode + // ... plus: import { HDNode } from '@ethersproject/hdnode' + **/ + const hdnode: HDNode = HDNode.fromMnemonic(mnemonic) + const rootNode: HDNode = hdnode.derivePath(UPORT_ROOT_DERIVATION_PATH) + const privateHex = rootNode.privateKey.substring(2) // original starts with '0x' + const publicHex = rootNode.publicKey.substring(2) // original starts with '0x' + let address = rootNode.address + + const prevIds = previousIdentifiers || []; + + if (toLowercase) { + const foundEqual = R.find( + (id) => utility.rawAddressOfDid(id.did) === address, + prevIds + ) + if (foundEqual) { + // They're trying to create a lowercase version of one that exists in normal case. + // (We really should notify the user.) + appStore.dispatch(appSlice.actions.addLog({log: true, msg: "Will create a normal-case version of the DID since a regular version exists."})) + } else { + address = address.toLowerCase() + } + } else { + // They're not trying to convert to lowercase. + const foundLower = R.find((id) => + utility.rawAddressOfDid(id.did) === address.toLowerCase(), + prevIds + ) + if (foundLower) { + // They're trying to create a normal case version of one that exists in lowercase. + // (We really should notify the user.) + appStore.dispatch(appSlice.actions.addLog({log: true, msg: "Will create a lowercase version of the DID since a lowercase version exists."})) + address = address.toLowerCase() + } + } + + appStore.dispatch(appSlice.actions.addLog({log: false, msg: "... derived keys and address..."})) + + const newId = newIdentifier(address, publicHex, privateHex, UPORT_ROOT_DERIVATION_PATH) + appStore.dispatch(appSlice.actions.addLog({log: false, msg: "... created new ID..."})) + + // awaiting because otherwise the UI may not see that a mnemonic was created + const savedId = await storeIdentifier(newId, mnemonic, mnemonicPassword) + appStore.dispatch(appSlice.actions.addLog({log: false, msg: "... stored new ID..."})) + return savedId +} + +// Create a totally new ID +export const createAndStoreIdentifier = async (mnemonicPassword) => { + + // This doesn't give us the entropy/seed. + //const id = await agent.didManagerCreate() + + const entropy = crypto.randomBytes(32) + const mnemonic = bip39.entropyToMnemonic(entropy) + appStore.dispatch(appSlice.actions.addLog({log: false, msg: "... generated mnemonic..."})) + + return importAndStoreIdentifier(mnemonic, mnemonicPassword, false, []) +} +``` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3dfcb7b..69d42cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "ethereum-cryptography": "^1.1.2", "ethereumjs-util": "^7.1.5", "ethr-did-resolver": "^8.0.0", + "localstorage-slim": "^2.3.0", "luxon": "^3.1.1", "merkletreejs": "^0.3.9", "papaparse": "^5.3.2", @@ -17459,6 +17460,11 @@ "node": ">=8.9.0" } }, + "node_modules/localstorage-slim": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/localstorage-slim/-/localstorage-slim-2.3.0.tgz", + "integrity": "sha512-vGuIEXmoSseZW2dO7Y9vFIs2iBORvxSMlFBpNQpTpuE/s9/myj1Kxz3iQsyDMSRyusLB/Z4T/hlcMH36PUiJrg==" + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz", diff --git a/package.json b/package.json index 9a3ab27..b083314 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "ethereum-cryptography": "^1.1.2", "ethereumjs-util": "^7.1.5", "ethr-did-resolver": "^8.0.0", + "localstorage-slim": "^2.3.0", "luxon": "^3.1.1", "merkletreejs": "^0.3.9", "papaparse": "^5.3.2", diff --git a/src/libs/crypto/index.ts b/src/libs/crypto/index.ts new file mode 100644 index 0000000..8068260 --- /dev/null +++ b/src/libs/crypto/index.ts @@ -0,0 +1,65 @@ +import { IIdentifier } from "@veramo/core"; +import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup"; +import { getRandomBytesSync } from "ethereum-cryptography/random"; +import { entropyToMnemonic } from "ethereum-cryptography/bip39"; +import { wordlist } from "ethereum-cryptography/bip39/wordlists/english"; +import { HDNode } from "@ethersproject/hdnode"; + +/** + * + * + * @param {string} address + * @param {string} publicHex + * @param {string} privateHex + * @param {string} derivationPath + * @return {*} {Omit} + */ +export const newIdentifier = ( + address: string, + publicHex: string, + privateHex: string, + derivationPath: string +): Omit => { + return { + did: DEFAULT_DID_PROVIDER_NAME + ":" + address, + keys: [ + { + kid: publicHex, + kms: "local", + meta: { derivationPath: derivationPath }, + privateKeyHex: privateHex, + publicKeyHex: publicHex, + type: "Secp256k1", + }, + ], + provider: DEFAULT_DID_PROVIDER_NAME, + services: [], + }; +}; + +export const deriveAddress = ( + mnemonic: string +): [string, string, string, string] => { + const UPORT_ROOT_DERIVATION_PATH = "m/7696500'/0'/0'/0'"; + mnemonic = mnemonic.trim().toLowerCase(); + + const hdnode: HDNode = HDNode.fromMnemonic(mnemonic); + const rootNode: HDNode = hdnode.derivePath(UPORT_ROOT_DERIVATION_PATH); + const privateHex = rootNode.privateKey.substring(2); // original starts with '0x' + const publicHex = rootNode.publicKey.substring(2); // original starts with '0x' + const address = rootNode.address; + + return [address, privateHex, publicHex, UPORT_ROOT_DERIVATION_PATH]; +}; + +/** + * + * + * @return {*} {string} + */ +export const createIdentifier = (): string => { + const entropy: Uint8Array = getRandomBytesSync(32); + const mnemonic = entropyToMnemonic(entropy, wordlist); + + return mnemonic; +}; diff --git a/src/libs/veramo/appSlice.ts b/src/libs/veramo/appSlice.ts new file mode 100644 index 0000000..2b6996a --- /dev/null +++ b/src/libs/veramo/appSlice.ts @@ -0,0 +1,100 @@ +/* import * as R from "ramda"; +import { configureStore, createSlice } from "@reduxjs/toolkit"; +import { IIdentifier } from "@veramo/core"; + +import { Contact } from "../entity/contact"; +import { Settings } from "../entity/settings"; +import * as utility from "../utility/utility"; + +const MAX_LOG_LENGTH = 2000000; + +export const DEFAULT_ENDORSER_API_SERVER = "https://endorser.ch:3000"; +export const DEFAULT_ENDORSER_VIEW_SERVER = "https://endorser.ch"; +export const LOCAL_ENDORSER_API_SERVER = "http://127.0.0.1:3000"; +export const LOCAL_ENDORSER_VIEW_SERVER = "http://127.0.0.1:3001"; +export const TEST_ENDORSER_API_SERVER = "https://test.endorser.ch:8000"; +export const TEST_ENDORSER_VIEW_SERVER = "https://test.endorser.ch:8080"; + +// for contents set in reducers +interface Payload { + type: string; + payload: T; +} + +interface LogMsg { + log: boolean; + msg: string; +} + +export const appSlice = createSlice({ + name: "app", + initialState: { + // This is nullable because it is cached state from the DB... + // it'll be null if we haven't even loaded from the DB yet. + settings: null as Settings, + + // This is nullable because it is cached state from the DB... + // it'll be null if we haven't even loaded from the DB yet. + identifiers: null as Array | null, + + // This is nullable because it is cached state from the DB... + // it'll be null if we haven't even loaded from the DB yet. + contacts: null as Array | null, + + viewServer: DEFAULT_ENDORSER_VIEW_SERVER, + + logMessage: "", + + advancedMode: false, + testMode: false, + }, + reducers: { + addIdentifier: (state, contents: Payload) => { + state.identifiers = state.identifiers.concat([contents.payload]); + }, + addLog: (state, contents: Payload) => { + if (state.logMessage.length > MAX_LOG_LENGTH) { + state.logMessage = + "\n..." + + state.logMessage.substring( + state.logMessage.length - MAX_LOG_LENGTH / 2 + ); + } + if (contents.payload.log) { + console.log(contents.payload.msg); + state.logMessage += "\n" + contents.payload.msg; + } + }, + setAdvancedMode: (state, contents: Payload) => { + state.advancedMode = contents.payload; + }, + setContacts: (state, contents: Payload>) => { + state.contacts = contents.payload; + }, + setContact: (state, contents: Payload) => { + const index = R.findIndex( + (c) => c.did === contents.payload.did, + state.contacts + ); + state.contacts[index] = contents.payload; + }, + setHomeScreen: (state, contents: Payload) => { + state.settings.homeScreen = contents.payload; + }, + setIdentifiers: (state, contents: Payload>) => { + state.identifiers = contents.payload; + }, + setSettings: (state, contents: Payload) => { + state.settings = contents.payload; + }, + setTestMode: (state, contents: Payload) => { + state.testMode = contents.payload; + }, + setViewServer: (state, contents: Payload) => { + state.viewServer = contents.payload; + }, + }, +}); + +export const appStore = configureStore({ reducer: appSlice.reducer }); + */ diff --git a/src/libs/veramo/setup.ts b/src/libs/veramo/setup.ts new file mode 100644 index 0000000..4b3e019 --- /dev/null +++ b/src/libs/veramo/setup.ts @@ -0,0 +1,151 @@ +// Created from the setup in https://veramo.io/docs/guides/react_native + +// Core interfaces +/* import { + createAgent, + IDIDManager, + IResolver, + IDataStore, + IKeyManager, +} from "@veramo/core"; + */ +// Core identity manager plugin +//import { DIDManager } from "@veramo/did-manager"; + +// Ethr did identity provider +//import { EthrDIDProvider } from "@veramo/did-provider-ethr"; + +// Core key manager plugin +//import { KeyManager } from "@veramo/key-manager"; + +// Custom key management system for RN +//import { KeyManagementSystem } from '@veramo/kms-local-react-native' + +// Custom resolver +// Custom resolvers +//import { DIDResolverPlugin } from "@veramo/did-resolver"; +/* import { Resolver } from "did-resolver"; +import { getResolver as ethrDidResolver } from "ethr-did-resolver"; +import { getResolver as webDidResolver } from "web-did-resolver"; + */ +// for VCs and VPs https://veramo.io/docs/api/credential-w3c +//import { CredentialIssuer } from '@veramo/credential-w3c' + +// Storage plugin using TypeOrm +/* import { + Entities, + KeyStore, + DIDStore, + IDataStoreORM, +} from "@veramo/data-store"; + */ +// TypeORM is installed with @veramo/typeorm +//import { createConnection } from 'typeorm' + +//import * as R from "ramda"; + +/* +import { Contact } from '../entity/contact' +import { Settings } from '../entity/settings' +import { PrivateData } from '../entity/privateData' + +import { Initial1616938713828 } from '../migration/1616938713828-initial' +import { SettingsContacts1616967972293 } from '../migration/1616967972293-settings-contacts' +import { EncryptedSeed1637856484788 } from '../migration/1637856484788-EncryptedSeed' +import { HomeScreenConfig1639947962124 } from '../migration/1639947962124-HomeScreenConfig' +import { HandlePublicKeys1652142819353 } from '../migration/1652142819353-HandlePublicKeys' +import { LastClaimsSeen1656811846836 } from '../migration/1656811846836-LastClaimsSeen' +import { ContactRegistered1662256903367 }from '../migration/1662256903367-ContactRegistered' +import { PrivateData1663080623479 } from '../migration/1663080623479-PrivateData' + +const ALL_ENTITIES = Entities.concat([Contact, Settings, PrivateData]) + +// Create react native DB connection configured by ormconfig.js + +export const dbConnection = createConnection({ + database: 'endorser-mobile.sqlite', + entities: ALL_ENTITIES, + location: 'default', + logging: ['error', 'info', 'warn'], + migrations: [ Initial1616938713828, SettingsContacts1616967972293, EncryptedSeed1637856484788, HomeScreenConfig1639947962124, HandlePublicKeys1652142819353, LastClaimsSeen1656811846836, ContactRegistered1662256903367, PrivateData1663080623479 ], + migrationsRun: true, + type: 'react-native', +}) +*/ +function didProviderName(netName: string) { + return "did:ethr" + (netName === "mainnet" ? "" : ":" + netName); +} + +//const NETWORK_NAMES = ["mainnet", "rinkeby"]; + +const DEFAULT_DID_PROVIDER_NETWORK_NAME = "mainnet"; + +export const DEFAULT_DID_PROVIDER_NAME = didProviderName( + DEFAULT_DID_PROVIDER_NETWORK_NAME +); + +export const HANDY_APP = false; + +// this is used as the object in RegisterAction claims +export const SERVICE_ID = "endorser.ch"; + +//const INFURA_PROJECT_ID = "INFURA_PROJECT_ID"; +/* +const providers = {} +NETWORK_NAMES.forEach((networkName) => { + providers[didProviderName(networkName)] = new EthrDIDProvider({ + defaultKms: 'local', + network: networkName, + rpcUrl: 'https://' + networkName + '.infura.io/v3/' + INFURA_PROJECT_ID, + gas: 1000001, + ttl: 60 * 60 * 24 * 30 * 12 + 1, + }) +}) + + +const didManager = new DIDManager({ + store: new DIDStore(dbConnection), + defaultProvider: DEFAULT_DID_PROVIDER_NAME, + providers: providers, +}) +*/ + +/* const basicDidResolvers = NETWORK_NAMES.map((networkName) => [ + networkName, + new Resolver({ + ethr: ethrDidResolver({ + networks: [ + { + name: networkName, + rpcUrl: + "https://" + networkName + ".infura.io/v3/" + INFURA_PROJECT_ID, + }, + ], + }).ethr, + web: webDidResolver().web, + }), +]); + +const basicResolverMap = R.fromPairs(basicDidResolvers) + +export const DEFAULT_BASIC_RESOLVER = basicResolverMap[DEFAULT_DID_PROVIDER_NETWORK_NAME] + +const agentDidResolvers = NETWORK_NAMES.map((networkName) => { + return new DIDResolverPlugin({ + resolver: basicResolverMap[networkName], + }) +}) + +let allPlugins = [ + new CredentialIssuer(), + new KeyManager({ + store: new KeyStore(dbConnection), + kms: { + local: new KeyManagementSystem(), + }, + }), + didManager, +].concat(agentDidResolvers) +*/ + +//export const agent = createAgent({ plugins: allPlugins }) diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index 98673d0..f9e0092 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -167,77 +167,62 @@