Refactor Identity

This commit is contained in:
haad
2023-02-17 11:12:32 +02:00
parent 5b6a70c7cc
commit 4b4d2ff35c
17 changed files with 231 additions and 221 deletions

View File

@@ -1,10 +1,9 @@
import Identity from './identity.js'
import Identity, { isIdentity, isEqual, decodeIdentity } from './identity.js'
import OrbitDBIdentityProvider from './providers/orbitdb.js'
// import DIDIdentityProvider from './identity-providers/did.js'
// import EthIdentityProvider from './identity-providers/ethereum.js'
import KeyStore from '../key-store.js'
import IdentityStore from './identity-store.js'
import { LRUStorage } from '../storage/index.js'
import { LRUStorage, IPFSBlockStorage, MemoryStorage } from '../storage/index.js'
import path from 'path'
const DefaultProviderType = 'orbitdb'
@@ -16,15 +15,18 @@ const supportedTypes = {
// [EthIdentityProvider.type]: EthIdentityProvider
}
const Identities = async ({ keystore, signingKeyStore, identityKeysPath, signingKeysPath, identityStore, ipfs } = {}) => {
const Identities = async ({ keystore, signingKeyStore, identityKeysPath, signingKeysPath, storage, ipfs } = {}) => {
keystore = keystore || new KeyStore(identityKeysPath || DefaultIdentityKeysPath)
signingKeyStore = signingKeyStore || (signingKeysPath ? new KeyStore(signingKeysPath) : keystore)
identityStore = identityStore || await IdentityStore({ ipfs })
storage = storage || (ipfs ? await IPFSBlockStorage({ ipfs, pin: true }) : await MemoryStorage())
const verifiedIdentitiesCache = await LRUStorage({ size: 1000 })
const getIdentity = async (hash) => {
return identityStore.get(hash)
const bytes = await storage.get(hash)
if (bytes) {
return decodeIdentity(bytes)
}
}
const createIdentity = async (options = {}) => {
@@ -38,18 +40,21 @@ const Identities = async ({ keystore, signingKeyStore, identityKeysPath, signing
const privateKey = await keystore.getKey(id) || await keystore.createKey(id)
const publicKey = keystore.getPublic(privateKey)
const idSignature = await KeyStore.sign(privateKey, id)
const pubKeyIdSignature = await identityProvider.signIdentity(publicKey + idSignature, options)
const identity = new Identity(id, publicKey, idSignature, pubKeyIdSignature, type, sign, verify)
const publicKeyAndIdSignature = await identityProvider.signIdentity(publicKey + idSignature, options)
const identity = await Identity({ id, publicKey, idSignature, publicKeyAndIdSignature, type, sign, verify })
const hash = await identityStore.put(identity.toJSON())
// TODO: fix this monkey patching
identity.hash = hash
// const { hash, bytes } = await encodeIdentity(identity)
// console.log(hash, bytes)
await storage.put(identity.hash, identity.bytes)
// const hash = await storage.put(identity.toJSON())
// // TODO: fix this monkey patching
// identity.hash = hash
return identity
}
const verifyIdentity = async (identity) => {
if (!Identity.isIdentity(identity)) {
if (!isIdentity(identity)) {
return false
}
@@ -62,14 +67,14 @@ const Identities = async ({ keystore, signingKeyStore, identityKeysPath, signing
const verifiedIdentity = await verifiedIdentitiesCache.get(signatures.id)
if (verifiedIdentity) {
return Identity.isEqual(identity, verifiedIdentity)
return isEqual(identity, verifiedIdentity)
}
const Provider = getProviderFor(identity.type)
const identityVerified = await Provider.verifyIdentity(identity)
if (identityVerified) {
await verifiedIdentitiesCache.put(signatures.id, identity.toJSON())
await verifiedIdentitiesCache.put(signatures.id, identity)
}
return identityVerified
@@ -89,47 +94,49 @@ const Identities = async ({ keystore, signingKeyStore, identityKeysPath, signing
return KeyStore.verify(signature, publicKey, data)
}
const isSupported = (type) => {
return Object.keys(supportedTypes).includes(type)
}
const getProviderFor = (type) => {
if (!isSupported(type)) {
throw new Error(`IdentityProvider type '${type}' is not supported`)
}
return supportedTypes[type]
}
const addIdentityProvider = (IdentityProvider) => {
if (!IdentityProvider) {
throw new Error('IdentityProvider must be given as an argument')
}
if (!IdentityProvider.type ||
typeof IdentityProvider.type !== 'string') {
throw new Error('Given IdentityProvider doesn\'t have a field \'type\'')
}
supportedTypes[IdentityProvider.type] = IdentityProvider
}
const removeIdentityProvider = (type) => {
delete supportedTypes[type]
}
return {
createIdentity,
verifyIdentity,
getIdentity,
sign,
verify,
isSupported,
addIdentityProvider,
removeIdentityProvider,
keystore,
signingKeyStore
}
}
export { Identities as default }
const isProviderSupported = (type) => {
return Object.keys(supportedTypes).includes(type)
}
const getProviderFor = (type) => {
if (!isProviderSupported(type)) {
throw new Error(`IdentityProvider type '${type}' is not supported`)
}
return supportedTypes[type]
}
const addIdentityProvider = (IdentityProvider) => {
if (!IdentityProvider) {
throw new Error('IdentityProvider must be given as an argument')
}
if (!IdentityProvider.type ||
typeof IdentityProvider.type !== 'string') {
throw new Error('Given IdentityProvider doesn\'t have a field \'type\'')
}
supportedTypes[IdentityProvider.type] = IdentityProvider
}
const removeIdentityProvider = (type) => {
delete supportedTypes[type]
}
export {
Identities as default,
isProviderSupported,
addIdentityProvider,
removeIdentityProvider
}

View File

@@ -1,38 +0,0 @@
import IPFSBlockStorage from '../storage/ipfs-block.js'
import MemoryStorage from '../storage/memory.js'
import * as dagCbor from '@ipld/dag-cbor'
import { sha256 } from 'multiformats/hashes/sha2'
import { base58btc } from 'multiformats/bases/base58'
import * as Block from 'multiformats/block'
const codec = dagCbor
const hasher = sha256
const IdentityStore = async ({ ipfs, storage } = {}) => {
storage = storage || (ipfs
? await IPFSBlockStorage({ ipfs, pin: true })
: await MemoryStorage())
const put = async (value) => {
const { cid, bytes } = await Block.encode({ value, codec, hasher })
const hash = cid.toString(base58btc)
await storage.put(hash, bytes)
return hash
}
const get = async (hash) => {
const bytes = await storage.get(hash)
if (bytes) {
const { value } = await Block.decode({ bytes, codec, hasher })
return value
}
}
return {
put,
get
}
}
export default IdentityStore

View File

@@ -1,98 +1,96 @@
import * as Block from 'multiformats/block'
import * as dagCbor from '@ipld/dag-cbor'
import { sha256 } from 'multiformats/hashes/sha2'
import { base58btc } from 'multiformats/bases/base58'
import isDefined from '../utils/is-defined.js'
class Identity {
constructor (id, publicKey, idSignature, pubKeyIdSignature, type, signFn, verifyFn) {
if (!isDefined(id)) {
throw new Error('Identity id is required')
}
const codec = dagCbor
const hasher = sha256
const hashStringEncoding = base58btc
if (!isDefined(publicKey)) {
throw new Error('Invalid public key')
}
if (!isDefined(idSignature)) {
throw new Error('Signature of the id (idSignature) is required')
}
if (!isDefined(pubKeyIdSignature)) {
throw new Error('Signature of (publicKey + idSignature) is required')
}
if (!isDefined(type)) {
throw new Error('Identity type is required')
}
// if (!isDefined(provider)) {
// throw new Error('Identity provider is required')
// }
this._id = id
this._publicKey = publicKey
this._signatures = Object.assign({}, { id: idSignature }, { publicKey: pubKeyIdSignature })
this._type = type
// this._provider = provider
this.hash = null
this.sign = signFn
this.verify = verifyFn
}
/**
* This is only used as a fallback to the clock id when necessary
* @return {string} public key hex encoded
*/
get id () {
return this._id
}
get publicKey () {
return this._publicKey
}
get signatures () {
return this._signatures
}
get type () {
return this._type
}
// get provider () {
// return this._provider
// }
toJSON () {
return {
id: this.id,
publicKey: this.publicKey,
signatures: this.signatures,
type: this.type
}
}
static isEqual (a, b) {
return a.id === b.id &&
a.publicKey === b.publicKey &&
a.signatures.id === b.signatures.id &&
a.signatures.publicKey === b.signatures.publicKey
}
static isIdentity (identity) {
return identity.id !== undefined &&
identity.publicKey !== undefined &&
identity.signatures !== undefined &&
identity.signatures.id !== undefined &&
identity.signatures.publicKey !== undefined &&
identity.type !== undefined
}
static toJSON (identity) {
return {
id: identity.id,
publicKey: identity.publicKey,
signatures: identity.signatures,
type: identity.type
}
}
const isEqual = (a, b) => {
return a.id === b.id &&
a.publicKey === b.publicKey &&
a.signatures.id === b.signatures.id &&
a.signatures.publicKey === b.signatures.publicKey
}
export default Identity
const isIdentity = (identity) => {
return isDefined(identity.id) &&
isDefined(identity.publicKey) &&
isDefined(identity.signatures) &&
isDefined(identity.signatures.id) &&
isDefined(identity.signatures.publicKey) &&
isDefined(identity.type)
}
/**
* Encode an Identity to a serializable form
* @param {Identity} identity Identity to encode
* @returns {Object} Object with fields hash and bytes
*/
const encodeIdentity = async (identity) => {
const { id, publicKey, signatures, type } = identity
const value = { id, publicKey, signatures, type }
const { cid, bytes } = await Block.encode({ value, codec, hasher })
const hash = cid.toString(hashStringEncoding)
return { hash, bytes }
}
/**
* Decode an Identity from bytes
* @param {Uint8Array} bytes Bytes from which to decode an Identity from
* @returns {Identity}
*/
const decodeIdentity = async (bytes) => {
const { value } = await Block.decode({ bytes, codec, hasher })
const { id, publicKey, signatures, type } = value
return Identity({
id,
publicKey,
idSignature: signatures.id,
publicKeyAndIdSignature: signatures.publicKey,
type
})
}
const Identity = async ({ id, publicKey, idSignature, publicKeyAndIdSignature, type, sign, verify } = {}) => {
if (!isDefined(id)) {
throw new Error('Identity id is required')
}
if (!isDefined(publicKey)) {
throw new Error('Invalid public key')
}
if (!isDefined(idSignature)) {
throw new Error('Signature of the id (idSignature) is required')
}
if (!isDefined(publicKeyAndIdSignature)) {
throw new Error('Signature of (publicKey + idSignature) is required')
}
if (!isDefined(type)) {
throw new Error('Identity type is required')
}
const signatures = Object.assign({}, { id: idSignature }, { publicKey: publicKeyAndIdSignature })
const identity = {
id,
publicKey,
signatures,
type,
sign,
verify
}
const { hash, bytes } = await encodeIdentity(identity)
identity.hash = hash
identity.bytes = bytes
return identity
}
export { Identity as default, isEqual, isIdentity, encodeIdentity, decodeIdentity }

View File

@@ -1,2 +1,8 @@
export { default as Identities } from './identities.js'
export {
default as Identities,
addIdentityProvider,
removeIdentityProvider,
isProviderSupported
} from './identities.js'
export { default as Identity } from './identity.js'

View File

@@ -58,12 +58,12 @@ const create = async (identity, id, payload, clock = null, next = [], refs = [])
/**
* Verifies an entry signature.
*
* @param {IdentityProvider} identityProvider The identity provider to use
* @param {Identities} identities Identities system to use
* @param {Entry} entry The entry being verified
* @return {Promise} A promise that resolves to a boolean value indicating if the signature is valid
*/
const verify = async (identityProvider, entry) => {
if (!identityProvider) throw new Error('Identity-provider is required, cannot verify entry')
const verify = async (identities, entry) => {
if (!identities) throw new Error('Identities is required, cannot verify entry')
if (!isEntry(entry)) throw new Error('Invalid Log entry')
if (!entry.key) throw new Error("Entry doesn't have a key")
if (!entry.sig) throw new Error("Entry doesn't have a signature")
@@ -79,7 +79,7 @@ const verify = async (identityProvider, entry) => {
const { bytes } = await Block.encode({ value, codec, hasher })
return identityProvider.verify(entry.sig, entry.key, bytes)
return identities.verify(entry.sig, entry.key, bytes)
}
/**

View File

@@ -1 +0,0 @@
MANIFEST-000031

View File

@@ -1,3 +0,0 @@
2023/02/16-14:47:21.736625 171233000 Recovering log #30
2023/02/16-14:47:21.737020 171233000 Delete type=3 #29
2023/02/16-14:47:21.737069 171233000 Delete type=0 #30

View File

@@ -1,3 +0,0 @@
2023/02/16-14:46:59.857680 16e03b000 Recovering log #28
2023/02/16-14:46:59.858826 16e03b000 Delete type=0 #28
2023/02/16-14:46:59.858852 16e03b000 Delete type=3 #27

View File

@@ -2,7 +2,7 @@ import assert from 'assert'
import path from 'path'
import rmrf from 'rimraf'
import { KeyStore, Identities } from '../../src/index.js'
import { Identity } from '../../src/identities/index.js'
import { Identity, addIdentityProvider } from '../../src/identities/index.js'
import { Ed25519Provider } from 'key-did-provider-ed25519'
import KeyDidResolver from 'key-did-resolver'
import DIDIdentityProvider from '../../src/identities/providers/did.js'
@@ -19,9 +19,9 @@ describe('DID Identity Provider', function () {
before(async () => {
keystore = new KeyStore(keypath)
await keystore.open()
identities = await Identities({ keystore })
DIDIdentityProvider.setDIDResolver(KeyDidResolver.getResolver())
identities.addIdentityProvider(DIDIdentityProvider)
addIdentityProvider(DIDIdentityProvider)
identities = await Identities({ keystore })
})
after(async () => {
@@ -82,7 +82,14 @@ describe('DID Identity Provider', function () {
})
it('DID identity with incorrect id does not verify', async () => {
const identity2 = new Identity('NotAnId', identity.publicKey, identity.signatures.id, identity.signatures.publicKey, identity.type, identity.provider)
const { publicKey, signatures, type } = identity
const identity2 = await Identity({
id: 'NotAnId',
publicKey,
idSignature: signatures.id,
publicKeyAndIdSignature: signatures.publicKey,
type
})
const verified = await identities.verifyIdentity(identity2)
assert.strictEqual(verified, false)
})
@@ -106,7 +113,14 @@ describe('DID Identity Provider', function () {
it('throws an error if private key is not found from keystore', async () => {
// Remove the key from the keystore (we're using a mock storage in these tests)
const modifiedIdentity = new Identity('this id does not exist', identity.publicKey, '<sig>', identity.signatures, identity.type, identity.provider)
const { publicKey, signatures, type } = identity
const modifiedIdentity = await Identity({
id: 'this id does not exist',
publicKey,
idSignature: '<sig>',
publicKeyAndIdSignature: signatures.publicKey,
type
})
let signature
let err
try {

View File

@@ -2,7 +2,7 @@ import assert from 'assert'
import path from 'path'
import rmrf from 'rimraf'
import { KeyStore, Identities } from '../../src/index.js'
import { Identity } from '../../src/identities/index.js'
import { Identity, addIdentityProvider } from '../../src/identities/index.js'
import EthIdentityProvider from '../../src/identities/providers/ethereum.js'
const keypath = path.resolve('./test/identities/fixtures/keys')
@@ -15,8 +15,8 @@ describe('Ethereum Identity Provider', function () {
before(async () => {
keystore = new KeyStore(keypath)
await keystore.open()
addIdentityProvider(EthIdentityProvider)
identities = await Identities({ keystore })
identities.addIdentityProvider(EthIdentityProvider)
})
after(async () => {
@@ -79,7 +79,14 @@ describe('Ethereum Identity Provider', function () {
})
it('ethereum identity with incorrect id does not verify', async () => {
const identity2 = new Identity('NotAnId', identity.publicKey, identity.signatures.id, identity.signatures.publicKey, identity.type, identity.provider)
const { publicKey, signatures, type } = identity
const identity2 = await Identity({
id: 'NotAnId',
publicKey,
idSignature: signatures.id,
publicKeyAndIdSignature: signatures.publicKey,
type
})
const verified = await identities.verifyIdentity(identity2)
assert.strictEqual(verified, false)
})
@@ -102,7 +109,14 @@ describe('Ethereum Identity Provider', function () {
it('throws an error if private key is not found from keystore', async () => {
// Remove the key from the keystore (we're using a mock storage in these tests)
const modifiedIdentity = new Identity('this id does not exist', identity.publicKey, '<sig>', identity.signatures, identity.type, identity.provider)
const { publicKey, signatures, type } = identity
const modifiedIdentity = await Identity({
id: 'this id does not exist',
publicKey,
idSignature: '<sig>',
publicKeyAndIdSignature: signatures.publicKey,
type
})
let signature
let err
try {

View File

@@ -2,7 +2,7 @@ import assert from 'assert'
import path from 'path'
import rmrf from 'rimraf'
import { KeyStore, Identities } from '../../src/index.js'
import { Identity } from '../../src/identities/index.js'
import { Identity, addIdentityProvider } from '../../src/identities/index.js'
import fs from 'fs-extra'
const fixturesPath = path.resolve('./test/identities/fixtures/keys')
const savedKeysPath = path.resolve('./test/identities/fixtures/savedKeys')
@@ -171,7 +171,7 @@ describe('Identity Provider', function () {
assert.strictEqual(identity.signatures.id, expectedIdSignature)
})
it('has a pubKeyIdSignature for the publicKey', async () => {
it('has a publicKeyAndIdSignature for the publicKey', async () => {
assert.strictEqual(identity.signatures.publicKey, expectedPkIdSignature)
})
@@ -179,8 +179,8 @@ describe('Identity Provider', function () {
const internalSigningKey = await savedKeysKeyStore.getKey(identity.id)
const externalSigningKey = await savedKeysKeyStore.getKey(id)
const idSignature = await KeyStore.sign(internalSigningKey, identity.id)
const pubKeyIdSignature = await KeyStore.sign(externalSigningKey, identity.publicKey + idSignature)
const expectedSignature = { id: idSignature, publicKey: pubKeyIdSignature }
const publicKeyAndIdSignature = await KeyStore.sign(externalSigningKey, identity.publicKey + idSignature)
const expectedSignature = { id: idSignature, publicKey: publicKeyAndIdSignature }
assert.deepStrictEqual(identity.signatures, expectedSignature)
})
})
@@ -234,7 +234,7 @@ describe('Identity Provider', function () {
static get type () { return 'fake' }
}
identities.addIdentityProvider(IP)
addIdentityProvider(IP)
identity = await identities.createIdentity({ type: IP.type })
const verified = await identities.verifyIdentity(identity)
assert.strictEqual(verified, false)
@@ -309,7 +309,14 @@ describe('Identity Provider', function () {
it('throws an error if private key is not found from keystore', async () => {
// Remove the key from the keystore (we're using a mock storage in these tests)
const modifiedIdentity = new Identity('this id does not exist', identity.publicKey, '<sig>', identity.signatures, identity.type)
const { publicKey, signatures, type } = identity
const modifiedIdentity = await Identity({
id: 'this id does not exist',
publicKey,
idSignature: '<sig>',
publicKeyAndIdSignature: signatures,
type
})
let signature
let err
try {

View File

@@ -12,7 +12,7 @@ describe('Identity', function () {
let identity
before(async () => {
identity = new Identity(id, publicKey, idSignature, publicKeyAndIdSignature, type, provider)
identity = await Identity({ id, publicKey, idSignature, publicKeyAndIdSignature, type })
})
it('has the correct id', async () => {
@@ -35,21 +35,21 @@ describe('Identity', function () {
// assert.deepStrictEqual(identity.provider, provider)
// })
it('converts identity to a JSON object', async () => {
const expected = {
id,
publicKey,
signatures: { id: idSignature, publicKey: publicKeyAndIdSignature },
type
}
assert.deepStrictEqual(identity.toJSON(), expected)
})
// it('converts identity to a JSON object', async () => {
// const expected = {
// id,
// publicKey,
// signatures: { id: idSignature, publicKey: publicKeyAndIdSignature },
// type
// }
// assert.deepStrictEqual(identity.toJSON(), expected)
// })
describe('Constructor inputs', () => {
it('throws and error if id was not given in constructor', async () => {
let err
try {
identity = new Identity()
identity = await Identity()
} catch (e) {
err = e.toString()
}
@@ -59,7 +59,7 @@ describe('Identity', function () {
it('throws and error if publicKey was not given in constructor', async () => {
let err
try {
identity = new Identity('abc')
identity = await Identity({ id: 'abc'})
} catch (e) {
err = e.toString()
}
@@ -69,7 +69,7 @@ describe('Identity', function () {
it('throws and error if identity signature was not given in constructor', async () => {
let err
try {
identity = new Identity('abc', publicKey)
identity = await Identity({ id: 'abc', publicKey })
} catch (e) {
err = e.toString()
}
@@ -79,7 +79,7 @@ describe('Identity', function () {
it('throws and error if identity signature was not given in constructor', async () => {
let err
try {
identity = new Identity('abc', publicKey, idSignature)
identity = await Identity({ id: 'abc', publicKey, idSignature })
} catch (e) {
err = e.toString()
}
@@ -99,7 +99,7 @@ describe('Identity', function () {
it('throws and error if identity type was not given in constructor', async () => {
let err
try {
identity = new Identity('abc', publicKey, idSignature, publicKeyAndIdSignature, null, provider)
identity = await Identity({ id: 'abc', publicKey, idSignature, publicKeyAndIdSignature })
} catch (e) {
err = e.toString()
}

View File

@@ -79,7 +79,16 @@ Object.keys(testAPIs).forEach((IPFS) => {
})
it('retrieves the identity from an entry', async() => {
const expected = testIdentity.toJSON()
const expected = {
id: testIdentity.id,
publicKey: testIdentity.publicKey,
signatures: testIdentity.signatures,
type: testIdentity.type,
hash: testIdentity.hash,
bytes: testIdentity.bytes,
sign: undefined,
verify: undefined,
}
const payload = 'hello world'
const entry = await create(testIdentity, 'A', payload)
const entryIdentity = await identities.getIdentity(entry.identity)