Skip to content

Commit

Permalink
feat(backup): introduce new schema to minimize backup length
Browse files Browse the repository at this point in the history
This commit adds a new backup schema designed to reduce the overall
length of backups. The new schema is implemented in version 0.0.2,
enhancing efficiency and reducing storage requirements.

Signed-off-by: Ehsan Mahmoudi <[email protected]>
  • Loading branch information
jeyem committed Nov 12, 2024
1 parent 5713cf9 commit a83ecba
Show file tree
Hide file tree
Showing 11 changed files with 628 additions and 355 deletions.
11 changes: 9 additions & 2 deletions src/domain/backup/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { Schema as v0_0_1 } from "./v0_0_1";
import { Schema as v0_0_2 } from "./v0_0_2";

/**
* All supported backup schemas
*/
export type Schema = v0_0_1;
export type Schema = v0_0_1 | v0_0_2;
export type Version = "0.0.1" | "0.0.2";

export const defaultVersion = "0.0.1";

export { v0_0_1 };
export const versions: Version[] = [
"0.0.1",
"0.0.2",
];

export { v0_0_1, v0_0_2 };
45 changes: 45 additions & 0 deletions src/domain/backup/v0_0_2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* Schema definition for Backup V0.0.2
*/
import * as TB from "@sinclair/typebox";

const credential = TB.Object({
recovery_id: TB.String(),
data: TB.String(),
});

const did = TB.Object({
did: TB.String(),
alias: TB.Optional(TB.String()),
});

const didpair = TB.Object({
holder: TB.String(),
recipient: TB.String(),
alias: TB.String(),
});

const key = TB.Object({
recovery_id: TB.String(),
key: TB.String(),
did: TB.Optional(TB.String()),
index: TB.Optional(TB.Number()),
});


export const Schema = TB.Object({
version: TB.Optional(TB.Literal("0.0.2")),
credentials: TB.Array(credential),
dids: TB.Array(did),
did_pairs: TB.Array(didpair),
keys: TB.Array(key),
});

export type Schema = TB.Static<typeof Schema>;

