diff --git a/examples/expressjs-ethr/src/setup.ts b/examples/expressjs-ethr/src/setup.ts index b861c4dc6..04e07527d 100644 --- a/examples/expressjs-ethr/src/setup.ts +++ b/examples/expressjs-ethr/src/setup.ts @@ -3,7 +3,7 @@ import { DafUniversalResolver } from 'daf-resolver-universal' import * as Daf from 'daf-core' import * as DidJwt from 'daf-did-jwt' -import { EthrDidFsController } from 'daf-ethr-did-fs' +import { IdentityProvider } from 'daf-ethr-did-fs' import * as W3c from 'daf-w3c' import * as SD from 'daf-selective-disclosure' @@ -43,7 +43,14 @@ if (process.env.DAF_UNIVERSAL_RESOLVER_URL) { }) } -const identityControllers = [new EthrDidFsController(identityStoreFilename)] +const identityProviders = [ + new IdentityProvider({ + fileName: identityStoreFilename, + network: 'rinkeby', + rpcUrl: 'https://rinkeby.infura.io/v3/' + infuraProjectId, + resolver: didResolver, + }), +] const serviceControllers = [TG.ServiceController] const messageValidator = new DBG.MessageValidator() @@ -62,7 +69,7 @@ actionHandler .setNext(new SD.ActionHandler()) export const core = new Daf.Core({ - identityControllers, + identityProviders, serviceControllers, didResolver, messageValidator, diff --git a/examples/expressjs-ethr/src/web-server.ts b/examples/expressjs-ethr/src/web-server.ts index 173431bd4..8edc04168 100644 --- a/examples/expressjs-ethr/src/web-server.ts +++ b/examples/expressjs-ethr/src/web-server.ts @@ -51,15 +51,14 @@ io.on('connection', function(socket) { async function main() { await dataStore.initialize() - // Get of create new issuer - let issuer: Daf.Issuer - const issuers = await core.identityManager.listIssuers() - if (issuers.length > 0) { - issuer = issuers[0] + // Get of create new identity + let identity: Daf.AbstractIdentity + const identities = await core.identityManager.getIdentities() + if (identities.length > 0) { + identity = identities[0] } else { - const types = await core.identityManager.listTypes() - const did = await core.identityManager.create(types[0]) - issuer = await core.identityManager.issuer(did) + const identityProviders = await core.identityManager.getIdentityProviderTypes() + identity = await core.identityManager.createIdentity(identityProviders[0].type) } app.get('/', async function(req, res) { @@ -83,7 +82,7 @@ async function main() { if (!did) { const signAction: SD.ActionSignSdr = { type: SD.ActionTypes.signSdr, - did: issuer.did, + did: identity.did, data: { tag: req.sessionID, claims: [ diff --git a/examples/react-app/src/App.tsx b/examples/react-app/src/App.tsx index 4e76cd320..293011d55 100644 --- a/examples/react-app/src/App.tsx +++ b/examples/react-app/src/App.tsx @@ -29,15 +29,17 @@ const App: React.FC = () => { const [receiver, setReceiver] = useState('did:web:uport.me') const [claimType, setClaimType] = useState('name') const [claimValue, setClaimValue] = useState('Alice') - const [controllerTypes, setControllerTypes] = useState(['']) - const [identities, setIdentities] = useState([{ type: '', did: '' }]) + const [identityProviders, setIdentityProviders] = useState([{ type: '', description: '' }]) + const [identities, setIdentities] = useState([{ identityProviderType: '', did: '' }]) useEffect(() => { - setControllerTypes(core.identityManager.listTypes()) + core.identityManager.getIdentityProviderTypes().then((providers: any) => { + setIdentityProviders(providers) + }) }, []) const updateIdentityList = () => { - core.identityManager.listIssuers().then(identities => { + core.identityManager.getIdentities().then((identities: any) => { setIdentities(identities) if (identities.length > 0) setActiveDid(identities[0].did) }) @@ -101,18 +103,18 @@ const App: React.FC = () => { Send Verifiable Credential - {controllerTypes.map(type => ( + {identityProviders.map(identityProvider => ( { - await core.identityManager.create(type) + await core.identityManager.createIdentity(identityProvider.type) updateIdentityList() }} > - Create {type} DID + Create {identityProvider.type} DID ))} @@ -125,7 +127,7 @@ const App: React.FC = () => { name="selecedDid" required onChange={(e: any) => setActiveDid(identity.did)} - label={`${identity.did} (${identity.type})`} + label={`${identity.did} (${identity.identityProviderType})`} value={identity.did} checked={activeDid === identity.did} /> diff --git a/examples/react-app/src/setup.ts b/examples/react-app/src/setup.ts index 8b3cfb7bd..2abe6863e 100644 --- a/examples/react-app/src/setup.ts +++ b/examples/react-app/src/setup.ts @@ -16,15 +16,17 @@ messageValidator.setNext(new DidJwt.MessageValidator()).setNext(new W3c.MessageV const actionHandler = new DBG.ActionHandler() actionHandler.setNext(new TG.ActionHandler()).setNext(new W3c.ActionHandler()) -const identityControllers: Daf.IdentityController[] = [new EthrDidLocalStorageController()] +const identityProviders: Daf.AbstractIdentityProvider[] = [ + // new EthrDidLocalStorageController() +] if (typeof window.ethereum !== 'undefined' && window.ethereum.isMetaMask) { EthrDidMetamaskController.snapId = 'http://localhost:8082/package.json' - identityControllers.push(new EthrDidMetamaskController()) + // identityProviders.push(new EthrDidMetamaskController()) } export const core = new Daf.Core({ - identityControllers, + identityProviders, serviceControllers: [], didResolver: new DafUniversalResolver({ url: 'https://uniresolver.io/1.0/identifiers/' }), messageValidator, diff --git a/examples/react-graphql/server/index.ts b/examples/react-graphql/server/index.ts index 3bbd1ec01..5698fc345 100644 --- a/examples/react-graphql/server/index.ts +++ b/examples/react-graphql/server/index.ts @@ -7,17 +7,25 @@ import * as TG from 'daf-trust-graph' import * as DBG from 'daf-debug' import * as URL from 'daf-url' import { NodeSqlite3 } from 'daf-node-sqlite3' -import { EthrDidFsController } from 'daf-ethr-did-fs' +import { IdentityProvider } from 'daf-ethr-did-fs' import { DafResolver } from 'daf-resolver' import { ApolloServer } from 'apollo-server' import merge from 'lodash.merge' import ws from 'ws' TG.ServiceController.webSocketImpl = ws +const infuraProjectId = '5ffc47f65c4042ce847ef66a3fa70d4c' -let didResolver = new DafResolver({ infuraProjectId: '5ffc47f65c4042ce847ef66a3fa70d4c' }) +let didResolver = new DafResolver({ infuraProjectId }) -const identityControllers = [new EthrDidFsController('./identity-store.json')] +const identityProviders = [ + new IdentityProvider({ + fileName: './identity-store.json', + network: 'rinkeby', + rpcUrl: 'https://rinkeby.infura.io/v3/' + infuraProjectId, + resolver: didResolver, + }), +] const serviceControllers = [TG.ServiceController] const messageValidator = new DBG.MessageValidator() @@ -34,7 +42,7 @@ actionHandler .setNext(new SD.ActionHandler()) export const core = new Daf.Core({ - identityControllers, + identityProviders, serviceControllers, didResolver, messageValidator, diff --git a/examples/send-vc/index.ts b/examples/send-vc/index.ts index 28cdf205a..b00fcb4fb 100644 --- a/examples/send-vc/index.ts +++ b/examples/send-vc/index.ts @@ -15,30 +15,40 @@ messageValidator.setNext(new DidJwt.MessageValidator()).setNext(new W3c.MessageV const actionHandler = new DBG.ActionHandler() actionHandler.setNext(new TG.ActionHandler()).setNext(new W3c.ActionHandler()) +const infuraProjectId = '5ffc47f65c4042ce847ef66a3fa70d4c' + +const didResolver = new DafUniversalResolver({ url: 'https://uniresolver.io/1.0/identifiers/' }) + export const core = new Daf.Core({ - identityControllers: [new Ethr.EthrDidFsController('./identity-store.json')], + identityProviders: [ + new Ethr.IdentityProvider({ + fileName: './identity-store.json', + network: 'rinkeby', + rpcUrl: 'https://rinkeby.infura.io/v3/' + infuraProjectId, + resolver: didResolver, + }), + ], serviceControllers: [], - didResolver: new DafUniversalResolver({ url: 'https://uniresolver.io/1.0/identifiers/' }), + didResolver, messageValidator, actionHandler, }) async function main() { // Get or create new issuer - let issuer: Daf.Issuer - const issuers = await core.identityManager.listIssuers() - if (issuers.length > 0) { - issuer = issuers[0] + let identity: Daf.AbstractIdentity + const identities = await core.identityManager.getIdentities() + if (identities.length > 0) { + identity = identities[0] } else { - const types = core.identityManager.listTypes() - const did = await core.identityManager.create(types[0]) - issuer = await core.identityManager.issuer(did) + const identityProviders = core.identityManager.getIdentityProviderTypes() + identity = await core.identityManager.createIdentity(identityProviders[0].type) } // Sign credential const credential = await core.handleAction({ type: W3c.ActionTypes.signVc, - did: issuer.did, + did: identity.did, data: { sub: 'did:web:uport.me', vc: { @@ -55,7 +65,7 @@ async function main() { await core.handleAction({ type: TG.ActionTypes.sendJwt, data: { - from: issuer.did, + from: identity.did, to: 'did:web:uport.me', jwt: credential, }, diff --git a/packages/daf-cli/src/identity-manager.ts b/packages/daf-cli/src/identity-manager.ts index 2280daa0d..671056dcb 100644 --- a/packages/daf-cli/src/identity-manager.ts +++ b/packages/daf-cli/src/identity-manager.ts @@ -10,6 +10,8 @@ program .option('-t, --types', 'List available identity controller types') .option('-c, --create', 'Create identity') .option('-d, --delete', 'Delete identity') + .option('-s, --service', 'Add service endpoint') + .option('-p, --publicKey', 'Add public key') .action(async cmd => { if (cmd.types) { const list = await core.identityManager.getIdentityProviderTypes() @@ -71,4 +73,67 @@ program console.error(e) } } + + if (cmd.service) { + try { + const identities = await core.identityManager.getIdentities() + const answers = await inquirer.prompt([ + { + type: 'list', + name: 'did', + choices: identities.map(item => item.did), + message: 'Select DID', + }, + { + type: 'text', + name: 'type', + message: 'Service type', + default: 'Messaging', + }, + { + type: 'text', + name: 'endpoint', + message: 'Endpoint', + }, + ]) + + const identity = await core.identityManager.getIdentity(answers.did) + const provider = await core.identityManager.getIdentityProvider(identity.identityProviderType) + const result = await provider.addService(identity.did, { + type: answers.type, + serviceEndpoint: answers.endpoint, + id: '', + }) + console.log('Success:', result) + } catch (e) { + console.error(e) + } + } + + if (cmd.publicKey) { + try { + const identities = await core.identityManager.getIdentities() + const answers = await inquirer.prompt([ + { + type: 'list', + name: 'did', + choices: identities.map(item => item.did), + message: 'Select DID', + }, + { + type: 'list', + name: 'type', + choices: ['Ed25519', 'Secp256k1'], + message: 'Type', + }, + ]) + + const identity = await core.identityManager.getIdentity(answers.did) + const provider = await core.identityManager.getIdentityProvider(identity.identityProviderType) + const result = await provider.addPublicKey(identity.did, answers.type) + console.log('Success:', result) + } catch (e) { + console.error(e) + } + } }) diff --git a/packages/daf-cli/src/setup.ts b/packages/daf-cli/src/setup.ts index a5a657c55..0614d5460 100644 --- a/packages/daf-cli/src/setup.ts +++ b/packages/daf-cli/src/setup.ts @@ -21,7 +21,6 @@ const debug = Debug('daf:cli') const defaultPath = process.env.HOME + '/.daf' -const identityStoreFilename = process.env.DAF_IDENTITY_STORE ?? defaultPath + '/identity-store-jwk.json' const dataStoreFilename = process.env.DAF_DATA_STORE ?? defaultPath + '/data-store-cli.sqlite3' const infuraProjectId = process.env.DAF_INFURA_ID ?? '5ffc47f65c4042ce847ef66a3fa70d4c' @@ -49,11 +48,17 @@ TG.ServiceController.webSocketImpl = ws const identityProviders = [ new EthrDidFs.IdentityProvider({ - fileName: identityStoreFilename, + fileName: defaultPath + 'rinkeby-identity-store.json', network: 'rinkeby', rpcUrl: 'https://rinkeby.infura.io/v3/' + infuraProjectId, resolver: didResolver, }), + new EthrDidFs.IdentityProvider({ + fileName: defaultPath + 'kovan-identity-store.json', + network: 'kovan', + rpcUrl: 'https://kovan.infura.io/v3/' + infuraProjectId, + resolver: didResolver, + }), ] const serviceControllers = [TG.ServiceController] diff --git a/packages/daf-core/src/index.ts b/packages/daf-core/src/index.ts index 36a80fe2a..c6adda262 100644 --- a/packages/daf-core/src/index.ts +++ b/packages/daf-core/src/index.ts @@ -3,7 +3,7 @@ export { AbstractActionHandler } from './action/action-handler' export { EncryptionKeyManager, KeyPair } from './encryption-manager' export { IdentityManager } from './identity/identity-manager' export { AbstractIdentity } from './identity/abstract-identity' -export { AbstractIdentityProvider } from './identity/abstract-identity-provider' +export { AbstractIdentityProvider, IdentityProviderDerived } from './identity/abstract-identity-provider' export { AbstractMessageValidator } from './message/abstract-message-validator' export { Message } from './message/message' export { ServiceManager, LastMessageTimestampForInstance, ServiceEventTypes } from './service/service-manager' diff --git a/packages/daf-ethr-did-fs/package-lock.json b/packages/daf-ethr-did-fs/package-lock.json new file mode 100644 index 000000000..a34d119f9 --- /dev/null +++ b/packages/daf-ethr-did-fs/package-lock.json @@ -0,0 +1,69 @@ +{ + "name": "daf-ethr-did-fs", + "version": "1.4.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" + }, + "elliptic": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz", + "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==", + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "js-sha3": { + "version": "0.8.0" + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" + } + } +} diff --git a/packages/daf-ethr-did-fs/package.json b/packages/daf-ethr-did-fs/package.json index 41def8f1e..26527a616 100644 --- a/packages/daf-ethr-did-fs/package.json +++ b/packages/daf-ethr-did-fs/package.json @@ -12,15 +12,17 @@ "daf-core": "^1.4.1", "daf-resolver": "^1.1.0", "debug": "^4.1.1", + "elliptic": "^6.5.2", "ethjs-provider-signer": "^0.1.4", "ethjs-signer": "^0.1.1", "ethr-did": "^1.1.0", - "jose": "^1.22.1", - "js-sha3": "^0.8.0" + "js-sha3": "^0.8.0", + "libsodium-wrappers": "^0.7.6" }, "devDependencies": { "@types/debug": "^4.1.5", "@types/ethjs-signer": "^0.1.0", + "@types/libsodium-wrappers": "^0.7.7", "ethjs-provider-signer": "^0.1.4", "ethjs-signer": "^0.1.1", "typescript": "^3.7.2" diff --git a/packages/daf-ethr-did-fs/src/__tests__/identity-provider.test.ts b/packages/daf-ethr-did-fs/src/__tests__/identity-provider.test.ts index 2adcfcbb7..662847d40 100644 --- a/packages/daf-ethr-did-fs/src/__tests__/identity-provider.test.ts +++ b/packages/daf-ethr-did-fs/src/__tests__/identity-provider.test.ts @@ -9,6 +9,18 @@ describe('daf-ethr-did-fs', () => { const infuraProjectId = '5ffc47f65c4042ce847ef66a3fa70d4c' const rpcUrl = 'https://rinkeby.infura.io/v3/' + infuraProjectId const resolver = new DafResolver({ infuraProjectId }) + const key = { + privateKey: 'da1ed1d75b6e3d28d306af4dcab9b893189cf248e52fe526e264b39b5e587ccf', + publicKey: '', + address: '0x76d331386cec35862a73aabdbfa5ef97cdac58cf', + type: 'Secp256k1', + } + + const serialized = { + did: 'did:ethr:rinkeby:0x76d331386cec35862a73aabdbfa5ef97cdac58cf', + controller: key, + keys: [key], + } const fileName = './store.json' const network = 'rinkeby' @@ -45,28 +57,35 @@ describe('daf-ethr-did-fs', () => { }) // it('imported identity adds serviceEndpoint', async () => { - // const serialized = { - // did: 'did:ethr:rinkeby:0xf09b1640417a4270b3631306b42403fa8c45d63d', - // keySet: { - // keys: [ - // { - // crv: 'secp256k1', - // x: 'z93SgW9Dagt89Sts0NEIN7XMPpHli5Vr1n8nZ97-Ae4', - // y: 'HracHTExUzEuhZm7auhpDgVJRGYumwhWZHdrNAvzogk', - // d: '', //@TODO: figure out how to do this without revealing private keys - // kty: 'EC', - // kid: 'yAfcGZwuMFCvi_PZmCs7WVihA-ZAnyr0LCoafnORGk4', - // }, - // ], - // }, - // } + // const identity = await identityProvider.importIdentity(JSON.stringify(serialized)) // const result = await identityProvider.addService(identity.did, { - // id: 'srvc4', + // id: 'srvc5', // type: 'Messaging', - // serviceEndpoint: 'https://localhos:5000/msg', + // serviceEndpoint: 'https://localhos/msg/6789', // }) + // console.log(result) + + // expect(result).toBeTruthy() + // }) + + // it('imported identity adds Secp256k1 publicKey', async () => { + + // const identity = await identityProvider.importIdentity(JSON.stringify(serialized)) + + // const result = await identityProvider.addPublicKey(identity.did, 'Secp256k1') + // console.log(result) + + // expect(result).toBeTruthy() + // }) + + // it('imported identity adds Ed25519 publicKey', async () => { + + // const identity = await identityProvider.importIdentity(JSON.stringify(serialized)) + + // const result = await identityProvider.addPublicKey(identity.did, 'Ed25519') + // console.log(result) // expect(result).toBeTruthy() // }) diff --git a/packages/daf-ethr-did-fs/src/daf-jose.ts b/packages/daf-ethr-did-fs/src/daf-jose.ts deleted file mode 100644 index ebeef67f7..000000000 --- a/packages/daf-ethr-did-fs/src/daf-jose.ts +++ /dev/null @@ -1,30 +0,0 @@ -import base64url from 'base64url' -import { keccak_256 } from 'js-sha3' -import { sha256 as sha256js, Message } from 'js-sha256' -import * as jose from 'jose' - -export function sha256(payload: Message): Buffer { - return Buffer.from(sha256js.arrayBuffer(payload)) -} - -function keccak(data: any): Buffer { - return Buffer.from(keccak_256.arrayBuffer(data)) -} -export function toEthereumAddress(hexPublicKey: string): string { - return `0x${keccak(Buffer.from(hexPublicKey.slice(2), 'hex')) - .slice(-20) - .toString('hex')}` -} - -export function convertECKeyToEthHexKeys(key: jose.JWK.ECKey) { - const bx = base64url.toBuffer(key.x) - const by = base64url.toBuffer(key.y) - const hexPrivateKey = key.d ? base64url.toBuffer(key.d).toString('hex') : '' - const hexPublicKey = '04' + Buffer.concat([bx, by], 64).toString('hex') - const address = toEthereumAddress(hexPublicKey) - return { - hexPrivateKey, - hexPublicKey, - address, - } -} diff --git a/packages/daf-ethr-did-fs/src/ethr-identity.ts b/packages/daf-ethr-did-fs/src/ethr-identity.ts index 52a6672b8..5cd8b4bce 100644 --- a/packages/daf-ethr-did-fs/src/ethr-identity.ts +++ b/packages/daf-ethr-did-fs/src/ethr-identity.ts @@ -1,49 +1,25 @@ import { AbstractIdentity, Resolver } from 'daf-core' import { SimpleSigner } from 'did-jwt' -import * as jose from 'jose' -import { convertECKeyToEthHexKeys } from './daf-jose' +import { Key } from './identity-provider' export class EthrIdentity extends AbstractIdentity { public readonly did: string public readonly identityProviderType: string - private readonly keyStore: jose.JWKS.KeyStore + private readonly keys: Key[] private readonly resolver: Resolver - constructor(options: { - identityProviderType: string - did: string - keyStore: jose.JWKS.KeyStore - resolver: Resolver - }) { + constructor(options: { identityProviderType: string; did: string; keys: Key[]; resolver: Resolver }) { super() this.did = options.did this.identityProviderType = options.identityProviderType - this.keyStore = options.keyStore + this.keys = options.keys this.resolver = options.resolver } public signer(keyId?: string) { - const key = this.keyStore.get({ kty: 'EC', crv: 'secp256k1' }) as jose.JWK.ECKey - const { hexPrivateKey } = convertECKeyToEthHexKeys(key) - return SimpleSigner(hexPrivateKey) - - // @TODO: Use jose for signer - - // return async (data: any) => { - // const test = jose.JWS.sign(data, key) - - // console.log(test) - - // const r = Buffer.from('r') - // const s = Buffer.from('s') - // const recoveryParam = 1 - - // return { - // r: leftpad(r.toString('hex')), - // s: leftpad(s.toString('hex')), - // recoveryParam - // } - // } + const key = this.keys.find(item => item.type === 'Secp256k1') + if (!key) throw Error('[ethr-identity] Key not found') + return SimpleSigner(key.privateKey) } async didDoc() { diff --git a/packages/daf-ethr-did-fs/src/identity-provider.ts b/packages/daf-ethr-did-fs/src/identity-provider.ts index 7905bf69f..9a2e89b14 100644 --- a/packages/daf-ethr-did-fs/src/identity-provider.ts +++ b/packages/daf-ethr-did-fs/src/identity-provider.ts @@ -1,19 +1,42 @@ import { AbstractIdentityProvider, AbstractIdentity, Resolver } from 'daf-core' -import * as jose from 'jose' -import { convertECKeyToEthHexKeys } from './daf-jose' - import { EthrIdentity } from './ethr-identity' import { sign } from 'ethjs-signer' const SignerProvider = require('ethjs-provider-signer') - const EthrDID = require('ethr-did') const fs = require('fs') import Debug from 'debug' const debug = Debug('daf:ethr-did-fs:identity-provider') +const EC = require('elliptic').ec +const secp256k1 = new EC('secp256k1') +import sodium from 'libsodium-wrappers' + +import { keccak_256 } from 'js-sha3' +import { sha256 as sha256js, Message } from 'js-sha256' + +export function sha256(payload: Message): Buffer { + return Buffer.from(sha256js.arrayBuffer(payload)) +} + +function keccak(data: any): Buffer { + return Buffer.from(keccak_256.arrayBuffer(data)) +} +export function toEthereumAddress(hexPublicKey: string): string { + return `0x${keccak(Buffer.from(hexPublicKey.slice(2), 'hex')) + .slice(-20) + .toString('hex')}` +} + +export interface Key { + type: string + privateKey: string + publicKey: string + address?: string +} interface SerializedIdentity { did: string - keySet: jose.JSONWebKeySet + controller: Key + keys: Key[] } interface FileContents { @@ -60,17 +83,10 @@ export class IdentityProvider extends AbstractIdentityProvider { return identity } - private getEthKeysFromSerialized(serialized: SerializedIdentity) { - const keyStore = jose.JWKS.asKeyStore(serialized.keySet) - const key = keyStore.get({ kty: 'EC', crv: 'secp256k1' }) as jose.JWK.ECKey - if (!key) throw Error('Key not found') - return convertECKeyToEthHexKeys(key) - } - private identityFromSerialized(serialized: SerializedIdentity): AbstractIdentity { return new EthrIdentity({ did: serialized.did, - keyStore: jose.JWKS.asKeyStore(serialized.keySet), + keys: serialized.keys, identityProviderType: this.type, resolver: this.resolver, }) @@ -86,12 +102,22 @@ export class IdentityProvider extends AbstractIdentityProvider { } async createIdentity() { - const key = jose.JWK.generateSync('EC', 'secp256k1') - const hexKeys = convertECKeyToEthHexKeys(key) + const kp = secp256k1.genKeyPair() + const publicKey = kp.getPublic('hex') + const privateKey = kp.getPrivate('hex') + const address = toEthereumAddress(publicKey) + + const key = { + privateKey, + publicKey, + address, + type: 'Secp256k1', + } const serialized = { - did: 'did:ethr:' + this.network + ':' + hexKeys.address, - keySet: new jose.JWKS.KeyStore([key]).toJWKS(true), + did: 'did:ethr:' + this.network + ':' + address, + controller: key, + keys: [key], } this.saveIdentity(serialized) @@ -134,11 +160,67 @@ export class IdentityProvider extends AbstractIdentityProvider { service: { id: string; type: string; serviceEndpoint: string }, ): Promise { const serialized = await this.getSerializedIdentity(did) - const hexKeys = this.getEthKeysFromSerialized(serialized) + + const provider = new SignerProvider(this.rpcUrl, { + signTransaction: (rawTx: any, cb: any) => + cb(null, sign(rawTx, '0x' + serialized.controller.privateKey)), + }) + const ethrDid = new EthrDID({ address: serialized.controller.address, provider }) + + const attribute = 'did/svc/' + service.type + const value = service.serviceEndpoint + const ttl = 86400 + const gas = 100000 + debug('ethrDid.setAttribute', { attribute, value, ttl, gas }) + const txHash = await ethrDid.setAttribute(attribute, value, ttl, gas) + debug({ txHash }) + return txHash + } + + async addPublicKey(did: string, type: 'Ed25519' | 'Secp256k1', proofPurpose?: string[]): Promise { + const serialized = await this.getSerializedIdentity(did) + const provider = new SignerProvider(this.rpcUrl, { - signTransaction: (rawTx: any, cb: any) => cb(null, sign(rawTx, '0x' + hexKeys.hexPrivateKey)), + signTransaction: (rawTx: any, cb: any) => + cb(null, sign(rawTx, '0x' + serialized.controller.privateKey)), }) - const ethrDid = new EthrDID({ address: hexKeys.address, provider }) - return ethrDid.setAttribute('did/svc/' + service.type, service.serviceEndpoint, 86400, 100000) + const ethrDid = new EthrDID({ address: serialized.controller.address, provider }) + + let usg = '' + let publicKey + let privateKey + let address + switch (type) { + case 'Ed25519': + await sodium.ready + const keyPair = sodium.crypto_sign_keypair() + privateKey = Buffer.from(keyPair.privateKey).toString('hex') + publicKey = Buffer.from(keyPair.publicKey).toString('hex') + usg = 'veriKey' + break + case 'Secp256k1': + const kp = secp256k1.genKeyPair() + publicKey = kp.getPublic('hex') + privateKey = kp.getPrivate('hex') + address = toEthereumAddress(publicKey) + usg = 'veriKey' + break + } + + const attribute = 'did/pub/' + type + '/' + usg + '/hex' + const value = '0x' + publicKey + const ttl = 86400 + const gas = 100000 + debug('ethrDid.setAttribute', { attribute, value, ttl, gas }) + const txHash = await ethrDid.setAttribute(attribute, value, ttl, gas) + + if (txHash) { + debug({ txHash }) + serialized.keys = [...serialized.keys, { privateKey, publicKey, address, type }] + this.saveIdentity(serialized) + return true + } + + return false } } diff --git a/packages/daf-ethr-did-local-storage/src/identity-controller.ts b/packages/daf-ethr-did-local-storage/src/identity-controller.ts index 48d10d171..cb275ec70 100644 --- a/packages/daf-ethr-did-local-storage/src/identity-controller.ts +++ b/packages/daf-ethr-did-local-storage/src/identity-controller.ts @@ -1,5 +1,5 @@ import Debug from 'debug' -import { IdentityController, Issuer } from 'daf-core' +import { AbstractIdentityProvider, AbstractIdentity } from 'daf-core' const EthrDID = require('ethr-did').default const debug = Debug('daf:ethr-did-local-storage:identity-controller') @@ -13,98 +13,98 @@ interface StorageContents { identities: Identity[] } -export class EthrDidLocalStorageController implements IdentityController { +export class EthrDidLocalStorageController { public type = 'ethr-did-local-storage' - private readFromStorage(): StorageContents { - try { - const raw = window.localStorage.getItem(this.type) || '' - return JSON.parse(raw) as StorageContents - } catch (e) { - return { identities: [] } - } - } - - private writeToStorage(json: StorageContents) { - return window.localStorage.setItem(this.type, JSON.stringify(json)) - } - - private issuerFromIdentity(identity: Identity): Issuer { - const ethrDid = new EthrDID({ ...identity }) - const issuer: Issuer = { - did: identity.did, - signer: ethrDid.signer, - type: this.type, - ethereumAddress: ethrDid.address, - } - return issuer - } - - async listDids() { - const { identities } = this.readFromStorage() - return identities.map(identity => identity.did) - } - - async listIssuers() { - const { identities } = this.readFromStorage() - return identities.map(this.issuerFromIdentity.bind(this)) as Issuer[] - } - - async create() { - const { identities } = this.readFromStorage() - const keyPair = EthrDID.createKeyPair() - const ethrDid = new EthrDID({ ...keyPair }) - - this.writeToStorage({ - identities: [ - ...identities, - { - did: ethrDid.did, - address: ethrDid.address, - privateKey: keyPair.privateKey, - }, - ], - }) - - debug('Created', ethrDid.did) - - return ethrDid.did - } - - async delete(did: string) { - const { identities } = this.readFromStorage() - - if (!identities.find(identity => identity.did === did)) { - return false - } - - const filteredIdentities = identities.filter(identity => identity.did !== did) - - this.writeToStorage({ identities: filteredIdentities }) - - debug('Deleted', did) - - return true - } - - async issuer(did: string) { - const { identities } = this.readFromStorage() - - const identity = identities.find(identity => identity.did === did) - if (!identity) { - return Promise.reject('Did not found: ' + did) - } - - return this.issuerFromIdentity(identity) - } - - async export(did: string) { - const { identities } = this.readFromStorage() - - const identity = identities.find(identity => identity.did === did) - if (!identity) { - return Promise.reject('Did not found: ' + did) - } - return identity.privateKey - } + // private readFromStorage(): StorageContents { + // try { + // const raw = window.localStorage.getItem(this.type) || '' + // return JSON.parse(raw) as StorageContents + // } catch (e) { + // return { identities: [] } + // } + // } + + // private writeToStorage(json: StorageContents) { + // return window.localStorage.setItem(this.type, JSON.stringify(json)) + // } + + // private issuerFromIdentity(identity: Identity): Issuer { + // const ethrDid = new EthrDID({ ...identity }) + // const issuer: Issuer = { + // did: identity.did, + // signer: ethrDid.signer, + // type: this.type, + // ethereumAddress: ethrDid.address, + // } + // return issuer + // } + + // async listDids() { + // const { identities } = this.readFromStorage() + // return identities.map(identity => identity.did) + // } + + // async listIssuers() { + // const { identities } = this.readFromStorage() + // return identities.map(this.issuerFromIdentity.bind(this)) as Issuer[] + // } + + // async create() { + // const { identities } = this.readFromStorage() + // const keyPair = EthrDID.createKeyPair() + // const ethrDid = new EthrDID({ ...keyPair }) + + // this.writeToStorage({ + // identities: [ + // ...identities, + // { + // did: ethrDid.did, + // address: ethrDid.address, + // privateKey: keyPair.privateKey, + // }, + // ], + // }) + + // debug('Created', ethrDid.did) + + // return ethrDid.did + // } + + // async delete(did: string) { + // const { identities } = this.readFromStorage() + + // if (!identities.find(identity => identity.did === did)) { + // return false + // } + + // const filteredIdentities = identities.filter(identity => identity.did !== did) + + // this.writeToStorage({ identities: filteredIdentities }) + + // debug('Deleted', did) + + // return true + // } + + // async issuer(did: string) { + // const { identities } = this.readFromStorage() + + // const identity = identities.find(identity => identity.did === did) + // if (!identity) { + // return Promise.reject('Did not found: ' + did) + // } + + // return this.issuerFromIdentity(identity) + // } + + // async export(did: string) { + // const { identities } = this.readFromStorage() + + // const identity = identities.find(identity => identity.did === did) + // if (!identity) { + // return Promise.reject('Did not found: ' + did) + // } + // return identity.privateKey + // } } diff --git a/packages/daf-ethr-did-metamask/src/identity-controller.ts b/packages/daf-ethr-did-metamask/src/identity-provider.ts similarity index 51% rename from packages/daf-ethr-did-metamask/src/identity-controller.ts rename to packages/daf-ethr-did-metamask/src/identity-provider.ts index 77ccfce5b..bb04ca9a9 100644 --- a/packages/daf-ethr-did-metamask/src/identity-controller.ts +++ b/packages/daf-ethr-did-metamask/src/identity-provider.ts @@ -1,32 +1,52 @@ import Debug from 'debug' -import { IdentityController, Issuer } from 'daf-core' -const debug = Debug('daf:ethr-did-metamask:identity-controller') +import { AbstractIdentityProvider, AbstractIdentity } from 'daf-core' +const debug = Debug('daf:ethr-did-metamask:identity-provider') declare global { interface Window { ethereum: any } } -export class EthrDidMetamaskController implements IdentityController { - static snapId = '' - public type = 'ethr-did-metamask' - async listDids() { - const did = await this.getDidFromMetamask() - return [did] +export class MetamaskIdentity extends AbstractIdentity { + public readonly did: string + public readonly identityProviderType: string + private readonly sign: any + + constructor(options: { identityProviderType: string; did: string; sign: any }) { + super() + this.did = options.did + this.identityProviderType = options.identityProviderType + this.sign = options.sign } - async listIssuers() { + public signer(keyId?: string) { + return this.sign + } + + async didDoc() { + return Promise.reject('not implemented') + } + async encrypt(): Promise {} + async decrypt(): Promise {} +} + +export class IdentityProvider extends AbstractIdentityProvider { + static snapId = '' + public type = 'ethr-did-metamask' + public description = 'metamask snap identity' + + async getIdentities() { try { - const issuer = await this.issuer(await this.getDidFromMetamask()) - return [issuer] + const identity = await this.getIdentity(await this.getDidFromMetamask()) + return [identity] } catch (e) { debug(e) return [] } } - async issuer(did: string) { + async getIdentity(did: string) { const metamaskDid = await this.getDidFromMetamask() if (did !== metamaskDid) { @@ -35,13 +55,12 @@ export class EthrDidMetamaskController implements IdentityController { const address = await this.getAddressFromMetamask() - const issuer: Issuer = { + const identity = new MetamaskIdentity({ did: metamaskDid, - signer: this.signer, - type: this.type, - ethereumAddress: address, - } - return issuer + sign: this.signer, + identityProviderType: this.type, + }) + return identity } private async signer(data: string): Promise { @@ -49,7 +68,7 @@ export class EthrDidMetamaskController implements IdentityController { return await window.ethereum.send({ method: 'wallet_invokePlugin', params: [ - EthrDidMetamaskController.snapId, + IdentityProvider.snapId, { method: 'sign', params: [data], @@ -71,7 +90,7 @@ export class EthrDidMetamaskController implements IdentityController { return await window.ethereum.send({ method: 'wallet_invokePlugin', params: [ - EthrDidMetamaskController.snapId, + IdentityProvider.snapId, { method: 'address', }, @@ -79,13 +98,13 @@ export class EthrDidMetamaskController implements IdentityController { }) } - async create() { + async createIdentity() { try { await window.ethereum.send({ method: 'wallet_enable', params: [ { - wallet_plugin: { [EthrDidMetamaskController.snapId]: {} }, + wallet_plugin: { [IdentityProvider.snapId]: {} }, }, ], }) @@ -93,15 +112,14 @@ export class EthrDidMetamaskController implements IdentityController { const did = await this.getDidFromMetamask() debug('Created', did) - return did + return this.getIdentity(did) } catch (e) { debug(e) return Promise.reject('Metamask plugin not enabled') } } - async delete(did: string) { - debug('Delete not supported') - return Promise.reject('Delete not supported ') + async deleteIdentity(did: string) { + return Promise.reject('not impemented') } } diff --git a/packages/daf-ethr-did-metamask/src/index.ts b/packages/daf-ethr-did-metamask/src/index.ts index afc4a8e57..531854f24 100644 --- a/packages/daf-ethr-did-metamask/src/index.ts +++ b/packages/daf-ethr-did-metamask/src/index.ts @@ -1 +1 @@ -export { EthrDidMetamaskController } from './identity-controller' +export { IdentityProvider } from './identity-provider' diff --git a/packages/daf-ethr-did-react-native/src/identity-provider.ts b/packages/daf-ethr-did-react-native/src/identity-provider.ts new file mode 100644 index 000000000..42bcd9f27 --- /dev/null +++ b/packages/daf-ethr-did-react-native/src/identity-provider.ts @@ -0,0 +1,104 @@ +import { AbstractIdentityProvider, AbstractIdentity } from 'daf-core' +import { RNUportHDSigner, getSignerForHDPath } from 'react-native-uport-signer' + +import Debug from 'debug' +const debug = Debug('daf:ethr-did-react-native:identity-provider') + +// ANDROID OPTIONS +// +// `singleprompt` - Prompt is only asked once per session or period of time +// `prompt` - Prompt every time +// `simple` - Not hardware protected but you don't loose your key if you change pin +// `cloud` - Backed up in some cloud storage + +const DEFAULT_LEVEL = 'simple' +const SHOW_SEED_PROMPT = 'Do you want to reveal seed phrase?' + +type level = 'singleprompt' | 'prompt' | 'simple' | 'cloud' + +export class Identity extends AbstractIdentity { + public readonly did: string + public readonly identityProviderType: string + private readonly sign: any + + constructor(options: { identityProviderType: string; did: string; sign: any }) { + super() + this.did = options.did + this.identityProviderType = options.identityProviderType + this.sign = options.sign + } + + public signer(keyId?: string) { + return this.sign + } + + async didDoc() { + return Promise.reject('not implemented') + } + async encrypt(): Promise {} + async decrypt(): Promise {} +} + +export class IdentityProvider extends AbstractIdentityProvider { + type = 'rnEthr' + description = 'Ethr identities backed by native signer' + private level: level + + constructor(securityLevel?: level) { + super() + this.level = securityLevel || DEFAULT_LEVEL + } + + identityFromAddress(address: string): Identity { + return new Identity({ + did: this.didFromAddress(address), + sign: getSignerForHDPath(address) as any, + identityProviderType: this.type, + }) + } + + didFromAddress(address: string): string { + return 'did:ethr:' + address + } + + async getIdentities() { + const addresses = await RNUportHDSigner.listSeedAddresses() + return addresses.map(this.identityFromAddress.bind(this)) as Identity[] + } + + async createIdentity() { + const { address } = await RNUportHDSigner.createSeed(this.level) + const did = this.didFromAddress(address) + debug('Created', did) + return this.identityFromAddress(address) + } + + async deleteIdentity(did: string) { + const address = did.slice(9) + try { + const result = await RNUportHDSigner.deleteSeed(address) + debug('Deleted', did) + return true + } catch (e) { + debug(e) + return false + } + } + + async getIdentity(did: string) { + const address = did.slice(9) + return this.identityFromAddress(address) + } + + async importIdentity(seed: string) { + const { address } = await RNUportHDSigner.importSeed(seed, this.level) + const identity = this.identityFromAddress(address) + debug('Imported %s', identity.did) + return identity + } + + async exportIdentity(did: string) { + const address = did.slice(9) + return await RNUportHDSigner.showSeed(address, SHOW_SEED_PROMPT) + } +} diff --git a/packages/daf-ethr-did-react-native/src/index.ts b/packages/daf-ethr-did-react-native/src/index.ts index f07562edf..531854f24 100644 --- a/packages/daf-ethr-did-react-native/src/index.ts +++ b/packages/daf-ethr-did-react-native/src/index.ts @@ -1,86 +1 @@ -import { IdentityController, Issuer } from 'daf-core' -import { RNUportHDSigner, getSignerForHDPath } from 'react-native-uport-signer' - -import Debug from 'debug' -const debug = Debug('daf:ethr-did-react-native:identity-controller') - -// ANDROID OPTIONS -// -// `singleprompt` - Prompt is only asked once per session or period of time -// `prompt` - Prompt every time -// `simple` - Not hardware protected but you don't loose your key if you change pin -// `cloud` - Backed up in some cloud storage - -const DEFAULT_LEVEL = 'simple' -const SHOW_SEED_PROMPT = 'Do you want to reveal seed phrase?' - -type level = 'singleprompt' | 'prompt' | 'simple' | 'cloud' - -class EthrDidRnController implements IdentityController { - type = 'rnEthr' - private level: level - - constructor(securityLevel?: level) { - this.level = securityLevel || DEFAULT_LEVEL - } - - issuerFromAddress(address: string): Issuer { - return { - did: this.didFromAddress(address), - signer: getSignerForHDPath(address) as any, - type: this.type, - } - } - - didFromAddress(address: string): string { - return 'did:ethr:' + address - } - - async listDids() { - const addresses = await RNUportHDSigner.listSeedAddresses() - return addresses.map(this.didFromAddress) - } - - async listIssuers() { - const addresses = await RNUportHDSigner.listSeedAddresses() - return addresses.map(this.issuerFromAddress.bind(this)) as Issuer[] - } - - async create() { - const { address } = await RNUportHDSigner.createSeed(this.level) - const did = this.didFromAddress(address) - debug('Created', did) - return did - } - - async delete(did: string) { - const address = did.slice(9) - try { - const result = await RNUportHDSigner.deleteSeed(address) - debug('Deleted', did) - return true - } catch (e) { - debug(e) - return false - } - } - - async issuer(did: string) { - const address = did.slice(9) - return this.issuerFromAddress(address) - } - - async import(seed: string) { - const { address } = await RNUportHDSigner.importSeed(seed, this.level) - const issuer = this.issuerFromAddress(address) - debug('Imported %s', issuer.did) - return issuer - } - - async export(did: string) { - const address = did.slice(9) - return await RNUportHDSigner.showSeed(address, SHOW_SEED_PROMPT) - } -} - -export default EthrDidRnController +export { IdentityProvider } from './identity-provider' diff --git a/yarn.lock b/yarn.lock index 1113d66e3..6a051c668 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3754,16 +3754,6 @@ asn1.js@^4.0.0: inherits "^2.0.1" minimalistic-assert "^1.0.0" -asn1.js@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.3.0.tgz#439099fe9174e09cff5a54a9dda70260517e8689" - integrity sha512-WHnQJFcOrIWT1RLOkFFBQkFVvyt9BPOOrH+Dp152Zk4R993rSzXUGPmkybIcUFhHE2d/iHH+nCaOWVCDbO8fgA== - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - safer-buffer "^2.1.0" - asn1@~0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" @@ -6320,7 +6310,7 @@ elliptic@6.3.2: hash.js "^1.0.0" inherits "^2.0.1" -elliptic@^6.0.0, elliptic@^6.4.0: +elliptic@^6.0.0, elliptic@^6.4.0, elliptic@^6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.2.tgz#05c5678d7173c049d8ca433552224a495d0e3762" integrity sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw== @@ -9577,13 +9567,6 @@ jest@24.9.0, jest@^24.9.0: import-local "^2.0.0" jest-cli "^24.9.0" -jose@^1.22.1: - version "1.22.1" - resolved "https://registry.yarnpkg.com/jose/-/jose-1.22.1.tgz#c8681ce92cf6dbe768b3e24f86049b652e3ca680" - integrity sha512-0TV4InJSlSFNXM3a9q3mwvk+hsp03G2/rHbxhR82wV1HbtJXt+5s0dazoYyfsNhLGDfxhN1htw8I0rBLLmtfLA== - dependencies: - asn1.js "^5.3.0" - js-levenshtein@^1.1.3: version "1.1.6" resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"