forked from mirrors/firefish
142 lines
3.5 KiB
TypeScript
142 lines
3.5 KiB
TypeScript
import * as crypto from "node:crypto";
|
|
import jsonld from "jsonld";
|
|
import { CONTEXTS } from "./contexts.js";
|
|
import fetch from "node-fetch";
|
|
import { httpAgent, httpsAgent } from "@/misc/fetch.js";
|
|
|
|
// RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017
|
|
|
|
export class LdSignature {
|
|
public debug = false;
|
|
public preLoad = true;
|
|
public loderTimeout = 10 * 1000;
|
|
|
|
public async signRsaSignature2017(
|
|
data: any,
|
|
privateKey: string,
|
|
creator: string,
|
|
domain?: string,
|
|
created?: Date,
|
|
): Promise<any> {
|
|
const options = {
|
|
type: "RsaSignature2017",
|
|
creator,
|
|
domain,
|
|
nonce: crypto.randomBytes(16).toString("hex"),
|
|
created: (created || new Date()).toISOString(),
|
|
} as {
|
|
type: string;
|
|
creator: string;
|
|
domain?: string;
|
|
nonce: string;
|
|
created: string;
|
|
};
|
|
|
|
if (!domain) {
|
|
options.domain = undefined;
|
|
}
|
|
|
|
const toBeSigned = await this.createVerifyData(data, options);
|
|
|
|
const signer = crypto.createSign("sha256");
|
|
signer.update(toBeSigned);
|
|
signer.end();
|
|
|
|
const signature = signer.sign(privateKey);
|
|
|
|
return {
|
|
...data,
|
|
signature: {
|
|
...options,
|
|
signatureValue: signature.toString("base64"),
|
|
},
|
|
};
|
|
}
|
|
|
|
public async verifyRsaSignature2017(
|
|
data: any,
|
|
publicKey: string,
|
|
): Promise<boolean> {
|
|
const toBeSigned = await this.createVerifyData(data, data.signature);
|
|
const verifier = crypto.createVerify("sha256");
|
|
verifier.update(toBeSigned);
|
|
return verifier.verify(publicKey, data.signature.signatureValue, "base64");
|
|
}
|
|
|
|
public async createVerifyData(data: any, options: any) {
|
|
const transformedOptions = {
|
|
...options,
|
|
"@context": "https://w3id.org/identity/v1",
|
|
};
|
|
transformedOptions["type"] = undefined;
|
|
transformedOptions["id"] = undefined;
|
|
transformedOptions["signatureValue"] = undefined;
|
|
const canonizedOptions = await this.normalize(transformedOptions);
|
|
const optionsHash = this.sha256(canonizedOptions);
|
|
const transformedData = { ...data };
|
|
transformedData["signature"] = undefined;
|
|
const cannonidedData = await this.normalize(transformedData);
|
|
if (this.debug) console.debug(`cannonidedData: ${cannonidedData}`);
|
|
const documentHash = this.sha256(cannonidedData);
|
|
const verifyData = `${optionsHash}${documentHash}`;
|
|
return verifyData;
|
|
}
|
|
|
|
public async normalize(data: any) {
|
|
const customLoader = this.getLoader();
|
|
return await jsonld.normalize(data, {
|
|
documentLoader: customLoader,
|
|
});
|
|
}
|
|
|
|
private getLoader() {
|
|
return async (url: string): Promise<any> => {
|
|
if (!url.match("^https?://")) throw new Error(`Invalid URL ${url}`);
|
|
|
|
if (this.preLoad) {
|
|
if (url in CONTEXTS) {
|
|
if (this.debug) console.debug(`HIT: ${url}`);
|
|
return {
|
|
contextUrl: null,
|
|
document: CONTEXTS[url],
|
|
documentUrl: url,
|
|
};
|
|
}
|
|
}
|
|
|
|
if (this.debug) console.debug(`MISS: ${url}`);
|
|
const document = await this.fetchDocument(url);
|
|
return {
|
|
contextUrl: null,
|
|
document: document,
|
|
documentUrl: url,
|
|
};
|
|
};
|
|
}
|
|
|
|
private async fetchDocument(url: string) {
|
|
const json = await fetch(url, {
|
|
headers: {
|
|
Accept: "application/ld+json, application/json",
|
|
},
|
|
// TODO
|
|
//timeout: this.loderTimeout,
|
|
agent: (u) => (u.protocol === "http:" ? httpAgent : httpsAgent),
|
|
}).then((res) => {
|
|
if (!res.ok) {
|
|
throw new Error(`${res.status} ${res.statusText}`);
|
|
} else {
|
|
return res.json();
|
|
}
|
|
});
|
|
|
|
return json;
|
|
}
|
|
|
|
public sha256(data: string): string {
|
|
const hash = crypto.createHash("sha256");
|
|
hash.update(data);
|
|
return hash.digest("hex");
|
|
}
|
|
}
|