export namespace Schema {
export type Credential = TB.Static<typeof credential>;
export type DID = TB.Static<typeof did>;
export type DIDPair = TB.Static<typeof didpair>;
export type Key = TB.Static<typeof key>;
}
2 changes: 1 addition & 1 deletion src/domain/buildingBlocks/Pluto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export interface Pluto {
/**
* create a Backup object from the stored data
*/
backup(): Promise<Backup.Schema>;
backup(version?: Backup.Version): Promise<Backup.Schema>;

/**
* load the given data into the store
Expand Down
5 changes: 3 additions & 2 deletions src/pluto/Pluto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { PeerDID } from "../peer-did/PeerDID";
import { BackupManager } from "./backup/BackupManager";
import { PlutoRepositories, repositoryFactory } from "./repositories";
import { Arrayable, asArray } from "../utils";
import { Version } from "../domain/backup";


/**
Expand Down Expand Up @@ -121,8 +122,8 @@ export class Pluto implements Domain.Pluto {
}

/** Backups **/
backup() {
return this.BackupMgr.backup();
backup(version?: Version) {
return this.BackupMgr.backup(version);
}

restore(backup: Domain.Backup.Schema) {
Expand Down
14 changes: 9 additions & 5 deletions src/pluto/backup/BackupManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Pluto } from "../Pluto";
import { PlutoRepositories } from "../repositories";
import { isEmpty } from "../../utils";
import { IBackupTask, IRestoreTask } from "./versions/interfaces";
import { Version } from "../../domain/backup";

/**
* BackupManager
Expand All @@ -21,7 +22,7 @@ export class BackupManager {
* @param version - backup schema version
* @returns {Promise<Domain.Backup.Schema>}
*/
backup(version?: string) {
backup(version?: Version) {
const task = this.getBackupTask(version);
return task.run();
}
Expand All @@ -37,21 +38,24 @@ export class BackupManager {
await task.run();
}

private getBackupTask(version: string = Domain.Backup.defaultVersion): IBackupTask {
private getBackupTask(version: Version = Domain.Backup.defaultVersion): IBackupTask {
switch (version) {
case "0.0.1":
return new Versions.v0_0_1.BackupTask(this.Pluto, this.Repositories);
case "0.0.2":
return new Versions.v0_0_2.BackupTask(this.Pluto, this.Repositories);
}

throw new Domain.PlutoError.BackupNotFoundError();
}

private getRestoreTask(backup: Domain.Backup.Schema): IRestoreTask {
const version = backup.version ?? Domain.Backup.defaultVersion;

switch (version) {
backup.version = backup.version ?? Domain.Backup.defaultVersion;
switch (backup.version) {
case "0.0.1":
return new Versions.v0_0_1.RestoreTask(this.Pluto, backup);
case "0.0.2":
return new Versions.v0_0_2.RestoreTask(this.Pluto, backup);
}

throw new Domain.PlutoError.RestoreNotFoundError();
Expand Down
1 change: 0 additions & 1 deletion src/pluto/backup/versions/0_0_1/Backup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ export class BackupTask implements IBackupTask {
index: key.index,
did: did?.uuid,
};

return acc.concat(backup);
}

Expand Down
97 changes: 97 additions & 0 deletions src/pluto/backup/versions/0_0_2/Backup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import * as Domain from "../../../../domain";
import * as Models from "../../../models";
import { JWTVerifiableCredentialRecoveryId } from "../../../../pollux/models/JWTVerifiableCredential";
import { repositoryFactory } from "../../../repositories/builders/factory";
import { IBackupTask } from "../interfaces";
import { SDJWTVerifiableCredentialRecoveryId } from "../../../../pollux/models/SDJWTVerifiableCredential";
import { base64url } from "multiformats/bases/base64";

export class BackupTask implements IBackupTask {
constructor(
private readonly Pluto: Domain.Pluto,
private readonly Repositories: ReturnType<typeof repositoryFactory>
) {}

async run(): Promise<Domain.Backup.Schema> {
const credentials = await this.getCredentialBackups();
const didModels = await this.Repositories.DIDs.getModels();
const dids = didModels.map(this.mapDid);
const did_pairs = await this.getDidPairBackups();
const keys = await this.getKeyBackups(didModels);

const json: Domain.Backup.v0_0_2 = {
version: "0.0.2",
credentials,
dids,
did_pairs,
keys,
};

return json;
}

async getCredentialBackups(): Promise<Domain.Backup.v0_0_1.Credential[]> {
const credentialModels = await this.Repositories.Credentials.getModels();
return credentialModels.map(this.mapCredential);
}

async getDidPairBackups(): Promise<Domain.Backup.v0_0_1.DIDPair[]> {
const pairLinks = await this.Repositories.DIDLinks.getModels({ selector: { role: Models.DIDLink.role.pair } });
const didTuples = pairLinks.map<Domain.Backup.v0_0_1.DIDPair>(link => ({
alias: link.alias ?? "",
holder: link.hostId,
recipient: link.targetId
}));

return didTuples;
}

async getKeyBackups(didModels: Models.DID[]): Promise<Domain.Backup.v0_0_1.Key[]> {
const keys = await this.Repositories.Keys.get();
const didKeyLinks = await this.Repositories.DIDKeyLinks.getModels();

const backupKeys = keys.reduce<Domain.Backup.v0_0_1.Key[]>((acc, key) => {
if (key.isExportable() && key.isStorable()) {
const keyLink = didKeyLinks.find(x => x.keyId === key.uuid);
const did = didModels.find(x => x.uuid === keyLink?.didId);
const jwk = key.to.JWK();

const backup: Domain.Backup.v0_0_1.Key = {
recovery_id: key.recoveryId,
key: base64url.baseEncode(Buffer.from(JSON.stringify(jwk))),
index: key.index,
did: did?.uuid,
};
return acc.concat(backup);
}

return acc;
}, []);

return backupKeys;
}

async getLinkSecretBackup(): Promise<Domain.Backup.v0_0_1.LinkSecret> {
const linksecret = await this.Repositories.LinkSecrets.findOne();

return linksecret?.secret ?? undefined;
}


private mapCredential = (model: Models.Credential): Domain.Backup.v0_0_1.Credential => {
const isJWT = model.recoveryId === JWTVerifiableCredentialRecoveryId;
const isSDJWT = model.recoveryId === SDJWTVerifiableCredentialRecoveryId;
const recoveryId = isJWT ? "jwt" : isSDJWT ? "sdjwt" : "anoncred";
const data = isJWT || isSDJWT ? JSON.parse(model.dataJson).id : model.dataJson;

return {
recovery_id: recoveryId,
data: base64url.baseEncode(Buffer.from(data)),
};
};

private mapDid = (model: Models.DID): Domain.Backup.v0_0_1.DID => ({
did: model.uuid,
alias: model.alias
});
}
91 changes: 91 additions & 0 deletions src/pluto/backup/versions/0_0_2/Restore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import * as Domain from "../../../../domain";
import { Ed25519PrivateKey } from "../../../../apollo/utils/Ed25519PrivateKey";
import { Secp256k1PrivateKey } from "../../../../apollo/utils/Secp256k1PrivateKey";
import { X25519PrivateKey } from "../../../../apollo/utils/X25519PrivateKey";
import { AnonCredsCredential } from "../../../../pollux/models/AnonCredsVerifiableCredential";
import { JWTCredential } from "../../../../pollux/models/JWTVerifiableCredential";
import { notEmptyString, notNil, validate } from "../../../../utils";
import { IRestoreTask } from "../interfaces";
import { base64url } from "multiformats/bases/base64";

export class RestoreTask implements IRestoreTask {
constructor(
private readonly Pluto: Domain.Pluto,
private readonly backup: Domain.Backup.v0_0_2,
) { }

async run(): Promise<void> {
validate(this.backup, Domain.Backup.v0_0_2);
await this.restoreCredentials();
await this.restoreDids();
await this.restoreDidPairs();
await this.restoreKeys();
}

async restoreCredentials() {
const credentials = this.backup.credentials.map<Domain.Credential>(item => {
const decoded = Buffer.from(base64url.baseDecode(item.data)).toString()
if (item.recovery_id === "jwt") {
return JWTCredential.fromJWS(decoded);
}
if (item.recovery_id === "anoncred") {
return AnonCredsCredential.fromJson(decoded);
}
throw new Domain.PlutoError.RestoreCredentialInvalidError();
});

await Promise.all(credentials.map(x => this.Pluto.storeCredential(x)));
}

async restoreDids() {
await Promise.all(
this.backup.dids.map(x => this.Pluto.storeDID(Domain.DID.from(x.did), [], x.alias))
);
}

async restoreDidPairs() {
await Promise.all(
this.backup.did_pairs.map(item => {
const host = Domain.DID.from(item.holder);
const target = Domain.DID.from(item.recipient);
return this.Pluto.storeDIDPair(host, target, item.alias);
})
);
}

async restoreKeys() {
return Promise.all(this.backup.keys.map(item => {

const jwk = JSON.parse(Buffer.from(base64url.baseDecode(item.key)).toString());
const key = this.jwkToDomain(jwk);
if (notNil(item.index)) {
key.keySpecification.set(Domain.KeyProperties.index, item.index.toString());
}
if (notEmptyString(item.did)) {
return this.Pluto.storeDID(Domain.DID.from(item.did), key);
} else {
return this.Pluto.storePrivateKey(key);
}
})
)
}

private jwkToDomain(jwk: Domain.JWK): Domain.PrivateKey {
if ((jwk.kty === "OKP" || jwk.kty === "EC") && notEmptyString(jwk.d)) {
switch (jwk.crv) {
case Domain.Curve.SECP256K1.toLowerCase():
return Secp256k1PrivateKey.from.String(jwk.d, "base64url");

case Domain.Curve.ED25519:
return Ed25519PrivateKey.from.String(jwk.d, "base64url");

case Domain.Curve.X25519:
return X25519PrivateKey.from.String(jwk.d, "base64url");
}

throw new Domain.PlutoError.RestoreKeyInvalidError();
}

throw new Domain.PlutoError.RestoreJWKInvalidError();
}
}
2 changes: 2 additions & 0 deletions src/pluto/backup/versions/0_0_2/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./Backup";
export * from "./Restore";
3 changes: 2 additions & 1 deletion src/pluto/backup/versions/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as v0_0_1 from "./0_0_1";
import * as v0_0_2 from "./0_0_2";

export { v0_0_1 };
export { v0_0_1, v0_0_2 };
Loading

0 comments on commit a83ecba

Please sign in to comment.