diff --git a/src/index.ts b/src/index.ts index b00177c1..121860eb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,62 +1,81 @@ -import { Domain } from "@input-output-hk/atala-prism-wallet-sdk"; +import { + Domain, + Ed25519PrivateKey, + KeyProperties, + Secp256k1PrivateKey, + X25519PrivateKey, +} from "@input-output-hk/atala-prism-wallet-sdk"; import { getRxStorageDexie } from "rxdb/plugins/storage-dexie"; import { wrappedKeyEncryptionCryptoJsStorage } from "rxdb/plugins/encryption-crypto-js"; -import { createRxDatabase } from "rxdb"; +import { RxDatabaseCreator, RxDocument, createRxDatabase } from "rxdb"; import { RxError } from "rxdb/dist/lib/rx-error"; import { addRxPlugin } from "rxdb"; import { RxDBMigrationPlugin } from "rxdb/plugins/migration"; +import { RxDBQueryBuilderPlugin } from "rxdb/plugins/query-builder"; import { v4 as uuidv4 } from "uuid"; +import type { + KeySchemaType, + KeySpec, + PlutoCollections, + PlutoDatabase, +} from "./types"; + import MessageSchema from "./schemas/Message"; import DIDSchema from "./schemas/DID"; -import { KeySpec, PlutoCollections, PlutoDatabase } from "./types"; import CredentialSchema from "./schemas/Credential"; import DIDPairSchema from "./schemas/DIDPair"; import MediatorSchema from "./schemas/Mediator"; import PrivateKeySchema from "./schemas/PrivateKey"; addRxPlugin(RxDBMigrationPlugin); -//New change +addRxPlugin(RxDBQueryBuilderPlugin); export class Database implements Domain.Pluto { - constructor(private db: PlutoDatabase) {} + private _db!: PlutoDatabase; + private get db() { + if (!this._db) { + throw new Error("Start Pluto first."); + } + return this._db; + } + + private collections = { + messages: { + schema: MessageSchema, + }, + dids: { + schema: DIDSchema, + }, + verifiableCredentials: { + schema: CredentialSchema, + }, + didpairs: { + schema: DIDPairSchema, + }, + mediators: { + schema: MediatorSchema, + }, + privateKeys: { + schema: PrivateKeySchema, + }, + }; + + constructor(private dbOptions: RxDatabaseCreator) {} static async createEncrypted(name: string, encryptionKey: Uint8Array) { - try { - const myDatabase = await createRxDatabase({ - ignoreDuplicate: true, - name: name, - storage: wrappedKeyEncryptionCryptoJsStorage({ - storage: getRxStorageDexie(), - }), - password: Buffer.from(encryptionKey).toString("hex"), - }); - await myDatabase.addCollections({ - messages: { - schema: MessageSchema, - }, - dids: { - schema: DIDSchema, - }, - verifiableCredentials: { - schema: CredentialSchema, - }, - didpairs: { - schema: DIDPairSchema, - }, - mediators: { - schema: MediatorSchema, - }, - privateKeys: { - schema: PrivateKeySchema, - }, - }); - return new Database(myDatabase); - } catch (err) { - if (err instanceof RxError && (err as RxError).code === "DB1") { - throw new Error("Invalid authentication"); - } else throw err; - } + return new Database({ + ignoreDuplicate: true, + name: name, + storage: wrappedKeyEncryptionCryptoJsStorage({ + storage: getRxStorageDexie(), + }), + password: Buffer.from(encryptionKey).toString("hex"), + }); + } + + async getMessage(id: string): Promise { + return this.db.messages.findOne().where({ id: id }).exec(); } async storeMessage(message: Domain.Message): Promise { @@ -64,15 +83,24 @@ export class Database implements Domain.Pluto { } async storeMessages(messages: Domain.Message[]): Promise { - await Promise.all(messages.map(this.storeMessage)); + await Promise.all(messages.map((message) => this.storeMessage(message))); } async getAllMessages(): Promise { - return this.db.messages.find({}).exec(); + return this.db.messages.find().exec(); } - start(): Promise { - throw new Error("Method not implemented."); + async start(): Promise { + const { dbOptions, collections } = this; + try { + const database = await createRxDatabase(dbOptions); + await database.addCollections(collections); + this._db = database; + } catch (err) { + if (err instanceof RxError && (err as RxError).code === "DB1") { + throw new Error("Invalid authentication"); + } else throw err; + } } async storePrismDID( @@ -89,38 +117,12 @@ export class Database implements Domain.Pluto { schema: did.schema, alias: alias, }); - - const initialSpec: KeySpec[] = [ - { - type: "string", - name: "raw", - value: Buffer.from(privateKey.getEncoded()).toString("hex"), - }, - { - type: "number", - name: "index", - value: `${keyPathIndex}`, - }, - ]; - - const keySpecification = Array.from(privateKey.keySpecification).reduce( - (all, [key, value]) => { - all.push({ - type: "string", - name: key, - value: `${value}`, - }); - return all; - }, - initialSpec + await this.storePrivateKeys( + privateKey, + did, + keyPathIndex, + privateKeyMetaId ?? null ); - - await this.db.privateKeys.insert({ - id: uuidv4(), - did: did.toString(), - type: privateKey.type, - keySpecification, - }); } async storePeerDID( @@ -209,46 +211,6 @@ export class Database implements Domain.Pluto { }); } - async storeMediator( - mediator: Domain.DID, - host: Domain.DID, - routing: Domain.DID - ): Promise { - await this.db.mediators.insert({ - id: uuidv4(), - mediatorDID: mediator.toString(), - hostDID: host.toString(), - routingDID: routing.toString(), - }); - } - - storeCredential(credential: Domain.VerifiableCredential): Promise { - throw new Error("Method not implemented."); - } - getAllPrismDIDs(): Promise { - throw new Error("Method not implemented."); - } - getDIDInfoByDID(did: Domain.DID): Promise { - throw new Error("Method not implemented."); - } - getDIDInfoByAlias(alias: string): Promise { - throw new Error("Method not implemented."); - } - getPrismDIDKeyPathIndex(did: Domain.DID): Promise { - throw new Error("Method not implemented."); - } - getPrismLastKeyPathIndex(): Promise { - throw new Error("Method not implemented."); - } - getAllPeerDIDs(): Promise { - throw new Error("Method not implemented."); - } - getDIDPrivateKeysByDID(did: Domain.DID): Promise { - throw new Error("Method not implemented."); - } - getDIDPrivateKeyByID(id: string): Promise { - throw new Error("Method not implemented."); - } async getAllDidPairs(): Promise { const { DID, DIDPair } = Domain; const results = await this.db.didpairs.find().exec(); @@ -300,6 +262,208 @@ export class Database implements Domain.Pluto { ) : null; } + + private getPrivateKeyFromDB( + privateKey: RxDocument + ): Domain.PrivateKey { + const { type, keySpecification } = privateKey; + const curve = keySpecification.find( + (item) => item.name === KeyProperties.curve + ); + const raw = keySpecification.find( + (item) => item.name === KeyProperties.rawKey + ); + if (!(type in Domain.KeyTypes)) { + throw new Error(`Invalid KeyType ${type}`); + } + if (!curve) { + throw new Error("Undefined key curve"); + } + if (!raw) { + throw new Error("Undefined key raw"); + } + if (!(curve.value in Domain.Curve)) { + throw new Error(`Invalid key curve ${curve.value}`); + } + + if (type === Domain.KeyTypes.EC) { + if (curve.value === Domain.Curve.SECP256K1) { + const index = keySpecification.find( + (item) => item.name === KeyProperties.index + ); + const seed = keySpecification.find( + (item) => item.name === KeyProperties.seed + ); + + const privateKey = new Secp256k1PrivateKey( + Buffer.from(raw.value, "hex") + ); + + privateKey.keySpecification.set(Domain.KeyProperties.rawKey, raw.value); + + privateKey.keySpecification.set( + Domain.KeyProperties.curve, + Domain.Curve.SECP256K1 + ); + + if (index) { + privateKey.keySpecification.set( + Domain.KeyProperties.index, + index.value + ); + } + + if (seed) { + privateKey.keySpecification.set( + Domain.KeyProperties.seed, + seed.value + ); + } + + return privateKey; + } + + if (curve.value === Domain.Curve.ED25519) { + const privateKey = new Ed25519PrivateKey(Buffer.from(raw.value, "hex")); + + privateKey.keySpecification.set(Domain.KeyProperties.rawKey, raw.value); + + privateKey.keySpecification.set( + Domain.KeyProperties.curve, + Domain.Curve.SECP256K1 + ); + + return privateKey; + } + } + + if (type === Domain.KeyTypes.Curve25519) { + if (curve.value === Domain.Curve.X25519) { + const privateKey = new X25519PrivateKey(Buffer.from(raw.value, "hex")); + + privateKey.keySpecification.set(Domain.KeyProperties.rawKey, raw.value); + + privateKey.keySpecification.set( + Domain.KeyProperties.curve, + Domain.Curve.SECP256K1 + ); + + return privateKey; + } + } + + throw new Error(`Invalid key${curve.value} ${type}`); + } + + async getDIDPrivateKeysByDID(did: Domain.DID): Promise { + const privateKeys = await this.db.privateKeys + .find() + .where({ did: did.toString() }) + .exec(); + return privateKeys.map(this.getPrivateKeyFromDB); + } + + async getDIDPrivateKeyByID(id: string): Promise { + const privateKey = await this.db.privateKeys.findOne().where({ id }).exec(); + return privateKey ? this.getPrivateKeyFromDB(privateKey) : null; + } + + async storeMediator( + mediator: Domain.DID, + host: Domain.DID, + routing: Domain.DID + ): Promise { + await this.db.mediators.insert({ + id: uuidv4(), + mediatorDID: mediator.toString(), + hostDID: host.toString(), + routingDID: routing.toString(), + }); + } + + async getAllPrismDIDs(): Promise { + const dids = await this.db.dids.find().where({ method: "prism" }).exec(); + + const prismDIDInfo: Domain.PrismDIDInfo[] = []; + + for (let did of dids) { + const didPrivateKeys = await this.getDIDPrivateKeysByDID(did); + + for (let privateKey of didPrivateKeys) { + const indexProp = privateKey.getProperty(Domain.KeyProperties.index); + + const index = indexProp ? parseInt(indexProp) : undefined; + + prismDIDInfo.push( + new Domain.PrismDIDInfo( + Domain.DID.fromString(did.did), + index, + did.alias + ) + ); + } + } + + return prismDIDInfo; + } + + async getDIDInfoByDID(did: Domain.DID): Promise { + const didDB = await this.db.dids + .findOne() + .where({ did: did.toString() }) + .exec(); + + if (didDB) { + const [privateKey] = await this.getDIDPrivateKeysByDID(did); + if (privateKey) { + const indexProp = privateKey.getProperty(Domain.KeyProperties.index); + const index = indexProp ? parseInt(indexProp) : undefined; + return new Domain.PrismDIDInfo( + Domain.DID.fromString(didDB.did), + index, + didDB.alias + ); + } + } + + return null; + } + + async getDIDInfoByAlias(alias: string): Promise { + const dids = await this.db.dids.find().where({ alias }).exec(); + const prismDIDInfo: Domain.PrismDIDInfo[] = []; + for (let did of dids) { + const didPrivateKeys = await this.getDIDPrivateKeysByDID(did); + for (let privateKey of didPrivateKeys) { + const indexProp = privateKey.getProperty(Domain.KeyProperties.index); + const index = indexProp ? parseInt(indexProp) : undefined; + prismDIDInfo.push( + new Domain.PrismDIDInfo( + Domain.DID.fromString(did.did), + index, + did.alias + ) + ); + } + } + return prismDIDInfo; + } + + getPrismDIDKeyPathIndex(did: Domain.DID): Promise { + throw new Error("Method not implemented."); + } + getPrismLastKeyPathIndex(): Promise { + throw new Error("Method not implemented."); + } + + storeCredential(credential: Domain.VerifiableCredential): Promise { + throw new Error("Method not implemented."); + } + + getAllPeerDIDs(): Promise { + throw new Error("Method not implemented."); + } + getAllMessagesByDID(did: Domain.DID): Promise { throw new Error("Method not implemented."); } @@ -327,9 +491,7 @@ export class Database implements Domain.Pluto { ): Promise { throw new Error("Method not implemented."); } - getMessage(id: string): Promise { - throw new Error("Method not implemented."); - } + getAllMediators(): Promise { throw new Error("Method not implemented."); } diff --git a/src/schemas/Credential.ts b/src/schemas/Credential.ts index 5ea101fc..2eaa2958 100644 --- a/src/schemas/Credential.ts +++ b/src/schemas/Credential.ts @@ -1,4 +1,4 @@ -import { CredentialSchemaType, Schema } from "../types"; +import type { CredentialSchemaType, Schema } from "../types"; const CredentialSchema: Schema = { version: 0, diff --git a/src/schemas/DID.ts b/src/schemas/DID.ts index afb330e7..90e79218 100644 --- a/src/schemas/DID.ts +++ b/src/schemas/DID.ts @@ -1,4 +1,4 @@ -import { DIDSchemaType, Schema } from "../types"; +import type { DIDSchemaType, Schema } from "../types"; const DIDSchema: Schema = { version: 0, @@ -26,7 +26,7 @@ const DIDSchema: Schema = { maxLength: 60, }, }, - encrypted: ["method", "methodId", "schema"], + encrypted: [], required: ["method", "methodId", "did", "schema"], }; diff --git a/src/schemas/DIDPair.ts b/src/schemas/DIDPair.ts index 7157bf9e..155bb97e 100644 --- a/src/schemas/DIDPair.ts +++ b/src/schemas/DIDPair.ts @@ -1,4 +1,4 @@ -import { DIDPairSchemaType, Schema } from "../types"; +import type { DIDPairSchemaType, Schema } from "../types"; const DIDPairSchema: Schema = { version: 0, diff --git a/src/schemas/Mediator.ts b/src/schemas/Mediator.ts index 8ca41f70..e262ef91 100644 --- a/src/schemas/Mediator.ts +++ b/src/schemas/Mediator.ts @@ -1,4 +1,4 @@ -import { Schema, MediarorSchemaType } from "../types"; +import type { Schema, MediarorSchemaType } from "../types"; const MediatorSchema: Schema = { version: 0, diff --git a/src/schemas/Message.ts b/src/schemas/Message.ts index db4849e1..9829092c 100644 --- a/src/schemas/Message.ts +++ b/src/schemas/Message.ts @@ -1,4 +1,4 @@ -import { Schema, MessageSchemaType } from "../types"; +import type { Schema, MessageSchemaType } from "../types"; const MessageSchema: Schema = { version: 0, diff --git a/src/schemas/PrivateKey.ts b/src/schemas/PrivateKey.ts index 2618d076..bb504866 100644 --- a/src/schemas/PrivateKey.ts +++ b/src/schemas/PrivateKey.ts @@ -1,4 +1,4 @@ -import { Schema, KeySchemaType } from "../types"; +import type { Schema, KeySchemaType } from "../types"; const PrivateKeySchema: Schema = { version: 0, diff --git a/tests/pluto.test.ts b/tests/pluto.test.ts index 3db68978..1a726d02 100644 --- a/tests/pluto.test.ts +++ b/tests/pluto.test.ts @@ -6,20 +6,33 @@ import { randomUUID } from "crypto"; import { Apollo, Domain } from "@input-output-hk/atala-prism-wallet-sdk"; const databaseName = "prism-db"; +const keyData = new Uint8Array(32); + +const createMessage = () => new Domain.Message("{}", randomUUID(), ""); +const defaultPassword = Buffer.from(keyData); +const apollo = new Apollo(); describe("Pluto + Dexie encrypted integration for browsers", () => { + it("Should require to start pluto database before using it", async () => { + const db = await Database.createEncrypted(databaseName, defaultPassword); + expect(db.getAllMessages()).rejects.toThrowError( + new Error("Start Pluto first.") + ); + }); + it("Should be able to instanciate an encrypted IndexDB Database and throw an error if started with wrong password", async () => { async function createAndLoad(password: Uint8Array) { const db = await Database.createEncrypted( databaseName, Buffer.from(password) ); + await db.start(); const messages = await db.getAllMessages(); expect(messages.length).toEqual(0); } - const keyData = new Uint8Array(32); await createAndLoad(keyData); + const keyData2 = keyData; keyData2[0] = 1; keyData2[1] = 2; @@ -32,15 +45,111 @@ describe("Pluto + Dexie encrypted integration for browsers", () => { it("Should store a new DID and its privateKeys", async () => { const db = await Database.createEncrypted( `${databaseName}${randomUUID()}`, - Buffer.from(new Uint8Array(32)) + defaultPassword ); + await db.start(); const did = Domain.DID.fromString( "did:prism:733e594871d7700d35e6116011a08fc11e88ff9d366d8b5571ffc1aa18d249ea:Ct8BCtwBEnQKH2F1dGhlbnRpY2F0aW9uYXV0aGVudGljYXRpb25LZXkQBEJPCglzZWNwMjU2azESIDS5zeYUkLCSAJLI6aLXRTPRxstCLPUEI6TgBrAVCHkwGiDk-ffklrHIFW7pKkT8i-YksXi-XXi5h31czUMaVClcpxJkCg9tYXN0ZXJtYXN0ZXJLZXkQAUJPCglzZWNwMjU2azESIDS5zeYUkLCSAJLI6aLXRTPRxstCLPUEI6TgBrAVCHkwGiDk-ffklrHIFW7pKkT8i-YksXi-XXi5h31czUMaVClcpw" ); - const privateKey = new Apollo().createPrivateKey({ + const privateKey = apollo.createPrivateKey({ type: Domain.KeyTypes.EC, curve: Domain.Curve.ED25519, }); await db.storePrismDID(did, 0, privateKey); }); + + it("Should store a Message", async () => { + const message = createMessage(); + const db = await Database.createEncrypted( + `${databaseName}${randomUUID()}`, + defaultPassword + ); + await db.start(); + await db.storeMessage(message); + const dbMesaage = await db.getMessage(message.id); + expect(dbMesaage).not.toBe(null); + expect(dbMesaage!.id).toBe(message.id); + }); + + it("Should return null if message is not found by id ", async () => { + const db = await Database.createEncrypted( + `${databaseName}${randomUUID()}`, + defaultPassword + ); + await db.start(); + const dbMesaage = await db.getMessage("notfound"); + expect(dbMesaage).toBe(null); + }); + + it("Should store multiple messages", async () => { + const db = await Database.createEncrypted( + `${databaseName}${randomUUID()}`, + defaultPassword + ); + await db.start(); + await db.storeMessages([createMessage(), createMessage()]); + }); + + it("Should store a peerDID", async () => { + const db = await Database.createEncrypted( + `${databaseName}${randomUUID()}`, + defaultPassword + ); + await db.start(); + const did = new Domain.DID( + "did", + "peer", + "2.Ez6LSms555YhFthn1WV8ciDBpZm86hK9tp83WojJUmxPGk1hZ.Vz6MkmdBjMyB4TS5UbbQw54szm8yvMMf1ftGV2sQVYAxaeWhE.SeyJpZCI6Im5ldy1pZCIsInQiOiJkbSIsInMiOiJodHRwczovL21lZGlhdG9yLnJvb3RzaWQuY2xvdWQiLCJhIjpbImRpZGNvbW0vdjIiXX0" + ); + await db.storePeerDID(did, [ + apollo.createPrivateKey({ + type: Domain.KeyTypes.EC, + curve: Domain.Curve.ED25519, + }), + apollo.createPrivateKey({ + type: Domain.KeyTypes.Curve25519, + curve: Domain.Curve.X25519, + }), + ]); + }); + + it("Should store a didPair", async () => { + const host = Domain.DID.fromString("did:prism:123456"); + const receiver = Domain.DID.fromString("did:prism:654321"); + const name = "example"; + const db = await Database.createEncrypted( + `${databaseName}${randomUUID()}`, + defaultPassword + ); + await db.start(); + await db.storeDIDPair(host, receiver, name); + }); + + it("Should get all the didPairs", async () => { + const host = Domain.DID.fromString("did:prism:123456"); + const receiver = Domain.DID.fromString("did:prism:654321"); + const name = "example"; + const db = await Database.createEncrypted( + `${databaseName}${randomUUID()}`, + defaultPassword + ); + await db.start(); + + expect((await db.getAllDidPairs()).length).toBe(0); + await db.storeDIDPair(host, receiver, name); + expect((await db.getAllDidPairs()).length).toBe(1); + }); + + it("Should get a did pair by its did", async () => { + const host = Domain.DID.fromString("did:prism:123456"); + const receiver = Domain.DID.fromString("did:prism:654321"); + const name = "example"; + const db = await Database.createEncrypted( + `${databaseName}${randomUUID()}`, + defaultPassword + ); + await db.start(); + await db.storeDIDPair(host, receiver, name); + expect(await db.getPairByDID(host)).not.toBe(null); + }); });