Skip to content

Commit

Permalink
feat: Implementing JWT Credential revocation notification (#184)
Browse files Browse the repository at this point in the history
Signed-off-by: Francisco Javier Ribó Labrador <[email protected]>
Signed-off-by: Francisco Javier Ribo Labrador <[email protected]>
  • Loading branch information
elribonazo committed May 2, 2024
1 parent 11ad81f commit 9aa8b8b
Show file tree
Hide file tree
Showing 35 changed files with 1,176 additions and 239 deletions.
4 changes: 2 additions & 2 deletions demos/browser/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const RequestPresentation = SDK.RequestPresentation;

const apollo = new SDK.Apollo();
const castor = new SDK.Castor(apollo);
const defaultMediatorDID = "did:peer:2.Ez6LSghwSE437wnDE1pt3X6hVDUQzSjsHzinpX3XFvMjRAm7y.Vz6Mkhh1e5CEYYq6JBUcTZ6Cp2ranCWRrv7Yax3Le4N59R6dd.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6Imh0dHBzOi8vc2l0LXByaXNtLW1lZGlhdG9yLmF0YWxhcHJpc20uaW8iLCJhIjpbImRpZGNvbW0vdjIiXX19.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6IndzczovL3NpdC1wcmlzbS1tZWRpYXRvci5hdGFsYXByaXNtLmlvL3dzIiwiYSI6WyJkaWRjb21tL3YyIl19fQ";
const defaultMediatorDID = "did:peer:2.Ez6LSghwSE437wnDE1pt3X6hVDUQzSjsHzinpX3XFvMjRAm7y.Vz6Mkhh1e5CEYYq6JBUcTZ6Cp2ranCWRrv7Yax3Le4N59R6dd.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9iZXRhLW1lZGlhdG9yLmF0YWxhcHJpc20uaW8iLCJyIjpbXSwiYSI6WyJkaWRjb21tL3YyIl19";

const useSDK = (mediatorDID: SDK.Domain.DID, pluto: SDK.Domain.Pluto) => {
const agent = SDK.Agent.initialize({ mediatorDID, pluto });
Expand Down Expand Up @@ -388,7 +388,7 @@ const Agent: React.FC<{ pluto: SDK.Domain.Pluto; }> = props => {
if (requestPresentations.length) {
for (const requestPresentation of requestPresentations) {
const lastCredentials = await pluto.getAllCredentials();
const lastCredential = lastCredentials.at(-1);
const lastCredential = lastCredentials.at(0);
const requestPresentationMessage = RequestPresentation.fromMessage(requestPresentation);

try {
Expand Down
25 changes: 19 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/domain/buildingBlocks/Pluto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export interface Pluto {
/**
* Revoke a Credential
*/
revokeCredential(uuid: string): Promise<void>;
revokeCredential(credential: Credential): Promise<void>;

/**
* Delete a previously stored messages
Expand Down
5 changes: 5 additions & 0 deletions src/domain/models/Credential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export abstract class Credential implements Pluto.Storable {
isStorable(): this is StorableCredential {
return "toStorable" in this;
}

isRevoked() {
const revoked = this.properties.get("revoked");
return revoked === true
}
}

export interface ProvableCredential {
Expand Down
15 changes: 11 additions & 4 deletions src/pluto/Pluto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,6 @@ export class Pluto implements Domain.Pluto {
this.Repositories = repositoryFactory(store, keyRestoration);
}

revokeCredential(uuid: string): Promise<void> {
throw new Error("Method not implemented.");
}

async deleteMessage(id: string): Promise<void> {
const message = await this.Repositories.Messages.findOne({ id });
//TODO: Improve error handling
Expand All @@ -121,6 +117,7 @@ export class Pluto implements Domain.Pluto {
}
}


async start(): Promise<void> {
if (this.store.start !== undefined) {
await this.store.start();
Expand All @@ -138,6 +135,16 @@ export class Pluto implements Domain.Pluto {
}


async revokeCredential(credential: Domain.Credential): Promise<void> {
if (!credential || !credential.isStorable()) {
throw new Error("Credential not found or invalid")
}
credential.properties.set("revoked", true);
const credentialModel = await this.Repositories.Credentials.toModel(credential)
await this.Repositories.Credentials.update(credentialModel)
}


/** Credential Metadata **/

async storeCredentialMetadata(metadata: Domain.CredentialMetadata): Promise<void> {
Expand Down
40 changes: 39 additions & 1 deletion src/pluto/models/Credential.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import * as sha256 from '@stablelib/sha256';

import { MigrationStrategies } from "rxdb";
import type { Model } from "./Model";
import { schemaFactory } from "./Schema";
import { JWTVerifiableCredentialRecoveryId } from "../../pollux/models/JWTVerifiableCredential";
import { AnonCredsRecoveryId } from "../../pollux/models/AnonCredsVerifiableCredential";
import { PlutoError } from '../../domain';

/**
* Definition for Storable Credential model
Expand Down Expand Up @@ -27,10 +33,10 @@ export interface Credential extends Model {
validUntil?: string;
revoked?: boolean;
// availableClaims?: string[];
id: string;
}

export const CredentialSchema = schemaFactory<Credential>(schema => {
schema.setRequired("recoveryId", "dataJson");
schema.addProperty("string", "recoveryId");
schema.addProperty("string", "dataJson");

Expand All @@ -43,4 +49,36 @@ export const CredentialSchema = schemaFactory<Credential>(schema => {
schema.addProperty("boolean", "revoked");

schema.setEncrypted("dataJson");
schema.setVersion(1);

//V1
schema.addProperty("string", "id");
schema.setRequired("recoveryId", "dataJson", "id");

});

export const CredentialMigration: MigrationStrategies = {
1: function (document) {
const recoveryId = document.recoveryId;
if (recoveryId == JWTVerifiableCredentialRecoveryId) {
const jwtObj = JSON.parse(document.dataJson);
return {
...document,
id: jwtObj.id
}
}
if (recoveryId == AnonCredsRecoveryId) {
const anoncredsObject = JSON.parse(document.dataJson);
if (anoncredsObject.revoked !== undefined) {
delete anoncredsObject.revoked;
}
const anoncredsStr = JSON.stringify(anoncredsObject)
return {
...document,
id: Buffer.from(sha256.hash(Buffer.from(anoncredsStr))).toString('hex')
}

}
throw new PlutoError.UnknownCredentialTypeError();
}
}
16 changes: 9 additions & 7 deletions src/pluto/models/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ interface Property {

type StringKeys<T> = Exclude<Extract<keyof T, string>, "uuid">;
type KeysOf<T, X> = { [K in keyof T]-?: X extends T[K] ? K : never; }[StringKeys<T>];
type KeysFor<T, P extends PropertyTypes> = P extends "number"
? KeysOf<T, number>
: P extends "string"
? KeysOf<T, string>
: P extends "boolean"
? KeysOf<T, boolean>
: never;
type KeysFor<T, P extends PropertyTypes> = P extends "number"
? KeysOf<T, number>
: P extends "string"
? KeysOf<T, string>
: P extends "boolean"
? KeysOf<T, boolean>
: never;

type PropertyTypes = "boolean" | "number" | "string";

Expand All @@ -29,6 +29,7 @@ interface SchemaGenerator<T> {
addProperty(type: PropertyTypes, key: string, opts?: any): void;
setEncrypted(...keys: StringKeys<T>[]): void;
setRequired(...keys: StringKeys<T>[]): void;
setVersion(version: number): void;
}

/**
Expand Down Expand Up @@ -57,6 +58,7 @@ export const schemaFactory = <T>(generator: (schema: SchemaGenerator<T>) => void
addProperty: (type: any, key: string, opts = {}) => { schema.properties[key] = { type, ...opts }; },
setEncrypted: (...keys) => { schema.encrypted.push(...keys); },
setRequired: (...keys) => { schema.required.push(...keys); },
setVersion: (version) => { schema.version = version; }
});

return schema;
Expand Down
17 changes: 11 additions & 6 deletions src/pluto/repositories/CredentialRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,19 @@ export class CredentialRepository extends MapperRepository<Models.Credential, Do
switch (model.recoveryId) {
case JWTVerifiableCredentialRecoveryId: {
const jwtObj = JSON.parse(model.dataJson);
const credential = JWTCredential.fromJWT(jwtObj, jwtObj.id);
const credential = JWTCredential.fromJWT(
jwtObj,
jwtObj.id,
jwtObj.revoked ?? false
);
return this.withId(credential, model.uuid);
}
case AnonCredsRecoveryId: {
const json = JSON.parse(model.dataJson);
const credential = new AnonCredsCredential(json);
const credential = new AnonCredsCredential(
json,
json.revoked ?? false
);
return this.withId(credential, model.uuid);
}
}
Expand All @@ -31,21 +38,19 @@ export class CredentialRepository extends MapperRepository<Models.Credential, Do
if (!credential.isStorable()) {
throw new Domain.PlutoError.CredentialNotStorable();
}

const item = credential.toStorable();

return {
uuid: credential.uuid,
id: item.id,
recoveryId: credential.recoveryId,
dataJson: item.credentialData,

issuer: item.issuer,
subject: item.subject,
credentialCreated: item.credentialCreated,
credentialUpdated: item.credentialUpdated,
credentialSchema: item.credentialSchema,
validUntil: item.validUntil,
revoked: item.revoked
revoked: item.revoked ?? false
};
}
}
35 changes: 22 additions & 13 deletions src/pluto/rxdb/Store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { Pluto } from "../Pluto";
import { Model } from '../models';
import { RxDBEncryptedMigrationPlugin } from '../migration';
import { Domain } from '../..';

import { RxDBUpdatePlugin } from 'rxdb/plugins/update';
export class RxdbStore implements Pluto.Store {
private _db?: RxDatabase<CollectionsOfDatabase, any, any>;

Expand All @@ -17,6 +17,7 @@ export class RxdbStore implements Pluto.Store {
addRxPlugin(RxDBQueryBuilderPlugin);
addRxPlugin(RxDBJsonDumpPlugin);
addRxPlugin(RxDBEncryptedMigrationPlugin);
addRxPlugin(RxDBUpdatePlugin);
}


Expand All @@ -32,18 +33,26 @@ export class RxdbStore implements Pluto.Store {
* Start the database and build collections
*/
async start(): Promise<void> {
if (!this._db) {
this._db = await createRxDatabase({
...this.options,
multiInstance: true
});
const collections = makeCollections(this.collections);
await this._db.addCollections(collections);
}
this._db = await createRxDatabase({
...this.options,
multiInstance: true
});
const collections = makeCollections(this.collections ?? {});
await this._db.addCollections(collections);
}

update<T extends Domain.Pluto.Storable>(name: string, model: T): Promise<void> {
throw new Error('Method not implemented.');
async update<T extends Domain.Pluto.Storable>(name: string, model: T): Promise<void> {
const table = this.getCollection(name);
const row = await table.findOne({
selector: {
uuid: model.uuid
}
}).exec();
if (row) {

//Improve error handling when not found
await row.patch(model)
}
}

async delete(name: string, uuid: string): Promise<void> {
Expand All @@ -53,7 +62,7 @@ export class RxdbStore implements Pluto.Store {
uuid: uuid
}
})
//TODO: Improve error handling
//TODO: Improve error handling, specially when not found
await row?.remove();
}

Expand Down Expand Up @@ -93,4 +102,4 @@ export class RxdbStore implements Pluto.Store {

await removeRxDatabase(this.options.name, this.db.storage);
}
}
}
7 changes: 4 additions & 3 deletions src/pluto/rxdb/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import * as Models from "../models";

export type CollectionList = Record<string, RxCollectionCreator>;

export const makeCollections = (additional: CollectionList = {}) => ({
"credentials": { schema: Models.CredentialSchema },
type MakeCollections = (additional?: CollectionList) => CollectionList
export const makeCollections: MakeCollections = (additional: CollectionList = {}) => ({
"credentials": { schema: Models.CredentialSchema, migrationStrategies: Models.CredentialMigration },
"credential-metadata": { schema: Models.CredentialMetadataSchema },
"didkey-link": { schema: Models.DIDKeyLinkSchema },
"did-link": { schema: Models.DIDLinkSchema },
"dids": { schema: Models.DIDSchema },
"keys": { schema: Models.KeySchema },
"messages": { schema: Models.MessageSchema },
...additional,
...(additional),
});
2 changes: 1 addition & 1 deletion src/pollux/Pollux.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export default class Pollux implements IPollux {
constructor(
private castor: Castor,
private api: Api = new ApiImpl()
) {}
) { }

async start() {
this._anoncreds = await AnoncredsLoader.getInstance();
Expand Down
Loading

0 comments on commit 9aa8b8b

Please sign in to comment.