diff --git a/packages/pkcs11/README.md b/packages/pkcs11/README.md new file mode 100644 index 0000000..16efcd7 --- /dev/null +++ b/packages/pkcs11/README.md @@ -0,0 +1,11 @@ +# `@peculiar/webcrypto-pkcs11` + +> TODO: description + +## Usage + +``` +const webcryptoPkcs11 = require('@peculiar/webcrypto-pkcs11'); + +// TODO: DEMONSTRATE API +``` diff --git a/packages/pkcs11/package.json b/packages/pkcs11/package.json new file mode 100644 index 0000000..3379c2d --- /dev/null +++ b/packages/pkcs11/package.json @@ -0,0 +1,38 @@ +{ + "name": "@peculiar/webcrypto-pkcs11", + "version": "3.0.0", + "description": "Now I’m the model of a modern major general / The venerated Virginian veteran whose men are all / Lining up, to put me up on a pedestal / Writin’ letters to relatives / Embellishin’ my elegance and eloquence / But the elephant is in the room / The truth is in ya face when ya hear the British cannons go / BOOM", + "keywords": [], + "author": "microshine ", + "homepage": "https://github.com/PeculiarVentures/webcrypto/tree/master/packages/webcrypto-pkcs11#readme", + "license": "ISC", + "main": "lib/webcrypto-pkcs11.js", + "directories": { + "lib": "lib", + "test": "__tests__" + }, + "files": [ + "lib" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/PeculiarVentures/webcrypto.git" + }, + "scripts": { + "test": "echo \"Error: run tests from root\" && exit 1" + }, + "bugs": { + "url": "https://github.com/PeculiarVentures/webcrypto/issues" + }, + "dependencies": { + "@peculiar/webcrypto-core": "^3.0.0", + "@peculiar/webcrypto-types": "^3.0.0", + "@peculiar/x509": "^1.6.4", + "graphene-pk11": "^2.3.0", + "pvtsutils": "^1.3.2", + "tslib": "^2.4.0" + } +} \ No newline at end of file diff --git a/packages/pkcs11/src/assert.ts b/packages/pkcs11/src/assert.ts new file mode 100644 index 0000000..d50641d --- /dev/null +++ b/packages/pkcs11/src/assert.ts @@ -0,0 +1,44 @@ +import * as graphene from "graphene-pk11"; +import { CryptoKey } from "./key"; + +export class Assert { + /** + * Throws exception whenever data is not an instance of Session + * @param data + * @throws TypeError + */ + public static isSession(data: any): asserts data is graphene.Session { + if (!(data instanceof graphene.Session)) { + throw new TypeError("PKCS#11 session is not initialized"); + } + } + + /** + * Throws exception whenever data is not an instance of Module + * @param data + * @throws TypeError + */ + public static isModule(data: any): asserts data is graphene.Module { + if (!(data instanceof graphene.Module)) { + throw new TypeError("PKCS#11 module is not initialized"); + } + } + + /** + * Throws exception whenever data is not an instance of PKCS#11 CryptoKey + * @param data + * @throws TypeError + */ + public static isCryptoKey(data: any): asserts data is CryptoKey { + if (!(data instanceof CryptoKey)) { + throw new TypeError("Object is not an instance of PKCS#11 CryptoKey"); + } + } + + public static requiredParameter(parameter: any, parameterName: string): asserts parameter { + if (!parameter) { + throw new Error(`Absent mandatory parameter \"${parameterName}\"`); + } + } + +} diff --git a/packages/pkcs11/src/cert_storage.ts b/packages/pkcs11/src/cert_storage.ts new file mode 100644 index 0000000..935b999 --- /dev/null +++ b/packages/pkcs11/src/cert_storage.ts @@ -0,0 +1,244 @@ +import * as core from "@peculiar/webcrypto-core"; +import * as types from "@peculiar/webcrypto-types"; +import * as graphene from "graphene-pk11"; +import * as pvtsutils from "pvtsutils"; + +import * as certs from "./certs"; +import { Crypto } from "./crypto"; +import { Pkcs11Object } from "./p11_object"; + +const TEMPLATES = [ + { class: graphene.ObjectClass.CERTIFICATE, certType: graphene.CertificateType.X_509, token: true }, + { class: graphene.ObjectClass.DATA, token: true, label: "X509 Request" }, +]; + +export interface IGetValue { + /** + * Returns item blob + * @param key Object identifier + */ + getValue(key: string): Promise; +} +export class CertificateStorage implements types.CryptoCertificateStorage, IGetValue { + + protected crypto: Crypto; + + constructor(crypto: Crypto) { + this.crypto = crypto; + } + + public async getValue(key: string): Promise { + const storageObject = this.getItemById(key); + if (storageObject instanceof graphene.X509Certificate) { + const x509Object = storageObject.toType(); + const x509 = new certs.X509Certificate(this.crypto); + x509.p11Object = x509Object; + return x509.exportCert(); + } else if (storageObject instanceof graphene.Data) { + const x509Object = storageObject.toType(); + const x509request = new certs.X509CertificateRequest(this.crypto); + x509request.p11Object = x509Object; + return x509request.exportCert(); + } + return null; + } + + public indexOf(item: types.CryptoCertificate): Promise; + public async indexOf(item: certs.CryptoCertificate) { + if (item instanceof certs.CryptoCertificate && item.p11Object?.token) { + return certs.CryptoCertificate.getID(item.p11Object); + } + return null; + } + + public async keys() { + const keys: string[] = []; + TEMPLATES.forEach((template) => { + this.crypto.session!.find(template, (obj) => { + const item = obj.toType(); + const id = certs.CryptoCertificate.getID(item); + keys.push(id); + }); + }); + return keys; + } + + public async clear() { + const objects: graphene.SessionObject[] = []; + TEMPLATES.forEach((template) => { + this.crypto.session!.find(template, (obj) => { + objects.push(obj); + }); + }); + objects.forEach((obj) => { + obj.destroy(); + }); + } + + public async hasItem(item: types.CryptoCertificate) { + if (!(item instanceof certs.CryptoCertificate)) { + throw new TypeError(`Parameter 'item' is not of type 'CryptoCertificate'`); + } + + const sessionObject = this.getItemById(item.id); + + return !!sessionObject; + } + + public getItem(index: string): Promise; + public getItem(index: string, algorithm: types.ImportAlgorithms, keyUsages: types.KeyUsage[]): Promise; + public async getItem(index: string, algorithm?: types.Algorithm, usages?: types.KeyUsage[]): Promise { + const storageObject = this.getItemById(index); + if (storageObject instanceof graphene.X509Certificate) { + const x509Object = storageObject.toType(); + const x509 = new certs.X509Certificate(this.crypto); + x509.p11Object = x509Object; + if (algorithm && usages) { + await x509.exportKey(algorithm, usages); + } else { + await x509.exportKey(); + } + return x509; + } else if (storageObject instanceof graphene.Data) { + const x509Object = storageObject.toType(); + const x509request = new certs.X509CertificateRequest(this.crypto); + x509request.p11Object = x509Object; + if (algorithm && usages) { + await x509request.exportKey(algorithm, usages); + } else { + await x509request.exportKey(); + } + return x509request; + } else { + // @ts-ignore + return null; + } + } + + public async removeItem(key: string) { + const sessionObject = this.getItemById(key); + if (sessionObject) { + sessionObject.destroy(); + } + } + + public async setItem(data: types.CryptoCertificate): Promise; + public async setItem(data: certs.CryptoCertificate) { + if (!(data instanceof certs.CryptoCertificate)) { + throw new Error("Incoming data is not PKCS#11 CryptoCertificate"); + } + Pkcs11Object.assertStorage(data.p11Object); + + // don't copy object from token + if (!data.p11Object.token) { + const template = this.crypto.templateBuilder.build({ + action: "copy", + type: data.type, + attributes: { + token: true, + } + }); + const obj = this.crypto.session.copy(data.p11Object, template); + return certs.CryptoCertificate.getID(obj.toType()); + } else { + return data.id; + } + } + + public exportCert(format: types.CryptoCertificateFormat, item: types.CryptoCertificate): Promise; + public exportCert(format: "raw", item: types.CryptoCertificate): Promise; + public exportCert(format: "pem", item: types.CryptoCertificate): Promise; + public async exportCert(format: types.CryptoCertificateFormat, cert: certs.CryptoCertificate): Promise { + switch (format) { + case "pem": { + throw Error("PEM format is not implemented"); + } + case "raw": { + return cert.exportCert(); + } + default: + throw new Error(`Unsupported format in use ${format}`); + } + } + + public async importCert(format: types.CryptoCertificateFormat, data: types.BufferSource | string, algorithm: certs.Pkcs11ImportAlgorithms, keyUsages: types.KeyUsage[]): Promise; + public async importCert(format: "raw", data: types.BufferSource, algorithm: certs.Pkcs11ImportAlgorithms, keyUsages: types.KeyUsage[]): Promise; + public async importCert(format: "pem", data: string, algorithm: certs.Pkcs11ImportAlgorithms, keyUsages: types.KeyUsage[]): Promise; + public async importCert(format: types.CryptoCertificateFormat, data: types.BufferSource | string, algorithm: certs.Pkcs11ImportAlgorithms, usages: types.KeyUsage[]): Promise { + let rawData: ArrayBuffer; + let rawType: types.CryptoCertificateType | null = null; + + //#region Check + switch (format) { + case "pem": + if (typeof data !== "string") { + throw new TypeError("data: Is not type string"); + } + if (core.PemConverter.isCertificate(data)) { + rawType = "x509"; + } else if (core.PemConverter.isCertificateRequest(data)) { + rawType = "request"; + } else { + throw new core.OperationError("data: Is not correct PEM data. Must be Certificate or Certificate Request"); + } + rawData = core.PemConverter.toArrayBuffer(data); + break; + case "raw": + if (!pvtsutils.BufferSourceConverter.isBufferSource(data)) { + throw new TypeError("data: Is not type ArrayBuffer or ArrayBufferView"); + } + rawData = pvtsutils.BufferSourceConverter.toArrayBuffer(data); + break; + default: + throw new TypeError("format: Is invalid value. Must be 'raw', 'pem'"); + } + //#endregion + switch (rawType) { + case "x509": { + const x509 = new certs.X509Certificate(this.crypto); + await x509.importCert(Buffer.from(rawData), algorithm, usages); + return x509; + } + case "request": { + const request = new certs.X509CertificateRequest(this.crypto); + await request.importCert(Buffer.from(rawData), algorithm, usages); + return request; + } + default: { + try { + const x509 = new certs.X509Certificate(this.crypto); + await x509.importCert(Buffer.from(rawData), algorithm, usages); + return x509; + } catch { + // nothing + } + + try { + const request = new certs.X509CertificateRequest(this.crypto); + await request.importCert(Buffer.from(rawData), algorithm, usages); + return request; + } catch { + // nothing + } + + throw new core.OperationError("Cannot parse Certificate or Certificate Request from incoming ASN1"); + } + } + } + + protected getItemById(id: string): graphene.SessionObject | null { + + let object: graphene.SessionObject | null = null; + TEMPLATES.forEach((template) => { + this.crypto.session!.find(template, (obj) => { + const item = obj.toType(); + if (id === certs.CryptoCertificate.getID(item)) { + object = item; + return false; + } + }); + }); + return object; + } + +} diff --git a/packages/pkcs11/src/certs/cert.ts b/packages/pkcs11/src/certs/cert.ts new file mode 100644 index 0000000..63c628e --- /dev/null +++ b/packages/pkcs11/src/certs/cert.ts @@ -0,0 +1,75 @@ +import * as graphene from "graphene-pk11"; +import * as types from "@peculiar/webcrypto-types"; + +import { Crypto } from "../crypto"; +import { CryptoKey } from "../key"; +import { Pkcs11Object } from "../p11_object"; +import { Pkcs11Params } from "../types"; + +export interface Pkcs11CryptoCertificate extends CryptoCertificate { + token: boolean; + sensitive: boolean; + label: string; +} + +export type Pkcs11ImportAlgorithms = types.ImportAlgorithms & Pkcs11Params; + +export abstract class CryptoCertificate extends Pkcs11Object implements Pkcs11CryptoCertificate { + public crypto: Crypto; + + public static getID(p11Object: graphene.Storage) { + let type: string | undefined; + let id: Buffer | undefined; + if (p11Object instanceof graphene.Data) { + type = "request"; + id = p11Object.objectId; + } else if (p11Object instanceof graphene.X509Certificate) { + type = "x509"; + id = p11Object.id; + } + if (!type || !id) { + throw new Error("Unsupported PKCS#11 object"); + } + return `${type}-${p11Object.handle.toString("hex")}-${id.toString("hex")}`; + } + + public get id() { + Pkcs11Object.assertStorage(this.p11Object); + + return CryptoCertificate.getID(this.p11Object); + } + + public type: types.CryptoCertificateType = "x509"; + public publicKey!: CryptoKey; + + public get token() { + try { + Pkcs11Object.assertStorage(this.p11Object); + return this.p11Object.token; + } catch { /* nothing */ } + return false; + } + + public get sensitive() { + return false; + } + + public get label() { + try { + Pkcs11Object.assertStorage(this.p11Object); + return this.p11Object.label; + } catch { /* nothing */ } + return ""; + } + + public constructor(crypto: Crypto) { + super(); + this.crypto = crypto; + } + + public abstract importCert(data: Buffer, algorithm: Pkcs11ImportAlgorithms, keyUsages: string[]): Promise; + public abstract exportCert(): Promise; + public abstract exportKey(): Promise; + public abstract exportKey(algorithm: types.Algorithm, usages: types.KeyUsage[]): Promise; + +} diff --git a/packages/pkcs11/src/certs/csr.ts b/packages/pkcs11/src/certs/csr.ts new file mode 100644 index 0000000..01c6175 --- /dev/null +++ b/packages/pkcs11/src/certs/csr.ts @@ -0,0 +1,114 @@ +import * as core from "@peculiar/webcrypto-core"; +import * as types from "@peculiar/webcrypto-types"; +import * as x509 from "@peculiar/x509"; +import * as graphene from "graphene-pk11"; +import * as pvtsutils from "pvtsutils"; + +import { CryptoKey } from "../key"; +import { Pkcs11Object } from "../p11_object"; +import { CryptoCertificate, Pkcs11ImportAlgorithms } from "./cert"; + +export class X509CertificateRequest extends CryptoCertificate implements types.CryptoX509CertificateRequest { + + public get subjectName() { + return this.getData()?.subject; + } + public override type: "request" = "request"; + public override p11Object?: graphene.Data; + public csr?: x509.Pkcs10CertificateRequest; + + public get value(): ArrayBuffer { + Pkcs11Object.assertStorage(this.p11Object); + + return new Uint8Array(this.p11Object.value).buffer as ArrayBuffer; + } + + /** + * Creates new CertificateRequest in PKCS11 session + * @param data + * @param algorithm + * @param keyUsages + */ + public async importCert(data: Buffer, algorithm: Pkcs11ImportAlgorithms, keyUsages: types.KeyUsage[]) { + const array = new Uint8Array(data).buffer as ArrayBuffer; + this.parse(array); + + + + const { token, label, sensitive, ...keyAlg } = algorithm; // remove custom attrs for key + this.publicKey = await this.getData().publicKey.export(keyAlg, keyUsages, this.crypto) as CryptoKey; + + const hashSPKI = this.publicKey.p11Object.id; + + const template = this.crypto.templateBuilder.build({ + action: "import", + type: "request", + attributes: { + id: hashSPKI, + label: algorithm.label || "X509 Request", + token: !!(algorithm.token), + }, + }); + + // set data attributes + template.value = Buffer.from(data); + + this.p11Object = this.crypto.session.create(template).toType(); + } + + public async exportCert() { + return this.value; + } + + public toJSON() { + return { + publicKey: this.publicKey.toJSON(), + subjectName: this.subjectName, + type: this.type, + value: pvtsutils.Convert.ToBase64Url(this.value), + }; + } + + public async exportKey(): Promise; + public async exportKey(algorithm: types.Algorithm, usages: types.KeyUsage[]): Promise; + public async exportKey(algorithm?: types.Algorithm, usages?: types.KeyUsage[]) { + if (!this.publicKey) { + const publicKeyID = this.id.replace(/\w+-\w+-/i, ""); + const keyIndexes = await this.crypto.keyStorage.keys(); + for (const keyIndex of keyIndexes) { + const parts = keyIndex.split("-"); + if (parts[0] === "public" && parts[2] === publicKeyID) { + if (algorithm && usages) { + this.publicKey = await this.crypto.keyStorage.getItem(keyIndex, algorithm, true, usages); + } else { + this.publicKey = await this.crypto.keyStorage.getItem(keyIndex); + } + break; + } + } + if (!this.publicKey) { + if (algorithm && usages) { + this.publicKey = await this.getData().publicKey.export(algorithm, usages, this.crypto) as CryptoKey; + } else { + this.publicKey = await this.getData().publicKey.export(this.crypto) as CryptoKey; + } + } + } + return this.publicKey; + } + + protected parse(data: ArrayBuffer) { + this.csr = new x509.Pkcs10CertificateRequest(data); + } + + /** + * returns parsed ASN1 value + */ + protected getData() { + if (!this.csr) { + this.parse(this.value); + } + return this.csr!; + } + +} diff --git a/packages/pkcs11/src/certs/index.ts b/packages/pkcs11/src/certs/index.ts new file mode 100644 index 0000000..c2916c9 --- /dev/null +++ b/packages/pkcs11/src/certs/index.ts @@ -0,0 +1,3 @@ +export * from "./cert" +export * from "./csr" +export * from "./x509" diff --git a/packages/pkcs11/src/certs/x509.ts b/packages/pkcs11/src/certs/x509.ts new file mode 100644 index 0000000..f67d140 --- /dev/null +++ b/packages/pkcs11/src/certs/x509.ts @@ -0,0 +1,144 @@ +import * as asn1Schema from "@peculiar/asn1-schema"; +import * as asnX509 from "@peculiar/asn1-x509"; +import * as core from "@peculiar/webcrypto-core"; +import * as types from "@peculiar/webcrypto-types"; +import * as x509 from "@peculiar/x509"; +import * as graphene from "graphene-pk11"; +import * as pvtsutils from "pvtsutils"; + +import { CryptoKey } from "../key"; +import { Pkcs11Object } from "../p11_object"; +import { CryptoCertificate, Pkcs11ImportAlgorithms } from "./cert"; + +export class X509Certificate extends CryptoCertificate implements types.CryptoX509Certificate { + + public get serialNumber() { + return this.getData().serialNumber; + } + public get notBefore() { + return this.getData().notBefore; + } + public get notAfter() { + return this.getData().notAfter; + } + public get issuerName() { + return this.getData().issuer; + } + public get subjectName() { + return this.getData().subject; + } + public override type: "x509" = "x509"; + + public get value(): ArrayBuffer { + Pkcs11Object.assertStorage(this.p11Object); + return new Uint8Array(this.p11Object.value).buffer; + } + + public override p11Object?: graphene.X509Certificate; + protected x509?: x509.X509Certificate; + + public async importCert(data: Buffer, algorithm: Pkcs11ImportAlgorithms, keyUsages: types.KeyUsage[]) { + const array = new Uint8Array(data); + this.parse(array.buffer as ArrayBuffer); + + const { token, label, sensitive, ...keyAlg } = algorithm; // remove custom attrs for key + this.publicKey = await this.getData().publicKey.export(keyAlg, keyUsages, this.crypto); + + const hashSPKI = this.publicKey.p11Object.id; + + const certLabel = this.getName(); + + const template = this.crypto.templateBuilder.build({ + action: "import", + type: "x509", + attributes: { + id: hashSPKI, + label: algorithm.label || certLabel, + token: !!(algorithm.token), + }, + }); + + // set X509 attributes + template.value = Buffer.from(data); + const asn = asn1Schema.AsnConvert.parse(data, asnX509.Certificate); + template.serial = Buffer.from(asn1Schema.AsnConvert.serialize(core.asn1.AsnIntegerArrayBufferConverter.toASN(asn.tbsCertificate.serialNumber))); + template.subject = Buffer.from(asn1Schema.AsnConvert.serialize(asn.tbsCertificate.subject)); + template.issuer = Buffer.from(asn1Schema.AsnConvert.serialize(asn.tbsCertificate.issuer)); + + this.p11Object = this.crypto.session.create(template).toType(); + } + + public async exportCert() { + return this.value; + } + + public toJSON() { + return { + publicKey: this.publicKey.toJSON(), + notBefore: this.notBefore, + notAfter: this.notAfter, + subjectName: this.subjectName, + issuerName: this.issuerName, + serialNumber: this.serialNumber, + type: this.type, + value: pvtsutils.Convert.ToBase64Url(this.value), + }; + } + + public async exportKey(): Promise; + public async exportKey(algorithm: types.Algorithm, usages: types.KeyUsage[]): Promise; + public async exportKey(algorithm?: types.Algorithm, usages?: types.KeyUsage[]) { + if (!this.publicKey) { + const publicKeyID = this.id.replace(/\w+-\w+-/i, ""); + const keyIndexes = await this.crypto.keyStorage.keys(); + for (const keyIndex of keyIndexes) { + const parts = keyIndex.split("-"); + if (parts[0] === "public" && parts[2] === publicKeyID) { + if (algorithm && usages) { + this.publicKey = await this.crypto.keyStorage.getItem(keyIndex, algorithm, usages); + } else { + this.publicKey = await this.crypto.keyStorage.getItem(keyIndex); + } + break; + } + } + if (!this.publicKey) { + if (algorithm && usages) { + this.publicKey = await this.getData().publicKey.export(algorithm, usages, this.crypto); + } else { + this.publicKey = await this.getData().publicKey.export(this.crypto); + } + } + } + return this.publicKey; + } + + protected parse(data: ArrayBuffer) { + this.x509 = new x509.X509Certificate(data); + } + + /** + * returns parsed ASN1 value + */ + protected getData() { + if (!this.x509) { + this.parse(this.value); + } + return this.x509!; + } + + /** + * Returns name from subject of the certificate + */ + protected getName() { + const name = new x509.Name(this.subjectName).toJSON(); + for (const item of name) { + const commonName = item.CN; + if (commonName && commonName.length > 0) { // CN + return commonName[0]; + } + } + return this.subjectName; + } + +} diff --git a/packages/pkcs11/src/const.ts b/packages/pkcs11/src/const.ts new file mode 100644 index 0000000..07eca1e --- /dev/null +++ b/packages/pkcs11/src/const.ts @@ -0,0 +1,3 @@ +// We have to use SHA-1 algorithm instead of SHA-2 +// OS X security uses SHA-1 for SecKeyItem's ID generation (kSecAttrApplicationLabel | kSecAttrPublicKeyHash) +export const ID_DIGEST = "SHA-1"; diff --git a/packages/pkcs11/src/crypto.ts b/packages/pkcs11/src/crypto.ts new file mode 100644 index 0000000..4841f39 --- /dev/null +++ b/packages/pkcs11/src/crypto.ts @@ -0,0 +1,184 @@ +// Core +import * as core from "@peculiar/webcrypto-core"; +import * as types from "@peculiar/webcrypto-types"; +import * as graphene from "graphene-pk11"; +import * as pkcs11 from "pkcs11js"; + +import { Assert } from "./assert"; +import { CertificateStorage } from "./cert_storage"; +import { KeyStorage } from "./key_storage"; +import { SubtleCrypto } from "./subtle"; +import { CryptoParams, ISessionContainer, ITemplateBuilder, ProviderInfo } from "./types"; +import { getProviderInfo } from "./utils"; +import { TemplateBuilder } from "./template_builder"; + +/** + * PKCS11 with WebCrypto Interface + */ +export class Crypto extends core.Crypto implements types.CryptoStorages, ISessionContainer { + public info?: ProviderInfo; + public subtle: SubtleCrypto; + + public keyStorage: KeyStorage; + public certStorage: CertificateStorage; + public isReadWrite: boolean; + public isLoggedIn: boolean; + public isLoginRequired: boolean; + + /** + * PKCS11 Slot + * @internal + */ + public slot: graphene.Slot; + /** + * PKCS11 Token + * @internal + */ + public token: graphene.Token; + + #session?: graphene.Session; + /** + * PKCS11 token + * @internal + */ + public get session() { + Assert.isSession(this.#session); + return this.#session; + } + + protected name?: string; + + private initialized: boolean; + + public templateBuilder: ITemplateBuilder = new TemplateBuilder(); + + /** + * Creates an instance of WebCrypto. + * @param props PKCS11 module init parameters + */ + constructor(props: CryptoParams) { + super(); + + const mod = graphene.Module.load(props.library, props.name || props.library); + this.name = props.name; + try { + if (props.libraryParameters) { + mod.initialize({ + libraryParameters: props.libraryParameters, + }); + } else { + mod.initialize(); + } + } catch (e) { + if (!(e instanceof pkcs11.Pkcs11Error && e.code === pkcs11.CKR_CRYPTOKI_ALREADY_INITIALIZED)) { + throw e; + } + } + this.initialized = true; + + const slotIndex = props.slot || 0; + const slots = mod.getSlots(true); + if (!(0 <= slotIndex && slotIndex < slots.length)) { + throw new core.CryptoError(`Slot by index ${props.slot} is not found`); + } + this.slot = slots.items(slotIndex); + this.token = this.slot.getToken(); + this.isLoginRequired = !!(this.token.flags & graphene.TokenFlag.LOGIN_REQUIRED); + this.isLoggedIn = !this.isLoginRequired; + this.isReadWrite = !!props.readWrite; + this.open(props.readWrite); + + if (props.pin && this.isLoginRequired) { + this.login(props.pin); + } + for (const i in props.vendors!) { + graphene.Mechanism.vendor(props.vendors![i]); + } + + this.subtle = new SubtleCrypto(this); + this.keyStorage = new KeyStorage(this); + this.certStorage = new CertificateStorage(this); + } + + public open(rw?: boolean) { + let flags = graphene.SessionFlag.SERIAL_SESSION; + if (rw) { + flags |= graphene.SessionFlag.RW_SESSION; + } + this.#session = this.slot.open(flags); + this.info = getProviderInfo(this.slot); + if (this.info && this.name) { + this.info.name = this.name; + } + } + + public reset() { + if (this.isLoggedIn && this.isLoginRequired) { + this.logout(); + } + this.session.close(); + + this.open(this.isReadWrite); + } + + public login(pin: string) { + if (!this.isLoginRequired) { + return; + } + + try { + this.session.login(pin); + } catch (error) { + if (!(error instanceof pkcs11.Pkcs11Error && error.code === pkcs11.CKR_USER_ALREADY_LOGGED_IN)) { + throw error; + } + } + + this.isLoggedIn = true; + } + + public logout() { + if (!this.isLoginRequired) { + return; + } + + try { + this.session.logout(); + } catch (error) { + if (!(error instanceof pkcs11.Pkcs11Error && error.code === pkcs11.CKR_USER_NOT_LOGGED_IN)) { + throw error; + } + } + + this.isLoggedIn = false; + } + + /** + * Generates cryptographically random values + * @param array Initialize array + */ + // Based on: https://github.com/KenanY/get-random-values + public getRandomValues(array: T): T { + if (!ArrayBuffer.isView(array)) { + throw new TypeError("Failed to execute 'getRandomValues' on 'Crypto': parameter 1 is not of type 'ArrayBufferView'"); + } + if (array.byteLength > 65536) { + throw new core.CryptoError(`Failed to execute 'getRandomValues' on 'Crypto': The ArrayBufferView's byte length (${array.byteLength}) exceeds the number of bytes of entropy available via this API (65536).`); + } + const bytes = new Uint8Array(this.session.generateRandom(array.byteLength)); + (array as unknown as Uint8Array).set(bytes); + return array; + } + + /** + * Close PKCS11 module + */ + public close() { + if (this.initialized) { + this.session.logout(); + this.session.close(); + this.slot.module.finalize(); + this.slot.module.close(); + } + } +} diff --git a/packages/pkcs11/src/index.ts b/packages/pkcs11/src/index.ts new file mode 100644 index 0000000..e4ee7a2 --- /dev/null +++ b/packages/pkcs11/src/index.ts @@ -0,0 +1,7 @@ +export { Crypto } from "./crypto"; +export { SubtleCrypto } from "./subtle"; +export { CryptoKey } from "./key"; +export { CryptoCertificate, X509Certificate, X509CertificateRequest } from "./certs"; +export { KeyStorage } from "./key_storage"; +export { CertificateStorage } from "./cert_storage"; +export * from "./template_builder"; diff --git a/packages/pkcs11/src/key.ts b/packages/pkcs11/src/key.ts new file mode 100644 index 0000000..78a38d5 --- /dev/null +++ b/packages/pkcs11/src/key.ts @@ -0,0 +1,177 @@ +// Core +import * as core from "@peculiar/webcrypto-core"; +import * as types from "@peculiar/webcrypto-types"; +import { KeyType } from "crypto"; +import * as graphene from "graphene-pk11"; + +import { Pkcs11KeyAlgorithm } from "./types"; + +export interface ITemplatePair { + privateKey: graphene.ITemplate; + publicKey: graphene.ITemplate; +} + +export class CryptoKey extends core.CryptoKey { + + public static defaultKeyAlgorithm() { + const alg: Pkcs11KeyAlgorithm = { + label: "", + name: "", + sensitive: false, + token: false, + }; + return alg; + } + + public static getID(p11Key: graphene.Key) { + let name: string; + switch (p11Key.class) { + case graphene.ObjectClass.PRIVATE_KEY: + name = "private"; + break; + case graphene.ObjectClass.PUBLIC_KEY: + name = "public"; + break; + case graphene.ObjectClass.SECRET_KEY: + name = "secret"; + break; + default: + throw new Error(`Unsupported Object type '${graphene.ObjectClass[p11Key.class]}'`); + } + return `${name}-${p11Key.handle.toString("hex")}-${p11Key.id.toString("hex")}`; + } + + public id: string; + public p11Object: graphene.Key | graphene.SecretKey | graphene.PublicKey | graphene.PrivateKey; + public override algorithm: T; + + public get key(): graphene.Key { + return this.p11Object.toType(); + } + + constructor(key: graphene.Key, alg: T | types.KeyAlgorithm, usages?: types.KeyUsage[]) { + super(); + this.p11Object = key; + switch (key.class) { + case graphene.ObjectClass.PUBLIC_KEY: + this.initPublicKey(key.toType()); + break; + case graphene.ObjectClass.PRIVATE_KEY: + this.initPrivateKey(key.toType()); + break; + case graphene.ObjectClass.SECRET_KEY: + this.initSecretKey(key.toType()); + break; + default: + throw new core.CryptoError(`Wrong incoming session object '${graphene.ObjectClass[key.class]}'`); + } + const { name, ...defaultAlg } = CryptoKey.defaultKeyAlgorithm(); + this.algorithm = { ...alg, ...defaultAlg } as T; + this.id = CryptoKey.getID(key); + + if (usages) { + this.usages = usages; + } + + try { + this.algorithm.label = key.label; + } catch { /*nothing*/ } + try { + this.algorithm.token = key.token; + } catch { /*nothing*/ } + try { + if (key instanceof graphene.PrivateKey || key instanceof graphene.SecretKey) { + this.algorithm.sensitive = key.get("sensitive"); + } + } catch { /*nothing*/ } + + this.onAssign(); + } + + public toJSON() { + return { + algorithm: this.algorithm, + type: this.type, + usages: this.usages, + extractable: this.extractable, + }; + } + + protected initPrivateKey(key: graphene.PrivateKey) { + this.p11Object = key; + this.type = "private"; + try { + // Yubico throws CKR_ATTRIBUTE_TYPE_INVALID + this.extractable = key.extractable; + } catch (e) { + this.extractable = false; + } + this.usages = []; + if (key.decrypt) { + this.usages.push("decrypt"); + } + if (key.derive) { + this.usages.push("deriveKey"); + this.usages.push("deriveBits"); + } + if (key.sign) { + this.usages.push("sign"); + } + if (key.unwrap) { + this.usages.push("unwrapKey"); + } + } + + protected initPublicKey(key: graphene.PublicKey) { + this.p11Object = key; + this.type = "public"; + this.extractable = true; + if (key.encrypt) { + this.usages.push("encrypt"); + } + if (key.verify) { + this.usages.push("verify"); + } + if (key.wrap) { + this.usages.push("wrapKey"); + } + } + + protected initSecretKey(key: graphene.SecretKey) { + this.p11Object = key; + this.type = "secret"; + try { + // Yubico throws CKR_ATTRIBUTE_TYPE_INVALID + this.extractable = key.extractable; + } catch (e) { + this.extractable = false; + } + if (key.sign) { + this.usages.push("sign"); + } + if (key.verify) { + this.usages.push("verify"); + } + if (key.encrypt) { + this.usages.push("encrypt"); + } + if (key.decrypt) { + this.usages.push("decrypt"); + } + if (key.wrap) { + this.usages.push("wrapKey"); + } + if (key.unwrap) { + this.usages.push("unwrapKey"); + } + if (key.derive) { + this.usages.push("deriveKey"); + this.usages.push("deriveBits"); + } + } + + protected onAssign() { + // nothing + } + +} diff --git a/packages/pkcs11/src/key_storage.ts b/packages/pkcs11/src/key_storage.ts new file mode 100644 index 0000000..28b59c5 --- /dev/null +++ b/packages/pkcs11/src/key_storage.ts @@ -0,0 +1,200 @@ +import * as core from "@peculiar/webcrypto-core"; +import * as types from "@peculiar/webcrypto-types"; +import * as graphene from "graphene-pk11"; +import { Crypto } from "./crypto"; +import { CryptoKey } from "./key"; +import { AesCryptoKey, EcCryptoKey, HmacCryptoKey, RsaCryptoKey } from "./mechs"; +import { Pkcs11KeyAlgorithm } from "./types"; +import * as utils from "./utils"; + +const OBJECT_TYPES = [graphene.ObjectClass.PRIVATE_KEY, graphene.ObjectClass.PUBLIC_KEY, graphene.ObjectClass.SECRET_KEY]; + +export class KeyStorage implements types.CryptoKeyStorage { + + protected crypto: Crypto; + + constructor(crypto: Crypto) { + this.crypto = crypto; + } + + public async keys() { + const keys: string[] = []; + OBJECT_TYPES.forEach((objectClass) => { + this.crypto.session!.find({ class: objectClass, token: true }, (obj) => { + const item = obj.toType(); + keys.push(CryptoKey.getID(item)); + }); + }); + return keys; + } + + public async indexOf(item: CryptoKey) { + if (item instanceof CryptoKey && item.key.token) { + return CryptoKey.getID(item.key); + } + return null; + } + + public async clear() { + const keys: graphene.SessionObject[] = []; + OBJECT_TYPES.forEach((objectClass) => { + this.crypto.session!.find({ class: objectClass, token: true }, (obj) => { + keys.push(obj); + }); + }); + keys.forEach((key) => { + key.destroy(); + }); + } + + public async getItem(key: string): Promise; + /** @deprecated Use getItem(index, algorithm, extractable, keyUsages) */ + public async getItem(key: string, algorithm: types.Algorithm, usages: types.KeyUsage[]): Promise; + public async getItem(index: string, algorithm: types.ImportAlgorithms, extractable: boolean, keyUsages: types.KeyUsage[]): Promise; + public async getItem(key: string, ...args: any[]) { + const subjectObject = this.getItemById(key); + if (subjectObject) { + const p11Key = subjectObject.toType(); + let alg: Pkcs11KeyAlgorithm | undefined; + let algorithm: types.Algorithm | undefined; + let usages: types.KeyUsage[] | undefined; + if (typeof args[0] === "object" && typeof args[1] === "boolean" && Array.isArray(args[2])) { + algorithm = args[0]; + usages = args[2]; + } else if (typeof args[0] === "object" && Array.isArray(args[1])) { + algorithm = args[0]; + usages = args[1]; + } + if (algorithm) { + alg = { + ...utils.prepareAlgorithm(algorithm), + token: false, + sensitive: false, + label: "", + }; + } else { + alg = { + name: "", + token: false, + sensitive: false, + label: "", + }; + switch (p11Key.type) { + case graphene.KeyType.RSA: { + if (p11Key.sign || p11Key.verify) { + alg.name = "RSASSA-PKCS1-v1_5"; + } else { + alg.name = "RSA-OAEP"; + } + (alg as any).hash = { name: "SHA-256" }; + break; + } + case graphene.KeyType.EC: { + if (p11Key.sign || p11Key.verify) { + alg.name = "ECDSA"; + } else { + alg.name = "ECDH"; + } + + break; + } + case graphene.KeyType.GENERIC_SECRET: + case graphene.KeyType.AES: { + if (p11Key.sign || p11Key.verify) { + alg.name = "HMAC"; + } else { + alg.name = "AES-CBC"; + } + break; + } + default: + throw new Error(`Unsupported type of key '${graphene.KeyType[p11Key.type] || p11Key.type}'`); + } + } + let CryptoKeyClass: typeof CryptoKey; + switch (alg.name.toUpperCase()) { + case "RSASSA-PKCS1-V1_5": + case "RSA-PSS": + case "RSA-OAEP": + CryptoKeyClass = RsaCryptoKey as typeof CryptoKey; + break; + case "ECDSA": + case "ECDH": + CryptoKeyClass = EcCryptoKey as typeof CryptoKey; + break; + case "HMAC": + CryptoKeyClass = HmacCryptoKey as typeof CryptoKey; + break; + case "AES-CBC": + case "AES-ECB": + case "AES-GCM": + CryptoKeyClass = AesCryptoKey as typeof CryptoKey; + break; + default: + CryptoKeyClass = CryptoKey; + } + const key = new CryptoKeyClass(p11Key, alg, usages); + + if (typeof args[1] === "boolean") { + key.extractable = args[1]; + } + + return key; + } else { + return null; + } + } + + public async removeItem(key: string) { + const sessionObject = this.getItemById(key); + if (sessionObject) { + sessionObject.destroy(); + } + } + + public async setItem(data: types.CryptoKey): Promise { + if (!(data instanceof CryptoKey)) { + throw new core.CryptoError("Parameter 1 is not P11CryptoKey"); + } + + const p11Key = data as CryptoKey; + + // don't copy object from token + const hasItem = await this.hasItem(data); + if (!(hasItem && p11Key.key.token)) { + const template = this.crypto.templateBuilder.build({ + action: "copy", + type: p11Key.type, + attributes: { + token: true, + } + }); + const obj = this.crypto.session.copy(p11Key.key, template); + return CryptoKey.getID(obj.toType()); + } else { + return data.id; + } + + } + + public async hasItem(key: CryptoKey) { + const item = this.getItemById(key.id); + return !!item; + } + + protected getItemById(id: string): graphene.SessionObject | null { + + let key: graphene.SessionObject | null = null; + OBJECT_TYPES.forEach((objectClass) => { + this.crypto.session!.find({ class: objectClass, token: true }, (obj) => { + const item = obj.toType(); + if (id === CryptoKey.getID(item)) { + key = item; + return false; + } + }); + }); + return key; + } + +} diff --git a/packages/pkcs11/src/mechs/aes/aes_cbc.ts b/packages/pkcs11/src/mechs/aes/aes_cbc.ts new file mode 100644 index 0000000..3952027 --- /dev/null +++ b/packages/pkcs11/src/mechs/aes/aes_cbc.ts @@ -0,0 +1,50 @@ +import * as types from "@peculiar/webcrypto-types"; +import * as core from "@peculiar/webcrypto-core"; + +import { Assert } from "../../assert"; +import { CryptoKey } from "../../key"; +import { IContainer, ISessionContainer } from "../../types"; +import { AesCrypto } from "./crypto"; +import { AesCryptoKey } from "./key"; + +export class AesCbcProvider extends core.AesCbcProvider implements IContainer { + + public crypto: AesCrypto; + + constructor(public container: ISessionContainer) { + super(); + + this.crypto = new AesCrypto(container); + } + + public override async onGenerateKey(algorithm: types.AesKeyGenParams, extractable: boolean, keyUsages: types.KeyUsage[], ...args: any[]): Promise { + const key = await this.crypto.generateKey( + { ...algorithm, name: this.name }, + extractable, + keyUsages); + + return key; + } + + public override async onEncrypt(algorithm: types.Algorithm, key: AesCryptoKey, data: ArrayBuffer): Promise { + return this.crypto.encrypt(false, algorithm, key, new Uint8Array(data)); + } + + public override async onDecrypt(algorithm: types.Algorithm, key: AesCryptoKey, data: ArrayBuffer): Promise { + return this.crypto.decrypt(false, algorithm, key, new Uint8Array(data)); + } + + public override async onExportKey(format: types.KeyFormat, key: AesCryptoKey): Promise { + return this.crypto.exportKey(format, key); + } + + public override async onImportKey(format: types.KeyFormat, keyData: types.JsonWebKey | ArrayBuffer, algorithm: types.Algorithm, extractable: boolean, keyUsages: types.KeyUsage[]): Promise { + return this.crypto.importKey(format, keyData, { ...algorithm, name: this.name }, extractable, keyUsages); + } + + public override checkCryptoKey(key: CryptoKey, keyUsage?: types.KeyUsage) { + super.checkCryptoKey(key, keyUsage); + Assert.isCryptoKey(key); + } + +} diff --git a/packages/pkcs11/src/mechs/aes/aes_ecb.ts b/packages/pkcs11/src/mechs/aes/aes_ecb.ts new file mode 100644 index 0000000..8fe04b8 --- /dev/null +++ b/packages/pkcs11/src/mechs/aes/aes_ecb.ts @@ -0,0 +1,52 @@ +import * as core from "@peculiar/webcrypto-core"; +import * as types from "@peculiar/webcrypto-types"; + +import { CryptoKey } from "../../key"; +import { IContainer, ISessionContainer, Pkcs11AesKeyGenParams, Pkcs11KeyImportParams } from "../../types"; +import { AesCrypto } from "./crypto"; +import { AesCryptoKey } from "./key"; + +export class AesEcbProvider extends core.ProviderCrypto implements IContainer { + + public name = "AES-ECB"; + public usages: types.KeyUsage[] = ["encrypt", "decrypt", "wrapKey", "unwrapKey"]; + public crypto: AesCrypto; + + constructor(public container: ISessionContainer) { + super(); + + this.crypto = new AesCrypto(container); + } + + public override async onGenerateKey(algorithm: Pkcs11AesKeyGenParams, extractable: boolean, keyUsages: types.KeyUsage[]): Promise { + const key = await this.crypto.generateKey( + { ...algorithm, name: this.name }, + extractable, + keyUsages); + + return key; + } + + public override async onEncrypt(algorithm: types.Algorithm, key: AesCryptoKey, data: ArrayBuffer): Promise { + return this.crypto.encrypt(true, algorithm, key, data); + } + + public override async onDecrypt(algorithm: types.Algorithm, key: AesCryptoKey, data: ArrayBuffer): Promise { + return this.crypto.decrypt(true, algorithm, key, data); + } + + public override async onExportKey(format: types.KeyFormat, key: AesCryptoKey): Promise { + return this.crypto.exportKey(format, key); + } + + public override async onImportKey(format: types.KeyFormat, keyData: types.JsonWebKey | ArrayBuffer, algorithm: Pkcs11KeyImportParams, extractable: boolean, keyUsages: types.KeyUsage[]): Promise { + return this.crypto.importKey(format, keyData, { ...algorithm, name: this.name }, extractable, keyUsages); + } + + public override checkCryptoKey(key: CryptoKey, keyUsage?: types.KeyUsage) { + super.checkCryptoKey(key, keyUsage); + if (!(key instanceof CryptoKey)) { + throw new TypeError("key: Is not a PKCS11 CryptoKey"); + } + } +} diff --git a/packages/pkcs11/src/mechs/aes/aes_gcm.ts b/packages/pkcs11/src/mechs/aes/aes_gcm.ts new file mode 100644 index 0000000..98f98c4 --- /dev/null +++ b/packages/pkcs11/src/mechs/aes/aes_gcm.ts @@ -0,0 +1,52 @@ +import * as core from "@peculiar/webcrypto-core"; +import * as types from "@peculiar/webcrypto-types"; + +import { CryptoKey } from "../../key"; +import { IContainer, ISessionContainer, Pkcs11KeyImportParams } from "../../types"; +import { Pkcs11AesKeyGenParams } from "../../types"; + +import { AesCrypto } from "./crypto"; +import { AesCryptoKey } from "./key"; + +export class AesGcmProvider extends core.AesGcmProvider implements IContainer { + + public crypto: AesCrypto; + + constructor(public container: ISessionContainer) { + super(); + + this.crypto = new AesCrypto(container); + } + + public override async onGenerateKey(algorithm: Pkcs11AesKeyGenParams, extractable: boolean, keyUsages: types.KeyUsage[]): Promise { + const key = await this.crypto.generateKey( + { ...algorithm, name: this.name }, + extractable, + keyUsages); + + return key; + } + + public override async onEncrypt(algorithm: types.Algorithm, key: AesCryptoKey, data: ArrayBuffer): Promise { + return this.crypto.encrypt(false, algorithm, key, new Uint8Array(data)); + } + + public override async onDecrypt(algorithm: types.Algorithm, key: AesCryptoKey, data: ArrayBuffer): Promise { + return this.crypto.decrypt(false, algorithm, key, new Uint8Array(data)); + } + + public override async onExportKey(format: types.KeyFormat, key: AesCryptoKey): Promise { + return this.crypto.exportKey(format, key); + } + + public override async onImportKey(format: types.KeyFormat, keyData: types.JsonWebKey | ArrayBuffer, algorithm: Pkcs11KeyImportParams, extractable: boolean, keyUsages: types.KeyUsage[]): Promise { + return this.crypto.importKey(format, keyData, { ...algorithm, name: this.name }, extractable, keyUsages); + } + + public override checkCryptoKey(key: CryptoKey, keyUsage?: types.KeyUsage) { + super.checkCryptoKey(key, keyUsage); + if (!(key instanceof CryptoKey)) { + throw new TypeError("key: Is not a PKCS11 CryptoKey"); + } + } +} diff --git a/packages/pkcs11/src/mechs/aes/crypto.ts b/packages/pkcs11/src/mechs/aes/crypto.ts new file mode 100644 index 0000000..664c488 --- /dev/null +++ b/packages/pkcs11/src/mechs/aes/crypto.ts @@ -0,0 +1,223 @@ +import * as core from "@peculiar/webcrypto-core"; +import * as types from "@peculiar/webcrypto-types"; +import * as graphene from "graphene-pk11"; +import { Convert } from "pvtsutils"; + +import { CryptoKey } from "../../key"; +import { GUID, prepareData } from "../../utils"; +import { IContainer, ISessionContainer, Pkcs11AesKeyAlgorithm, Pkcs11AesKeyGenParams, Pkcs11KeyImportParams } from "../../types"; + +import { AesCryptoKey } from "./key"; + +export class AesCrypto implements IContainer { + + constructor(public container: ISessionContainer) { + } + + public async generateKey(algorithm: Pkcs11AesKeyGenParams, extractable: boolean, keyUsages: types.KeyUsage[]): Promise { + return new Promise((resolve, reject) => { + const template = this.container.templateBuilder.build({ + action: "generate", + type: "secret", + attributes: { + id: GUID(), + label: algorithm.label || `AES-${algorithm.length}`, + token: algorithm.token, + sensitive: algorithm.sensitive, + extractable, + usages: keyUsages, + }, + }); + template.keyType = graphene.KeyType.AES; + template.valueLen = algorithm.length >> 3; + + // PKCS11 generation + this.container.session.generateKey(graphene.KeyGenMechanism.AES, template, (err, aesKey) => { + try { + if (err) { + reject(new core.CryptoError(`Aes: Can not generate new key\n${err.message}`)); + } else { + if (!aesKey) { + throw new Error("Cannot get key from callback function"); + } + resolve(new AesCryptoKey(aesKey, algorithm)); + } + } catch (e) { + reject(e); + } + }); + }); + } + + public async exportKey(format: string, key: AesCryptoKey): Promise { + const template = key.key.getAttribute({ value: null, valueLen: null }); + switch (format.toLowerCase()) { + case "jwk": + const aes: string = /AES-(\w+)/.exec(key.algorithm.name!)![1]; + const jwk: types.JsonWebKey = { + kty: "oct", + k: Convert.ToBase64Url(template.value!), + alg: `A${template.valueLen! * 8}${aes}`, + ext: true, + key_ops: key.usages, + }; + return jwk; + case "raw": + return new Uint8Array(template.value!).buffer; + break; + default: + throw new core.OperationError("format: Must be 'jwk' or 'raw'"); + } + } + + public async importKey(format: string, keyData: types.JsonWebKey | ArrayBuffer, algorithm: Pkcs11KeyImportParams, extractable: boolean, keyUsages: types.KeyUsage[]): Promise { + // get key value + let value: ArrayBuffer; + + switch (format.toLowerCase()) { + case "jwk": + const jwk = keyData as types.JsonWebKey; + if (!jwk.k) { + throw new core.OperationError("jwk.k: Cannot get required property"); + } + keyData = Convert.FromBase64Url(jwk.k); + case "raw": + value = keyData as ArrayBuffer; + switch (value.byteLength) { + case 16: + case 24: + case 32: + break; + default: + throw new core.OperationError("keyData: Is wrong key length"); + } + break; + default: + throw new core.OperationError("format: Must be 'jwk' or 'raw'"); + } + + // prepare key algorithm + const aesAlg: Pkcs11AesKeyAlgorithm = { + ...AesCryptoKey.defaultKeyAlgorithm(), + ...algorithm, + length: value.byteLength * 8, + }; + const template: graphene.ITemplate = this.container.templateBuilder.build({ + action: "import", + type: "secret", + attributes: { + id: GUID(), + label: algorithm.label || `AES-${aesAlg.length}`, + token: algorithm.token, + sensitive: algorithm.sensitive, + extractable, + usages: keyUsages, + }, + }); + template.keyType = graphene.KeyType.AES; + template.value = Buffer.from(value); + + // create session object + const sessionObject = this.container.session.create(template); + const key = new AesCryptoKey(sessionObject.toType(), aesAlg); + return key; + } + + public async encrypt(padding: boolean, algorithm: types.Algorithm, key: AesCryptoKey, data: ArrayBuffer): Promise { + // add padding if needed + if (padding) { + const blockLength = 16; + const mod = blockLength - (data.byteLength % blockLength); + const pad = Buffer.alloc(mod); + pad.fill(mod); + data = Buffer.concat([Buffer.from(data), pad]); + } + + return new Promise((resolve, reject) => { + const enc = Buffer.alloc(this.getOutputBufferSize(key.algorithm, true, data.byteLength)); + const mechanism = this.wc2pk11(algorithm); + this.container.session.createCipher(mechanism, key.key) + .once(Buffer.from(data), enc, (err, data2) => { + if (err) { + reject(err); + } else { + resolve(new Uint8Array(data2).buffer); + } + }); + }); + } + + public async decrypt(padding: boolean, algorithm: types.Algorithm, key: AesCryptoKey, data: ArrayBuffer): Promise { + const dec = await new Promise((resolve, reject) => { + const buf = Buffer.alloc(this.getOutputBufferSize(key.algorithm, false, data.byteLength)); + const mechanism = this.wc2pk11(algorithm); + this.container.session.createDecipher(mechanism, key.key) + .once(Buffer.from(data), buf, (err, data2) => { + if (err) { + reject(err); + } else { + resolve(data2); + } + }); + }); + if (padding) { + // Remove padding + const paddingLength = dec[dec.length - 1]; + + const res = new Uint8Array(dec.slice(0, dec.length - paddingLength)); + return res.buffer; + } else { + return new Uint8Array(dec).buffer; + } + } + + protected isAesGCM(algorithm: types.Algorithm): algorithm is types.AesGcmParams { + return algorithm.name.toUpperCase() === "AES-GCM"; + } + + protected isAesCBC(algorithm: types.Algorithm): algorithm is types.AesCbcParams { + return algorithm.name.toUpperCase() === "AES-CBC"; + } + + protected isAesECB(algorithm: types.Algorithm): algorithm is types.Algorithm { + return algorithm.name.toUpperCase() === "AES-ECB"; + } + + protected wc2pk11(algorithm: types.Algorithm) { + const session = this.container.session; + if (this.isAesGCM(algorithm)) { + const aad = algorithm.additionalData ? prepareData(algorithm.additionalData) : undefined; + let AesGcmParamsClass = graphene.AesGcmParams; + if (session.slot.module.cryptokiVersion.major >= 2 && + session.slot.module.cryptokiVersion.minor >= 40) { + AesGcmParamsClass = graphene.AesGcm240Params; + } + const params = new AesGcmParamsClass(prepareData(algorithm.iv), aad, algorithm.tagLength || 128); + return { name: "AES_GCM", params }; + } else if (this.isAesCBC(algorithm)) { + return { name: "AES_CBC_PAD", params: prepareData(algorithm.iv) }; + } else if (this.isAesECB(algorithm)) { + return { name: "AES_ECB", params: null }; + } else { + throw new core.OperationError("Unrecognized algorithm name"); + } + } + + /** + * Returns a size of output buffer of enc/dec operation + * @param keyAlg key algorithm + * @param enc type of operation + * `true` - encryption operation + * `false` - decryption operation + * @param dataSize size of incoming data + */ + protected getOutputBufferSize(keyAlg: Pkcs11AesKeyAlgorithm, enc: boolean, dataSize: number): number { + const len = keyAlg.length >> 3; + if (enc) { + return (Math.ceil(dataSize / len) * len) + len; + } else { + return dataSize; + } + } + +} diff --git a/packages/pkcs11/src/mechs/aes/index.ts b/packages/pkcs11/src/mechs/aes/index.ts new file mode 100644 index 0000000..216e027 --- /dev/null +++ b/packages/pkcs11/src/mechs/aes/index.ts @@ -0,0 +1,4 @@ +export * from "./aes_cbc"; +export * from "./aes_ecb"; +export * from "./aes_gcm"; +export * from "./key"; diff --git a/packages/pkcs11/src/mechs/aes/key.ts b/packages/pkcs11/src/mechs/aes/key.ts new file mode 100644 index 0000000..8e504c9 --- /dev/null +++ b/packages/pkcs11/src/mechs/aes/key.ts @@ -0,0 +1,10 @@ +import { CryptoKey } from "../../key"; +import { Pkcs11AesKeyAlgorithm } from "../../types"; + +export class AesCryptoKey extends CryptoKey { + + protected override onAssign() { + this.algorithm.length = this.key.get("valueLen") << 3; + } + +} diff --git a/packages/pkcs11/src/mechs/ec/crypto.ts b/packages/pkcs11/src/mechs/ec/crypto.ts new file mode 100644 index 0000000..0fdcbbb --- /dev/null +++ b/packages/pkcs11/src/mechs/ec/crypto.ts @@ -0,0 +1,353 @@ +import * as asn1Schema from "@peculiar/asn1-schema"; +import * as jsonSchema from "@peculiar/json-schema"; +import * as core from "@peculiar/webcrypto-core"; +import * as types from "@peculiar/webcrypto-types"; +import * as graphene from "graphene-pk11"; +import * as pvtsutils from "pvtsutils"; + +import { Assert } from "../../assert"; +import { CryptoKey } from "../../key"; +import { IContainer, ISessionContainer, Pkcs11Attributes, ITemplateBuildParameters, ITemplate, Pkcs11EcKeyGenParams, Pkcs11EcKeyImportParams } from "../../types"; +import { GUID, digest, b64UrlDecode } from "../../utils"; +import { EcCryptoKey } from "./key"; + +const id_ecPublicKey = "1.2.840.10045.2.1"; + +export class EcCrypto implements IContainer { + + public publicKeyUsages = ["verify"]; + public privateKeyUsages = ["sign", "deriveKey", "deriveBits"]; + + public constructor(public container: ISessionContainer) { + } + + public async generateKey(algorithm: Pkcs11EcKeyGenParams, extractable: boolean, keyUsages: types.KeyUsage[]): Promise { + return new Promise((resolve, reject) => { + // Create PKCS#11 templates + const attrs: Pkcs11Attributes = { + id: GUID(), + label: algorithm.label, + token: algorithm.token, + sensitive: algorithm.sensitive, + extractable, + usages: keyUsages, + }; + const privateTemplate = this.createTemplate({ + action: "generate", + type: "private", + attributes: attrs, + }); + const publicTemplate = this.createTemplate({ + action: "generate", + type: "public", + attributes: attrs, + }); + + // EC params + publicTemplate.paramsEC = Buffer.from(core.EcCurves.get(algorithm.namedCurve).raw); + + // PKCS11 generation + this.container.session.generateKeyPair(graphene.KeyGenMechanism.EC, publicTemplate, privateTemplate, (err, keys) => { + try { + if (err) { + reject(err); + } else { + if (!keys) { + throw new Error("Cannot get keys from callback function"); + } + const wcKeyPair = { + privateKey: new EcCryptoKey(keys.privateKey, algorithm), + publicKey: new EcCryptoKey(keys.publicKey, algorithm), + }; + resolve(wcKeyPair as any); + } + } catch (e) { + reject(e); + } + }); + }); + } + + public async exportKey(format: types.KeyFormat, key: EcCryptoKey): Promise { + switch (format.toLowerCase()) { + case "jwk": { + if (key.type === "private") { + return this.exportJwkPrivateKey(key); + } else { + return this.exportJwkPublicKey(key); + } + } + case "pkcs8": { + const jwk = await this.exportJwkPrivateKey(key); + return this.jwk2pkcs(jwk); + } + case "spki": { + const jwk = await this.exportJwkPublicKey(key); + return this.jwk2spki(jwk); + } + case "raw": { + // export subjectPublicKey BIT_STRING value + const jwk = await this.exportJwkPublicKey(key); + if ((key.algorithm as types.EcKeyGenParams).namedCurve === "X25519") { + return pvtsutils.Convert.FromBase64Url(jwk.x!); + } else { + const publicKey = jsonSchema.JsonParser.fromJSON(jwk, { targetSchema: core.asn1.EcPublicKey }); + return publicKey.value; + } + } + default: + throw new core.OperationError("format: Must be 'jwk', 'raw', pkcs8' or 'spki'"); + } + } + + public async importKey(format: types.KeyFormat, keyData: types.JsonWebKey | ArrayBuffer, algorithm: Pkcs11EcKeyImportParams, extractable: boolean, keyUsages: types.KeyUsage[]): Promise { + switch (format.toLowerCase()) { + case "jwk": { + const jwk: any = keyData; + if (jwk.d) { + return this.importJwkPrivateKey(jwk, algorithm, extractable, keyUsages); + } else { + return this.importJwkPublicKey(jwk, algorithm, extractable, keyUsages); + } + } + case "spki": { + const jwk = this.spki2jwk(keyData as ArrayBuffer); + return this.importJwkPublicKey(jwk, algorithm, extractable, keyUsages); + } + case "pkcs8": { + const jwk = this.pkcs2jwk(keyData as ArrayBuffer); + return this.importJwkPrivateKey(jwk, algorithm, extractable, keyUsages); + } + case "raw": { + const curve = core.EcCurves.get(algorithm.namedCurve); + const ecPoint = core.EcUtils.decodePoint(keyData as Uint8Array, curve.size); + const jwk: types.JsonWebKey = { + kty: "EC", + crv: algorithm.namedCurve, + x: pvtsutils.Convert.ToBase64Url(ecPoint.x), + }; + if (ecPoint.y) { + jwk.y = pvtsutils.Convert.ToBase64Url(ecPoint.y); + } + return this.importJwkPublicKey(jwk, algorithm, extractable, keyUsages); + } + default: + throw new core.OperationError("format: Must be 'jwk', 'raw', 'pkcs8' or 'spki'"); + } + } + + public getAlgorithm(p11AlgorithmName: string | number) { + const mechanisms = this.container.session.slot.getMechanisms(); + let EC: string | undefined; + for (let i = 0; i < mechanisms.length; i++) { + const mechanism = mechanisms.tryGetItem(i); + if (mechanism && (mechanism.name === p11AlgorithmName || mechanism.name === "ECDSA")) { + EC = mechanism.name; + } + } + if (!EC) { + throw new Error(`Cannot get PKCS11 EC mechanism by name '${p11AlgorithmName}'`); + } + return EC; + } + + public prepareData(hashAlgorithm: string, data: Buffer) { + // use nodejs crypto for digest calculating + return digest(hashAlgorithm.replace("-", ""), data); + } + + protected importJwkPrivateKey(jwk: types.JsonWebKey, algorithm: Pkcs11EcKeyImportParams, extractable: boolean, keyUsages: types.KeyUsage[]) { + const template = this.createTemplate({ + action: "import", + type: "private", + attributes: { + id: GUID(), + token: algorithm.token, + sensitive: algorithm.sensitive, + label: algorithm.label, + extractable, + usages: keyUsages, + }, + }); + + // Set EC private key attributes + template.paramsEC = Buffer.from(core.EcCurves.get(algorithm.namedCurve).raw); + template.value = b64UrlDecode(jwk.d!); + + const p11key = this.container.session.create(template).toType(); + + return new EcCryptoKey(p11key, algorithm); + } + + protected importJwkPublicKey(jwk: types.JsonWebKey, algorithm: Pkcs11EcKeyImportParams, extractable: boolean, keyUsages: types.KeyUsage[]) { + const namedCurve = core.EcCurves.get(algorithm.namedCurve); + const template = this.createTemplate({ + action: "import", + type: "public", + attributes: { + id: GUID(), + token: algorithm.token, + label: algorithm.label, + extractable, + usages: keyUsages, + } + }); + + // Set EC public key attributes + template.paramsEC = Buffer.from(namedCurve.raw);; + let pointEc: Buffer; + if (namedCurve.name === "curve25519") { + pointEc = b64UrlDecode(jwk.x!); + } else { + const point = core.EcUtils.encodePoint({ x: b64UrlDecode(jwk.x!), y: b64UrlDecode(jwk.y!) }, namedCurve.size); + const derPoint = asn1Schema.AsnConvert.serialize(new asn1Schema.OctetString(point)); + pointEc = Buffer.from(derPoint); + } + template.pointEC = pointEc; + + const p11key = this.container.session.create(template).toType(); + + return new EcCryptoKey(p11key, algorithm); + } + + protected exportJwkPublicKey(key: EcCryptoKey) { + const pkey: graphene.ITemplate = key.key.getAttribute({ + pointEC: null, + }); + const curve = core.EcCurves.get(key.algorithm.namedCurve); + // Parse DER-encoded of ANSI X9.62 ECPoint value ''Q'' + const p11PointEC = pkey.pointEC; + if (!p11PointEC) { + throw new Error("Cannot get required ECPoint attribute"); + } + const derEcPoint = asn1Schema.AsnConvert.parse(p11PointEC, asn1Schema.OctetString); + const ecPoint = core.EcUtils.decodePoint(derEcPoint, curve.size); + const jwk: types.JsonWebKey = { + kty: "EC", + crv: key.algorithm.namedCurve, + ext: true, + key_ops: key.usages, + x: pvtsutils.Convert.ToBase64Url(ecPoint.x), + }; + if (curve.name !== "curve25519") { + jwk.y = pvtsutils.Convert.ToBase64Url(ecPoint.y!); + } + return jwk; + } + + protected async exportJwkPrivateKey(key: EcCryptoKey) { + const pkey: graphene.ITemplate = key.key.getAttribute({ + value: null, + }); + const jwk: types.JsonWebKey = { + kty: "EC", + crv: (key.algorithm as types.EcKeyGenParams).namedCurve, + ext: true, + key_ops: key.usages, + d: pvtsutils.Convert.ToBase64Url(pkey.value!), + }; + return jwk; + } + + /** + * Creates PKCS11 template + * @param params + */ + protected createTemplate(params: ITemplateBuildParameters): ITemplate { + const template = this.container.templateBuilder.build({ + ...params, + attributes: { + ...params.attributes, + label: params.attributes.label || "EC", + }, + }); + + template.keyType = graphene.KeyType.EC; + + return template; + } + + protected spki2jwk(raw: ArrayBuffer): types.JsonWebKey { + const keyInfo = asn1Schema.AsnParser.parse(raw, core.asn1.PublicKeyInfo); + + if (keyInfo.publicKeyAlgorithm.algorithm !== id_ecPublicKey) { + throw new Error("SPKI is not EC public key"); + } + + const namedCurveId = asn1Schema.AsnParser.parse(keyInfo.publicKeyAlgorithm.parameters!, core.asn1.ObjectIdentifier); + const namedCurve = core.EcCurves.get(namedCurveId.value); + + const ecPublicKey = new core.asn1.EcPublicKey(keyInfo.publicKey); + const json = jsonSchema.JsonSerializer.toJSON(ecPublicKey); + + return { + kty: "EC", + crv: namedCurve.name, + ...json, + }; + } + + protected jwk2pkcs(jwk: types.JsonWebKey): ArrayBuffer { + Assert.requiredParameter(jwk.crv, "crv"); + const namedCurve = core.EcCurves.get(jwk.crv); + + const ecPrivateKey = jsonSchema.JsonParser.fromJSON(jwk, { targetSchema: core.asn1.EcPrivateKey }); + + const keyInfo = new core.asn1.PrivateKeyInfo(); + keyInfo.privateKeyAlgorithm = new core.asn1.AlgorithmIdentifier(); + keyInfo.privateKeyAlgorithm.algorithm = id_ecPublicKey; + keyInfo.privateKeyAlgorithm.parameters = namedCurve.raw; + keyInfo.privateKey = asn1Schema.AsnSerializer.serialize(ecPrivateKey); + + return asn1Schema.AsnSerializer.serialize(keyInfo); + } + + protected getCoordinate(b64: string, coordinateLength: number) { + const buf = pvtsutils.Convert.FromBase64Url(b64); + const offset = coordinateLength - buf.byteLength; + const res = new Uint8Array(coordinateLength); + res.set(new Uint8Array(buf), offset); + + return res.buffer as ArrayBuffer; + } + + protected jwk2spki(jwk: types.JsonWebKey) { + if (!jwk.crv) { + throw new Error("Absent mandatory parameter \"crv\""); + } + const namedCurve = core.EcCurves.get(jwk.crv); + + const ecPublicKey = jsonSchema.JsonParser.fromJSON(jwk, { targetSchema: core.asn1.EcPublicKey }); + + const keyInfo = new core.asn1.PublicKeyInfo(); + keyInfo.publicKeyAlgorithm.algorithm = id_ecPublicKey; + keyInfo.publicKeyAlgorithm.parameters = namedCurve.raw; + keyInfo.publicKey = ecPublicKey.value; + return asn1Schema.AsnSerializer.serialize(keyInfo); + } + + protected pkcs2jwk(raw: ArrayBuffer): types.JsonWebKey { + const keyInfo = asn1Schema.AsnParser.parse(raw, core.asn1.PrivateKeyInfo); + + if (keyInfo.privateKeyAlgorithm.algorithm !== id_ecPublicKey) { + throw new Error("PKCS8 is not EC private key"); + } + + if (!keyInfo.privateKeyAlgorithm.parameters) { + throw new Error("Cannot get required Named curve parameters from ASN.1 PrivateKeyInfo structure"); + } + + const namedCurveId = asn1Schema.AsnParser.parse(keyInfo.privateKeyAlgorithm.parameters!, core.asn1.ObjectIdentifier); + const namedCurve = core.EcCurves.get(namedCurveId.value); + + const ecPrivateKey = asn1Schema.AsnParser.parse(keyInfo.privateKey, core.asn1.EcPrivateKey); + const json = jsonSchema.JsonSerializer.toJSON(ecPrivateKey); + + return { + kty: "EC", + crv: namedCurve.name, + ...json, + }; + } + +} diff --git a/packages/pkcs11/src/mechs/ec/ec_dh.ts b/packages/pkcs11/src/mechs/ec/ec_dh.ts new file mode 100644 index 0000000..8b2a444 --- /dev/null +++ b/packages/pkcs11/src/mechs/ec/ec_dh.ts @@ -0,0 +1,108 @@ +import * as core from "@peculiar/webcrypto-core"; +import * as types from "@peculiar/webcrypto-types"; +import * as graphene from "graphene-pk11"; + +import { CryptoKey } from "../../key"; +import { IContainer, ISessionContainer, Pkcs11EcKeyGenParams } from "../../types"; +import { EcCrypto } from "./crypto"; +import { EcCryptoKey } from "./key"; + +export class EcdhProvider extends core.EcdhProvider implements IContainer { + + public override namedCurves = core.EcCurves.names; + + public override usages: types.ProviderKeyPairUsage = { + privateKey: ["sign", "deriveKey", "deriveBits"], + publicKey: ["verify"], + }; + + public crypto: EcCrypto; + + constructor(public container: ISessionContainer) { + super(); + + this.crypto = new EcCrypto(container); + } + + public override async onGenerateKey(algorithm: Pkcs11EcKeyGenParams, extractable: boolean, keyUsages: types.KeyUsage[]): Promise { + const key = await this.crypto.generateKey( + { ...algorithm, name: this.name }, + extractable, + keyUsages); + + return key; + } + + public override async onExportKey(format: types.KeyFormat, key: EcCryptoKey): Promise { + return this.crypto.exportKey(format, key); + } + + public override async onImportKey(format: types.KeyFormat, keyData: types.JsonWebKey | ArrayBuffer, algorithm: types.EcKeyImportParams, extractable: boolean, keyUsages: types.KeyUsage[]): Promise { + const key = await this.crypto.importKey(format, keyData, { ...algorithm, name: this.name }, extractable, keyUsages); + return key; + } + + public override checkCryptoKey(key: CryptoKey, keyUsage?: types.KeyUsage) { + super.checkCryptoKey(key, keyUsage); + if (!(key instanceof EcCryptoKey)) { + throw new TypeError("key: Is not EC CryptoKey"); + } + } + + public async onDeriveBits(algorithm: types.EcdhKeyDeriveParams, baseKey: EcCryptoKey, length: number): Promise { + return new Promise((resolve, reject) => { + let valueLen = 256; + switch (baseKey.algorithm.namedCurve) { + case "P-256": + case "K-256": + valueLen = 256; + break; + case "P-384": + valueLen = 384; + break; + case "P-521": + valueLen = 534; + break; + } + + // TODO Use template provider + const template: graphene.ITemplate = { + token: false, + sensitive: false, + class: graphene.ObjectClass.SECRET_KEY, + keyType: graphene.KeyType.GENERIC_SECRET, + extractable: true, + encrypt: true, + decrypt: true, + valueLen: valueLen >> 3, + }; + + // derive key + const ecPoint = (algorithm.public as EcCryptoKey).key.getAttribute({ pointEC: null }).pointEC!; + this.container.session.deriveKey( + { + name: "ECDH1_DERIVE", + params: new graphene.EcdhParams( + graphene.EcKdf.NULL, + null as any, + ecPoint, // CKA_EC_POINT + ), + }, + baseKey.key, + template, + (err, key) => { + if (err) { + reject(err); + } else { + if (!key) { + throw new Error("Cannot get key from callback function"); + } + const secretKey = key.toType(); + const value = secretKey.getAttribute({ value: null }).value as Buffer; + resolve(new Uint8Array(value.slice(0, length >> 3)).buffer); + } + }); + }); + } + +} diff --git a/packages/pkcs11/src/mechs/ec/ec_dsa.ts b/packages/pkcs11/src/mechs/ec/ec_dsa.ts new file mode 100644 index 0000000..5b1d135 --- /dev/null +++ b/packages/pkcs11/src/mechs/ec/ec_dsa.ts @@ -0,0 +1,112 @@ +import * as core from "@peculiar/webcrypto-core"; +import * as types from "@peculiar/webcrypto-types"; + +import { CryptoKey } from "../../key"; +import { IContainer, ISessionContainer, Pkcs11EcKeyGenParams, Pkcs11EcKeyImportParams } from "../../types"; +import { EcCrypto } from "./crypto"; +import { EcCryptoKey } from "./key"; + +export class EcdsaProvider extends core.EcdsaProvider implements IContainer { + + public override namedCurves = core.EcCurves.names; + + public override usages: types.ProviderKeyPairUsage = { + privateKey: ["sign", "deriveKey", "deriveBits"], + publicKey: ["verify"], + }; + + public crypto: EcCrypto; + + constructor(public container: ISessionContainer) { + super(); + + this.crypto = new EcCrypto(container); + } + + public override async onGenerateKey(algorithm: Pkcs11EcKeyGenParams, extractable: boolean, keyUsages: types.KeyUsage[]): Promise { + const key = await this.crypto.generateKey( + { ...algorithm, name: this.name }, + extractable, + keyUsages); + + return key; + } + + public override async onSign(algorithm: types.EcdsaParams, key: EcCryptoKey, data: ArrayBuffer): Promise { + return new Promise((resolve, reject) => { + let buf = Buffer.from(data); + const mechanism = this.wc2pk11(algorithm, algorithm); + mechanism.name = this.crypto.getAlgorithm(mechanism.name); + if (mechanism.name === "ECDSA") { + buf = this.crypto.prepareData((algorithm.hash as types.Algorithm).name, buf); + } + this.container.session.createSign(mechanism, key.key).once(buf, (err, data2) => { + if (err) { + reject(err); + } else { + resolve(new Uint8Array(data2).buffer); + } + }); + }); + } + + public override async onVerify(algorithm: types.EcdsaParams, key: EcCryptoKey, signature: ArrayBuffer, data: ArrayBuffer): Promise { + return new Promise((resolve, reject) => { + let buf = Buffer.from(data); + const mechanism = this.wc2pk11(algorithm, algorithm); + mechanism.name = this.crypto.getAlgorithm(mechanism.name); + if (mechanism.name === "ECDSA") { + buf = this.crypto.prepareData((algorithm.hash as types.Algorithm).name, buf); + } + this.container.session.createVerify(mechanism, key.key).once(buf, Buffer.from(signature), (err, data2) => { + if (err) { + reject(err); + } else { + resolve(data2); + } + }); + }); + } + + public override async onExportKey(format: types.KeyFormat, key: EcCryptoKey): Promise { + return this.crypto.exportKey(format, key); + } + + public override async onImportKey(format: types.KeyFormat, keyData: types.JsonWebKey | ArrayBuffer, algorithm: Pkcs11EcKeyImportParams, extractable: boolean, keyUsages: types.KeyUsage[]): Promise { + const key = await this.crypto.importKey(format, keyData, { ...algorithm, name: this.name }, extractable, keyUsages); + return key; + } + + public override checkCryptoKey(key: CryptoKey, keyUsage?: types.KeyUsage) { + super.checkCryptoKey(key, keyUsage); + if (!(key instanceof EcCryptoKey)) { + throw new TypeError("key: Is not EC CryptoKey"); + } + } + + protected wc2pk11(alg: types.EcdsaParams, keyAlg: types.KeyAlgorithm): { name: string, params: null; } { + let algName: string; + const hashAlg = (alg.hash as types.Algorithm).name.toUpperCase(); + switch (hashAlg) { + case "SHA-1": + algName = "ECDSA_SHA1"; + break; + case "SHA-224": + algName = "ECDSA_SHA224"; + break; + case "SHA-256": + algName = "ECDSA_SHA256"; + break; + case "SHA-384": + algName = "ECDSA_SHA384"; + break; + case "SHA-512": + algName = "ECDSA_SHA512"; + break; + default: + throw new core.OperationError(`Cannot create PKCS11 mechanism from algorithm '${hashAlg}'`); + } + return { name: algName, params: null }; + } + +} diff --git a/packages/pkcs11/src/mechs/ec/index.ts b/packages/pkcs11/src/mechs/ec/index.ts new file mode 100644 index 0000000..3b75d8b --- /dev/null +++ b/packages/pkcs11/src/mechs/ec/index.ts @@ -0,0 +1,4 @@ +export * from "./ec_dh"; +export * from "./ec_dsa"; +export * from "./key"; +export * from "./crypto"; diff --git a/packages/pkcs11/src/mechs/ec/key.ts b/packages/pkcs11/src/mechs/ec/key.ts new file mode 100644 index 0000000..0f9125c --- /dev/null +++ b/packages/pkcs11/src/mechs/ec/key.ts @@ -0,0 +1,22 @@ +import { AsnConvert } from "@peculiar/asn1-schema"; +import * as core from "@peculiar/webcrypto-core"; +import { CryptoKey } from "../../key"; +import { Pkcs11EcKeyAlgorithm } from "../../types"; + +export class EcCryptoKey extends CryptoKey { + + protected override onAssign() { + if (!this.algorithm.namedCurve) { + try { + const paramsECDSA = AsnConvert.parse(this.key.get("paramsECDSA"), core.asn1.ObjectIdentifier); + try { + const pointEC = core.EcCurves.get(paramsECDSA.value); + this.algorithm.namedCurve = pointEC.name; + } catch { + this.algorithm.namedCurve = paramsECDSA.value; + } + } catch { /*nothing*/ } + } + } + +} diff --git a/packages/pkcs11/src/mechs/hmac/hmac.ts b/packages/pkcs11/src/mechs/hmac/hmac.ts new file mode 100644 index 0000000..a971373 --- /dev/null +++ b/packages/pkcs11/src/mechs/hmac/hmac.ts @@ -0,0 +1,190 @@ +import * as core from "@peculiar/webcrypto-core"; +import * as types from "@peculiar/webcrypto-types"; +import * as graphene from "graphene-pk11"; +import * as pvtsutils from "pvtsutils"; + +import { CryptoKey } from "../../key"; +import { + IContainer, ISessionContainer, ITemplateBuildParameters, ITemplate, + Pkcs11HmacKeyAlgorithm, Pkcs11HmacKeyGenParams, Pkcs11HmacKeyImportParams, +} from "../../types"; +import { GUID } from "../../utils"; +import { HmacCryptoKey } from "./key"; + +export class HmacProvider extends core.HmacProvider implements IContainer { + + constructor(public container: ISessionContainer) { + super(); + } + + public async onGenerateKey(algorithm: Pkcs11HmacKeyGenParams, extractable: boolean, keyUsages: types.KeyUsage[]): Promise { + return new Promise((resolve, reject) => { + const length = (algorithm.length || this.getDefaultLength((algorithm.hash as types.Algorithm).name)) >> 3 << 3; + algorithm = { ...algorithm, name: this.name, length }; + + const template = this.createTemplate({ + action: "generate", + type: "secret", + attributes: { + token: algorithm.token, + sensitive: algorithm.sensitive, + label: algorithm.label, + extractable, + usages: keyUsages + }, + }); + template.valueLen = length >> 3; + + // PKCS11 generation + this.container.session.generateKey(graphene.KeyGenMechanism.GENERIC_SECRET, template, (err, key) => { + try { + if (err) { + reject(new core.CryptoError(`HMAC: Cannot generate new key\n${err.message}`)); + } else { + if (!key) { + throw new Error("Cannot get key from callback function"); + } + resolve(new HmacCryptoKey(key, algorithm)); + } + } catch (e) { + reject(e); + } + }); + }); + } + + public override async onSign(algorithm: types.Algorithm, key: HmacCryptoKey, data: ArrayBuffer): Promise { + return new Promise((resolve, reject) => { + const mechanism = this.wc2pk11(algorithm, key.algorithm); + this.container.session.createSign(mechanism, key.key).once(Buffer.from(data), (err, data2) => { + if (err) { + reject(err); + } else { + resolve(new Uint8Array(data2).buffer); + } + }); + }); + } + + public override async onVerify(algorithm: types.Algorithm, key: HmacCryptoKey, signature: ArrayBuffer, data: ArrayBuffer): Promise { + return new Promise((resolve, reject) => { + const mechanism = this.wc2pk11(algorithm, key.algorithm); + this.container.session.createVerify(mechanism, key.key).once(Buffer.from(data), Buffer.from(signature), (err, ok) => { + if (err) { + reject(err); + } else { + resolve(ok); + } + }); + }); + } + + public async onImportKey(format: types.KeyFormat, keyData: types.JsonWebKey | ArrayBuffer, algorithm: Pkcs11HmacKeyImportParams, extractable: boolean, keyUsages: types.KeyUsage[]): Promise { + // get key value + let value: ArrayBuffer; + + switch (format.toLowerCase()) { + case "jwk": + const jwk = keyData as types.JsonWebKey; + if (!jwk.k) { + throw new core.OperationError("jwk.k: Cannot get required property"); + } + keyData = pvtsutils.Convert.FromBase64Url(jwk.k); + case "raw": + value = keyData as ArrayBuffer; + break; + default: + throw new core.OperationError("format: Must be 'jwk' or 'raw'"); + } + // prepare key algorithm + const hmacAlg = { + ...algorithm, + name: this.name, + length: value.byteLength * 8 || this.getDefaultLength((algorithm as any).hash.name), + } as Pkcs11HmacKeyAlgorithm; + const template: graphene.ITemplate = this.createTemplate({ + action: "import", + type: "secret", + attributes: { + token: algorithm.token, + sensitive: algorithm.sensitive, + label: algorithm.label, + extractable, + usages: keyUsages + }, + }); + template.value = Buffer.from(value); + + // create session object + const sessionObject = this.container.session.create(template); + const key = new HmacCryptoKey(sessionObject.toType(), hmacAlg); + return key; + } + + public async onExportKey(format: types.KeyFormat, key: HmacCryptoKey): Promise { + const template = key.key.getAttribute({ value: null }); + switch (format.toLowerCase()) { + case "jwk": + const jwk: types.JsonWebKey = { + kty: "oct", + k: pvtsutils.Convert.ToBase64Url(template.value!), + alg: `HS${key.algorithm.hash.name.replace("SHA-", "")}`, + ext: true, + key_ops: key.usages, + }; + return jwk; + case "raw": + return new Uint8Array(template.value!).buffer; + break; + default: + throw new core.OperationError("format: Must be 'jwk' or 'raw'"); + } + } + + public override checkCryptoKey(key: CryptoKey, keyUsage?: types.KeyUsage) { + super.checkCryptoKey(key, keyUsage); + if (!(key instanceof HmacCryptoKey)) { + throw new TypeError("key: Is not HMAC CryptoKey"); + } + } + + protected createTemplate(params: ITemplateBuildParameters): ITemplate { + const template = this.container.templateBuilder.build({ + ...params, + attributes: { + ...params.attributes, + id: GUID(), + label: params.attributes.label || "HMAC", + }, + }); + + template.keyType = graphene.KeyType.GENERIC_SECRET; + + return template; + } + + protected wc2pk11(alg: types.Algorithm, keyAlg: types.HmacKeyAlgorithm): graphene.IAlgorithm { + let res: string; + switch (keyAlg.hash.name.toUpperCase()) { + case "SHA-1": + res = "SHA_1_HMAC"; + break; + case "SHA-224": + res = "SHA224_HMAC"; + break; + case "SHA-256": + res = "SHA256_HMAC"; + break; + case "SHA-384": + res = "SHA384_HMAC"; + break; + case "SHA-512": + res = "SHA512_HMAC"; + break; + default: + throw new core.OperationError(`Cannot create PKCS11 mechanism from algorithm '${keyAlg.hash.name}'`); + } + return { name: res, params: null }; + } + +} diff --git a/packages/pkcs11/src/mechs/hmac/index.ts b/packages/pkcs11/src/mechs/hmac/index.ts new file mode 100644 index 0000000..618fd78 --- /dev/null +++ b/packages/pkcs11/src/mechs/hmac/index.ts @@ -0,0 +1,2 @@ +export * from "./hmac"; +export * from "./key"; diff --git a/packages/pkcs11/src/mechs/hmac/key.ts b/packages/pkcs11/src/mechs/hmac/key.ts new file mode 100644 index 0000000..893b13b --- /dev/null +++ b/packages/pkcs11/src/mechs/hmac/key.ts @@ -0,0 +1,10 @@ +import { CryptoKey } from "../../key"; +import { Pkcs11HmacKeyAlgorithm } from "../../types"; + +export class HmacCryptoKey extends CryptoKey { + + protected override onAssign() { + this.algorithm.length = this.key.get("valueLen") << 3; + } + +} diff --git a/packages/pkcs11/src/mechs/index.ts b/packages/pkcs11/src/mechs/index.ts new file mode 100644 index 0000000..1b0f682 --- /dev/null +++ b/packages/pkcs11/src/mechs/index.ts @@ -0,0 +1,5 @@ +export * from "./aes"; +export * from "./ec"; +export * from "./hmac"; +export * from "./rsa"; +export * from "./sha"; diff --git a/packages/pkcs11/src/mechs/rsa/crypto.ts b/packages/pkcs11/src/mechs/rsa/crypto.ts new file mode 100644 index 0000000..dca599a --- /dev/null +++ b/packages/pkcs11/src/mechs/rsa/crypto.ts @@ -0,0 +1,364 @@ +import * as asnSchema from "@peculiar/asn1-schema"; +import * as jsonSchema from "@peculiar/json-schema"; +import * as core from "@peculiar/webcrypto-core"; +import * as types from "@peculiar/webcrypto-types"; +import * as graphene from "graphene-pk11"; +import * as pvtsutils from "pvtsutils"; + +import { CryptoKey } from "../../key"; +import { + IContainer, ISessionContainer, + Pkcs11RsaHashedKeyGenParams, Pkcs11Attributes, Pkcs11RsaHashedImportParams, + ITemplateBuildParameters, ITemplate, +} from "../../types"; +import { GUID, digest, b64UrlDecode } from "../../utils"; + +import { RsaCryptoKey } from "./key"; + +const HASH_PREFIXES: { [alg: string]: Buffer; } = { + "sha-1": Buffer.from([0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x05, 0x00, 0x04, 0x14]), + "sha-256": Buffer.from([0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20]), + "sha-384": Buffer.from([0x30, 0x41, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02, 0x05, 0x00, 0x04, 0x30]), + "sha-512": Buffer.from([0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40]), +}; + +export class RsaCrypto implements IContainer { + + public publicKeyUsages = ["verify", "encrypt", "wrapKey"]; + public privateKeyUsages = ["sign", "decrypt", "unwrapKey"]; + + public constructor(public container: ISessionContainer) { } + + public async generateKey(algorithm: Pkcs11RsaHashedKeyGenParams, extractable: boolean, keyUsages: types.KeyUsage[]): Promise { + const size = algorithm.modulusLength; + const exp = Buffer.from(algorithm.publicExponent); + + // Create PKCS#11 templates + const attrs: Pkcs11Attributes = { + id: GUID(), + label: algorithm.label, + token: algorithm.token, + sensitive: algorithm.sensitive, + extractable, + usages: keyUsages, + }; + const privateTemplate = this.createTemplate({ + action: "generate", + type: "private", + attributes: attrs, + }); + const publicTemplate = this.createTemplate({ + action: "generate", + type: "public", + attributes: attrs, + }); + + // Set RSA params + publicTemplate.publicExponent = exp; + publicTemplate.modulusBits = size; + + // PKCS11 generation + return new Promise((resolve, reject) => { + this.container.session.generateKeyPair(graphene.KeyGenMechanism.RSA, publicTemplate, privateTemplate, (err, keys) => { + try { + if (err) { + reject(new core.CryptoError(`Rsa: Can not generate new key\n${err.message}`)); + } else { + if (!keys) { + throw new Error("Cannot get keys from callback function"); + } + const wcKeyPair = { + privateKey: new RsaCryptoKey(keys.privateKey, algorithm), + publicKey: new RsaCryptoKey(keys.publicKey, algorithm), + }; + resolve(wcKeyPair as any); + } + } catch (e) { + reject(e); + } + }); + }); + } + + public async exportKey(format: types.KeyFormat, key: RsaCryptoKey): Promise { + switch (format.toLowerCase()) { + case "jwk": + if (key.type === "private") { + return this.exportJwkPrivateKey(key); + } else { + return this.exportJwkPublicKey(key); + } + case "pkcs8": { + const jwk = await this.exportJwkPrivateKey(key); + return this.jwk2pkcs(jwk); + } + case "spki": { + const jwk = await this.exportJwkPublicKey(key); + return this.jwk2spki(jwk); + } + case "raw": { + const jwk = await this.exportJwkPublicKey(key); + const spki = this.jwk2spki(jwk); + const asn = asnSchema.AsnConvert.parse(spki, core.asn1.PublicKeyInfo); + return asn.publicKey; + } + default: + throw new core.OperationError("format: Must be 'raw', 'jwk', 'pkcs8' or 'spki'"); + } + } + + public async importKey(format: types.KeyFormat, keyData: types.JsonWebKey | ArrayBuffer, algorithm: types.RsaHashedImportParams, extractable: boolean, keyUsages: types.KeyUsage[]): Promise { + switch (format.toLowerCase()) { + case "jwk": + const jwk: any = keyData; + if (jwk.d) { + return this.importJwkPrivateKey(jwk, algorithm as types.RsaHashedKeyGenParams, extractable, keyUsages); + } else { + return this.importJwkPublicKey(jwk, algorithm as types.RsaHashedKeyGenParams, extractable, keyUsages); + } + case "spki": { + const raw = new Uint8Array(keyData as Uint8Array).buffer as ArrayBuffer; + const jwk = this.spki2jwk(raw); + return this.importJwkPublicKey(jwk, algorithm as types.RsaHashedKeyGenParams, extractable, keyUsages); + } + case "pkcs8": { + const raw = new Uint8Array(keyData as Uint8Array).buffer as ArrayBuffer; + const jwk = this.pkcs2jwk(raw); + return this.importJwkPrivateKey(jwk, algorithm as types.RsaHashedKeyGenParams, extractable, keyUsages); + } + default: + throw new core.OperationError("format: Must be 'jwk', 'pkcs8' or 'spki'"); + } + } + + public getAlgorithm(wcAlgorithmName: string, p11AlgorithmName: string) { + const DEFAULT_RSA = wcAlgorithmName === "RSASSA-PKCS1-v1_5" ? "RSA_PKCS" + : wcAlgorithmName === "RSA-PSS" ? "RSA_PKCS_PSS" + : wcAlgorithmName === "RSA-OAEP" ? "RSA_PKCS_OAEP" : "RSA_PKCS"; + + const mechanisms = this.container.session.slot.getMechanisms(); + let RSA: string | undefined; + for (let i = 0; i < mechanisms.length; i++) { + const mechanism = mechanisms.tryGetItem(i); + if (mechanism && (mechanism.name === p11AlgorithmName || mechanism.name === DEFAULT_RSA)) { + RSA = mechanism.name; + } + } + if (!RSA) { + throw new Error(`Cannot get PKCS11 RSA mechanism by name '${p11AlgorithmName}'`); + } + return RSA; + } + + public prepareData(hashAlgorithm: string, data: Buffer) { + // use nodejs crypto for digest calculating + const hash = digest(hashAlgorithm.replace("-", ""), data); + + // enveloping hash + const hashPrefix = HASH_PREFIXES[hashAlgorithm.toLowerCase()]; + if (!hashPrefix) { + throw new Error(`Cannot get prefix for hash '${hashAlgorithm}'`); + } + return Buffer.concat([hashPrefix, hash]); + } + + protected jwkAlgName(algorithm: types.RsaHashedKeyAlgorithm) { + switch (algorithm.name.toUpperCase()) { + case "RSA-OAEP": + const mdSize = /(\d+)$/.exec(algorithm.hash.name)![1]; + return `RSA-OAEP${mdSize !== "1" ? `-${mdSize}` : ""}`; + case "RSASSA-PKCS1-V1_5": + return `RS${/(\d+)$/.exec(algorithm.hash.name)![1]}`; + case "RSA-PSS": + return `PS${/(\d+)$/.exec(algorithm.hash.name)![1]}`; + default: + throw new core.OperationError("algorithm: Is not recognized"); + } + } + + protected async exportJwkPublicKey(key: RsaCryptoKey) { + const pkey: graphene.ITemplate = key.key.getAttribute({ + publicExponent: null, + modulus: null, + }); + + // Remove padding + pkey.publicExponent = pkey.publicExponent!.length > 3 + ? pkey.publicExponent!.slice(pkey.publicExponent!.length - 3) + : pkey.publicExponent; + + const alg = this.jwkAlgName(key.algorithm as types.RsaHashedKeyAlgorithm); + const jwk: types.JsonWebKey = { + kty: "RSA", + alg, + ext: true, + key_ops: key.usages, + e: pvtsutils.Convert.ToBase64Url(pkey.publicExponent!), + n: pvtsutils.Convert.ToBase64Url(pkey.modulus!), + }; + + return jwk; + } + + protected async exportJwkPrivateKey(key: RsaCryptoKey) { + const pkey: graphene.ITemplate = key.key.getAttribute({ + publicExponent: null, + modulus: null, + privateExponent: null, + prime1: null, + prime2: null, + exp1: null, + exp2: null, + coefficient: null, + }); + + // Remove padding + pkey.publicExponent = pkey.publicExponent!.length > 3 + ? pkey.publicExponent!.slice(pkey.publicExponent!.length - 3) + : pkey.publicExponent; + + const alg = this.jwkAlgName(key.algorithm as types.RsaHashedKeyAlgorithm); + const jwk: types.JsonWebKey = { + kty: "RSA", + alg, + ext: true, + key_ops: key.usages, + e: pvtsutils.Convert.ToBase64Url(pkey.publicExponent!), + n: pvtsutils.Convert.ToBase64Url(pkey.modulus!), + d: pvtsutils.Convert.ToBase64Url(pkey.privateExponent!), + p: pvtsutils.Convert.ToBase64Url(pkey.prime1!), + q: pvtsutils.Convert.ToBase64Url(pkey.prime2!), + dp: pvtsutils.Convert.ToBase64Url(pkey.exp1!), + dq: pvtsutils.Convert.ToBase64Url(pkey.exp2!), + qi: pvtsutils.Convert.ToBase64Url(pkey.coefficient!), + }; + + return jwk; + } + + protected importJwkPrivateKey(jwk: types.JsonWebKey, algorithm: Pkcs11RsaHashedKeyGenParams, extractable: boolean, keyUsages: types.KeyUsage[]) { + const template = this.createTemplate({ + action: "import", + type: "private", + attributes: { + id: GUID(), + token: algorithm.token, + sensitive: algorithm.sensitive, + label: algorithm.label, + extractable, + usages: keyUsages + }, + }); + + // Set RSA private key attributes + template.publicExponent = b64UrlDecode(jwk.e!); + template.modulus = b64UrlDecode(jwk.n!); + template.privateExponent = b64UrlDecode(jwk.d!); + template.prime1 = b64UrlDecode(jwk.p!); + template.prime2 = b64UrlDecode(jwk.q!); + template.exp1 = b64UrlDecode(jwk.dp!); + template.exp2 = b64UrlDecode(jwk.dq!); + template.coefficient = b64UrlDecode(jwk.qi!); + + const p11key = this.container.session.create(template).toType(); + + return new RsaCryptoKey(p11key, algorithm); + } + + protected importJwkPublicKey(jwk: types.JsonWebKey, algorithm: Pkcs11RsaHashedImportParams, extractable: boolean, keyUsages: types.KeyUsage[]) { + const template = this.createTemplate({ + action: "import", + type: "public", + attributes: { + id: GUID(), + token: algorithm.token, + label: algorithm.label, + extractable, + usages: keyUsages + }, + }); + + // Set RSA public key attributes + template.publicExponent = b64UrlDecode(jwk.e!); + template.modulus = b64UrlDecode(jwk.n!); + + const p11key = this.container.session.create(template).toType(); + + return new RsaCryptoKey(p11key, algorithm); + } + + /** + * Creates PKCS11 template + * @param params + */ + protected createTemplate(params: ITemplateBuildParameters): ITemplate { + const template = this.container.templateBuilder.build({ + ...params, + attributes: { + ...params.attributes, + label: params.attributes.label || "RSA", + } + }); + + template.keyType = graphene.KeyType.RSA; + + return template; + } + + protected jwk2spki(jwk: types.JsonWebKey) { + const key = jsonSchema.JsonParser.fromJSON(jwk, { targetSchema: core.asn1.RsaPublicKey }); + + const keyInfo = new core.asn1.PublicKeyInfo(); + keyInfo.publicKeyAlgorithm.algorithm = "1.2.840.113549.1.1.1"; + keyInfo.publicKeyAlgorithm.parameters = null; + + keyInfo.publicKey = asnSchema.AsnSerializer.serialize(key); + + return asnSchema.AsnSerializer.serialize(keyInfo); + } + + protected jwk2pkcs(jwk: types.JsonWebKey) { + const key = jsonSchema.JsonParser.fromJSON(jwk, { targetSchema: core.asn1.RsaPrivateKey }); + + const keyInfo = new core.asn1.PrivateKeyInfo(); + keyInfo.privateKeyAlgorithm.algorithm = "1.2.840.113549.1.1.1"; + keyInfo.privateKeyAlgorithm.parameters = null; + + keyInfo.privateKey = asnSchema.AsnSerializer.serialize(key); + + return asnSchema.AsnSerializer.serialize(keyInfo); + } + + protected pkcs2jwk(raw: ArrayBuffer): types.JsonWebKey { + const keyInfo = asnSchema.AsnParser.parse(raw, core.asn1.PrivateKeyInfo); + + if (keyInfo.privateKeyAlgorithm.algorithm !== "1.2.840.113549.1.1.1") { + throw new Error("PKCS8 is not RSA private key"); + } + + const key = asnSchema.AsnParser.parse(keyInfo.privateKey, core.asn1.RsaPrivateKey); + const json = jsonSchema.JsonSerializer.toJSON(key); + + return { + kty: "RSA", + ...json, + }; + } + + protected spki2jwk(raw: ArrayBuffer): types.JsonWebKey { + const keyInfo = asnSchema.AsnParser.parse(raw, core.asn1.PublicKeyInfo); + + if (keyInfo.publicKeyAlgorithm.algorithm !== "1.2.840.113549.1.1.1") { + throw new Error("PKCS8 is not RSA private key"); + } + + const key = asnSchema.AsnParser.parse(keyInfo.publicKey, core.asn1.RsaPublicKey); + const json = jsonSchema.JsonSerializer.toJSON(key); + + return { + kty: "RSA", + ...json, + }; + } + +} diff --git a/packages/pkcs11/src/mechs/rsa/index.ts b/packages/pkcs11/src/mechs/rsa/index.ts new file mode 100644 index 0000000..161ab1c --- /dev/null +++ b/packages/pkcs11/src/mechs/rsa/index.ts @@ -0,0 +1,5 @@ +export * from "./crypto"; +export * from "./rsa-pss"; +export * from "./rsa-oaep"; +export * from "./rsa-ssa"; +export * from "./key"; diff --git a/packages/pkcs11/src/mechs/rsa/key.ts b/packages/pkcs11/src/mechs/rsa/key.ts new file mode 100644 index 0000000..ffab4c2 --- /dev/null +++ b/packages/pkcs11/src/mechs/rsa/key.ts @@ -0,0 +1,29 @@ +import { CryptoKey } from "../../key"; +import { Pkcs11RsaHashedKeyAlgorithm } from "../../types"; + +export class RsaCryptoKey extends CryptoKey { + + protected override onAssign() { + if (!this.algorithm.modulusLength) { + this.algorithm.modulusLength = 0; + try { + this.algorithm.modulusLength = this.key.get("modulus").length << 3; + } catch { /*nothing*/ } + } + + if (!this.algorithm.publicExponent) { + this.algorithm.publicExponent = new Uint8Array(0); + try { + let publicExponent = this.key.get("publicExponent") as Buffer; + + // Remove padding + publicExponent = publicExponent.length > 3 + ? publicExponent.slice(publicExponent.length - 3) + : publicExponent; + + this.algorithm.publicExponent = new Uint8Array(publicExponent); + } catch { /*nothing*/ } + } + } + +} diff --git a/packages/pkcs11/src/mechs/rsa/rsa-oaep.ts b/packages/pkcs11/src/mechs/rsa/rsa-oaep.ts new file mode 100644 index 0000000..11fdd51 --- /dev/null +++ b/packages/pkcs11/src/mechs/rsa/rsa-oaep.ts @@ -0,0 +1,111 @@ +import * as graphene from "graphene-pk11"; +import * as core from "@peculiar/webcrypto-core"; +import * as types from "@peculiar/webcrypto-types"; + +import { CryptoKey } from "../../key"; +import { + IContainer, ISessionContainer, + Pkcs11RsaHashedImportParams, Pkcs11RsaHashedKeyAlgorithm, Pkcs11RsaHashedKeyGenParams, +} from "../../types"; + +import { RsaCrypto } from "./crypto"; +import { RsaCryptoKey } from "./key"; + +export class RsaOaepProvider extends core.RsaOaepProvider implements IContainer { + + public override usages: types.ProviderKeyPairUsage = { + privateKey: ["sign", "decrypt", "unwrapKey"], + publicKey: ["verify", "encrypt", "wrapKey"], + }; + public crypto: RsaCrypto; + + constructor(public container: ISessionContainer) { + super(); + + this.crypto = new RsaCrypto(container); + } + + public async onGenerateKey(algorithm: Pkcs11RsaHashedKeyGenParams, extractable: boolean, keyUsages: types.KeyUsage[]): Promise { + const key = await this.crypto.generateKey( + { ...algorithm, name: this.name }, + extractable, + keyUsages); + + return key; + } + + public async onEncrypt(algorithm: types.RsaOaepParams, key: RsaCryptoKey, data: ArrayBuffer): Promise { + return new Promise((resolve, reject) => { + const buf = Buffer.from(data); + const mechanism = this.wc2pk11(algorithm, key.algorithm); + const context = Buffer.alloc((key.algorithm).modulusLength >> 3); + this.container.session.createCipher(mechanism, key.key) + .once(buf, context, (err, data2) => { + if (err) { + reject(err); + } else { + resolve(new Uint8Array(data2).buffer); + } + }); + }); + } + + public async onDecrypt(algorithm: types.RsaOaepParams, key: RsaCryptoKey, data: ArrayBuffer): Promise { + return new Promise((resolve, reject) => { + const buf = Buffer.from(data); + const mechanism = this.wc2pk11(algorithm, key.algorithm); + const context = Buffer.alloc((key.algorithm).modulusLength >> 3); + this.container.session.createDecipher(mechanism, key.key) + .once(buf, context, (err, data2) => { + if (err) { + reject(err); + } else { + resolve(new Uint8Array(data2).buffer); + } + }); + }); + } + + public async onExportKey(format: types.KeyFormat, key: RsaCryptoKey): Promise { + return this.crypto.exportKey(format, key); + } + + public async onImportKey(format: types.KeyFormat, keyData: types.JsonWebKey | ArrayBuffer, algorithm: Pkcs11RsaHashedImportParams, extractable: boolean, keyUsages: types.KeyUsage[]): Promise { + const key = await this.crypto.importKey(format, keyData, { ...algorithm, name: this.name }, extractable, keyUsages); + return key; + } + + public override checkCryptoKey(key: CryptoKey, keyUsage?: types.KeyUsage) { + super.checkCryptoKey(key, keyUsage); + if (!(key instanceof RsaCryptoKey)) { + throw new TypeError("key: Is not PKCS11 CryptoKey"); + } + } + + protected wc2pk11(alg: types.RsaOaepParams, keyAlg: Pkcs11RsaHashedKeyAlgorithm): graphene.IAlgorithm { + let params: graphene.RsaOaepParams; + const sourceData = alg.label ? Buffer.from((alg as types.RsaOaepParams).label as Uint8Array) : undefined; + switch (keyAlg.hash.name.toUpperCase()) { + case "SHA-1": + params = new graphene.RsaOaepParams(graphene.MechanismEnum.SHA1, graphene.RsaMgf.MGF1_SHA1, sourceData); + break; + case "SHA-224": + params = new graphene.RsaOaepParams(graphene.MechanismEnum.SHA224, graphene.RsaMgf.MGF1_SHA224, sourceData); + break; + case "SHA-256": + params = new graphene.RsaOaepParams(graphene.MechanismEnum.SHA256, graphene.RsaMgf.MGF1_SHA256, sourceData); + break; + case "SHA-384": + params = new graphene.RsaOaepParams(graphene.MechanismEnum.SHA384, graphene.RsaMgf.MGF1_SHA384, sourceData); + break; + case "SHA-512": + params = new graphene.RsaOaepParams(graphene.MechanismEnum.SHA512, graphene.RsaMgf.MGF1_SHA512, sourceData); + break; + default: + throw new core.OperationError(`Cannot create PKCS11 mechanism from algorithm '${keyAlg.hash.name}'`); + } + const res = { name: "RSA_PKCS_OAEP", params }; + return res; + } + +} diff --git a/packages/pkcs11/src/mechs/rsa/rsa-pss.ts b/packages/pkcs11/src/mechs/rsa/rsa-pss.ts new file mode 100644 index 0000000..0f8c051 --- /dev/null +++ b/packages/pkcs11/src/mechs/rsa/rsa-pss.ts @@ -0,0 +1,116 @@ +import * as graphene from "graphene-pk11"; +import * as core from "@peculiar/webcrypto-core"; +import * as types from "@peculiar/webcrypto-types"; + +import { CryptoKey } from "../../key"; +import { IContainer, ISessionContainer } from "../../types"; +import { RsaCrypto } from "./crypto"; +import { RsaCryptoKey } from "./key"; + +export class RsaPssProvider extends core.RsaPssProvider implements IContainer { + + public override usages: types.ProviderKeyPairUsage = { + privateKey: ["sign", "decrypt", "unwrapKey"], + publicKey: ["verify", "encrypt", "wrapKey"], + }; + public crypto: RsaCrypto; + + constructor(public container: ISessionContainer) { + super(); + + this.crypto = new RsaCrypto(container); + } + + public async onGenerateKey(algorithm: types.RsaHashedKeyGenParams, extractable: boolean, keyUsages: types.KeyUsage[]): Promise { + const key = await this.crypto.generateKey( + { ...algorithm, name: this.name }, + extractable, + keyUsages); + + return key; + } + + public async onSign(algorithm: types.RsaPssParams, key: RsaCryptoKey, data: ArrayBuffer): Promise { + return new Promise((resolve, reject) => { + let buf = Buffer.from(data); + const mechanism = this.wc2pk11(algorithm, key.algorithm as types.RsaHashedKeyAlgorithm); + mechanism.name = this.crypto.getAlgorithm(this.name, mechanism.name); + if (mechanism.name === "RSA_PKCS_PSS") { + buf = this.crypto.prepareData((key as any).algorithm.hash.name, buf); + } + this.container.session.createSign(mechanism, key.key).once(buf, (err, data2) => { + if (err) { + reject(err); + } else { + resolve(new Uint8Array(data2).buffer); + } + }); + }); + } + + public async onVerify(algorithm: types.RsaPssParams, key: RsaCryptoKey, signature: ArrayBuffer, data: ArrayBuffer): Promise { + return new Promise((resolve, reject) => { + let buf = Buffer.from(data); + const mechanism = this.wc2pk11(algorithm, key.algorithm as types.RsaHashedKeyAlgorithm); + mechanism.name = this.crypto.getAlgorithm(this.name, mechanism.name); + if (mechanism.name === "RSA_PKCS_PSS") { + buf = this.crypto.prepareData((key as any).algorithm.hash.name, buf); + } + this.container.session.createVerify(mechanism, key.key).once(buf, Buffer.from(signature), (err, data2) => { + if (err) { + reject(err); + } else { + resolve(data2); + } + }); + }); + } + + public async onExportKey(format: types.KeyFormat, key: RsaCryptoKey): Promise { + return this.crypto.exportKey(format, key); + } + + public async onImportKey(format: types.KeyFormat, keyData: types.JsonWebKey | ArrayBuffer, algorithm: types.RsaHashedImportParams, extractable: boolean, keyUsages: types.KeyUsage[]): Promise { + const key = await this.crypto.importKey(format, keyData, { ...algorithm, name: this.name }, extractable, keyUsages); + return key; + } + + public override checkCryptoKey(key: CryptoKey, keyUsage?: types.KeyUsage) { + super.checkCryptoKey(key, keyUsage); + if (!(key instanceof RsaCryptoKey)) { + throw new TypeError("key: Is not PKCS11 CryptoKey"); + } + } + + protected wc2pk11(alg: types.RsaPssParams, keyAlg: types.RsaHashedKeyAlgorithm): { name: string, params: graphene.IParams; } { + let mech: string; + let param: graphene.RsaPssParams; + const saltLen = alg.saltLength; + switch (keyAlg.hash.name.toUpperCase()) { + case "SHA-1": + mech = "SHA1_RSA_PKCS_PSS"; + param = new graphene.RsaPssParams(graphene.MechanismEnum.SHA1, graphene.RsaMgf.MGF1_SHA1, saltLen); + break; + case "SHA-224": + mech = "SHA224_RSA_PKCS_PSS"; + param = new graphene.RsaPssParams(graphene.MechanismEnum.SHA224, graphene.RsaMgf.MGF1_SHA224, saltLen); + break; + case "SHA-256": + mech = "SHA256_RSA_PKCS_PSS"; + param = new graphene.RsaPssParams(graphene.MechanismEnum.SHA256, graphene.RsaMgf.MGF1_SHA256, saltLen); + break; + case "SHA-384": + mech = "SHA384_RSA_PKCS_PSS"; + param = new graphene.RsaPssParams(graphene.MechanismEnum.SHA384, graphene.RsaMgf.MGF1_SHA384, saltLen); + break; + case "SHA-512": + mech = "SHA512_RSA_PKCS_PSS"; + param = new graphene.RsaPssParams(graphene.MechanismEnum.SHA512, graphene.RsaMgf.MGF1_SHA512, saltLen); + break; + default: + throw new core.OperationError(`Cannot create PKCS11 mechanism from algorithm '${keyAlg.hash.name}'`); + } + return { name: mech, params: param }; + } + +} diff --git a/packages/pkcs11/src/mechs/rsa/rsa-ssa.ts b/packages/pkcs11/src/mechs/rsa/rsa-ssa.ts new file mode 100644 index 0000000..3fc5f46 --- /dev/null +++ b/packages/pkcs11/src/mechs/rsa/rsa-ssa.ts @@ -0,0 +1,108 @@ +import * as core from "@peculiar/webcrypto-core"; +import * as types from "@peculiar/webcrypto-types"; + +import { CryptoKey } from "../../key"; +import { IContainer, ISessionContainer } from "../../types"; +import { RsaCrypto } from "./crypto"; +import { RsaCryptoKey } from "./key"; + +export class RsaSsaProvider extends core.RsaSsaProvider implements IContainer { + + public override usages: types.ProviderKeyPairUsage = { + privateKey: ["sign", "decrypt", "unwrapKey"], + publicKey: ["verify", "encrypt", "wrapKey"], + }; + public crypto: RsaCrypto; + + constructor(public container: ISessionContainer) { + super(); + + this.crypto = new RsaCrypto(container); + } + + public async onGenerateKey(algorithm: types.RsaHashedKeyGenParams, extractable: boolean, keyUsages: types.KeyUsage[]): Promise { + const key = await this.crypto.generateKey( + { ...algorithm, name: this.name }, + extractable, + keyUsages); + + return key; + } + + public async onSign(algorithm: types.Algorithm, key: RsaCryptoKey, data: ArrayBuffer): Promise { + return new Promise((resolve, reject) => { + let buf = Buffer.from(data); + const mechanism = this.wc2pk11(algorithm, key.algorithm as types.RsaHashedKeyAlgorithm); + mechanism.name = this.crypto.getAlgorithm(this.name, mechanism.name); + if (mechanism.name === "RSA_PKCS") { + buf = this.crypto.prepareData((key as any).algorithm.hash.name, buf); + } + this.container.session.createSign(mechanism, key.key).once(buf, (err, data2) => { + if (err) { + reject(err); + } else { + resolve(new Uint8Array(data2).buffer); + } + }); + }); + } + + public async onVerify(algorithm: types.Algorithm, key: RsaCryptoKey, signature: ArrayBuffer, data: ArrayBuffer): Promise { + return new Promise((resolve, reject) => { + let buf = Buffer.from(data); + const mechanism = this.wc2pk11(algorithm, key.algorithm as types.RsaHashedKeyAlgorithm); + mechanism.name = this.crypto.getAlgorithm(this.name, mechanism.name); + if (mechanism.name === "RSA_PKCS") { + buf = this.crypto.prepareData((key as any).algorithm.hash.name, buf); + } + this.container.session.createVerify(mechanism, key.key).once(buf, Buffer.from(signature), (err, data2) => { + if (err) { + reject(err); + } else { + resolve(data2); + } + }); + }); + } + + public async onExportKey(format: types.KeyFormat, key: RsaCryptoKey): Promise { + return this.crypto.exportKey(format, key); + } + + public async onImportKey(format: types.KeyFormat, keyData: types.JsonWebKey | ArrayBuffer, algorithm: types.RsaHashedImportParams, extractable: boolean, keyUsages: types.KeyUsage[]): Promise { + const key = await this.crypto.importKey(format, keyData, { ...algorithm, name: this.name }, extractable, keyUsages); + return key; + } + + public override checkCryptoKey(key: CryptoKey, keyUsage?: types.KeyUsage) { + super.checkCryptoKey(key, keyUsage); + if (!(key instanceof RsaCryptoKey)) { + throw new TypeError("key: Is not PKCS11 CryptoKey"); + } + } + + protected wc2pk11(alg: types.Algorithm, keyAlg: types.RsaHashedKeyAlgorithm): { name: string, params: null; } { + let res: string; + switch (keyAlg.hash.name.toUpperCase()) { + case "SHA-1": + res = "SHA1_RSA_PKCS"; + break; + case "SHA-224": + res = "SHA224_RSA_PKCS"; + break; + case "SHA-256": + res = "SHA256_RSA_PKCS"; + break; + case "SHA-384": + res = "SHA384_RSA_PKCS"; + break; + case "SHA-512": + res = "SHA512_RSA_PKCS"; + break; + default: + throw new core.OperationError(`Cannot create PKCS11 mechanism from algorithm '${keyAlg.hash.name}'`); + } + return { name: res, params: null }; + } + +} diff --git a/packages/pkcs11/src/mechs/sha/crypto.ts b/packages/pkcs11/src/mechs/sha/crypto.ts new file mode 100644 index 0000000..8f46813 --- /dev/null +++ b/packages/pkcs11/src/mechs/sha/crypto.ts @@ -0,0 +1,27 @@ +import * as types from "@peculiar/webcrypto-types"; +import type * as graphene from "graphene-pk11"; + +import { IContainer, ISessionContainer } from "../../types"; + +export class ShaCrypto implements IContainer { + + public constructor(public container: ISessionContainer) { } + + public async digest(algorithm: types.Algorithm, data: ArrayBuffer) { + const p11Mech: graphene.IAlgorithm = { + name: algorithm.name.toUpperCase().replace("-", ""), + params: null, + }; + + return new Promise((resolve, reject) => { + this.container.session.createDigest(p11Mech).once(Buffer.from(data), (err, data) => { + if (err) { + reject(err); + } else { + resolve(new Uint8Array(data).buffer); + } + }); + }); + } + +} diff --git a/packages/pkcs11/src/mechs/sha/index.ts b/packages/pkcs11/src/mechs/sha/index.ts new file mode 100644 index 0000000..f85391e --- /dev/null +++ b/packages/pkcs11/src/mechs/sha/index.ts @@ -0,0 +1,4 @@ +export * from "./sha_1"; +export * from "./sha_256"; +export * from "./sha_384"; +export * from "./sha_512"; diff --git a/packages/pkcs11/src/mechs/sha/sha_1.ts b/packages/pkcs11/src/mechs/sha/sha_1.ts new file mode 100644 index 0000000..9046af0 --- /dev/null +++ b/packages/pkcs11/src/mechs/sha/sha_1.ts @@ -0,0 +1,22 @@ +import * as core from "@peculiar/webcrypto-core"; +import * as types from "@peculiar/webcrypto-types"; + +import { IContainer, ISessionContainer } from "../../types"; +import { ShaCrypto } from "./crypto"; + +export class Sha1Provider extends core.ProviderCrypto implements IContainer { + public name = "SHA-1"; + public usages: types.KeyUsage[] = []; + public crypto: ShaCrypto; + + constructor(public container: ISessionContainer) { + super(); + + this.crypto = new ShaCrypto(container); + } + + public override async onDigest(algorithm: types.Algorithm, data: ArrayBuffer): Promise { + return this.crypto.digest(algorithm, data); + } + +} diff --git a/packages/pkcs11/src/mechs/sha/sha_256.ts b/packages/pkcs11/src/mechs/sha/sha_256.ts new file mode 100644 index 0000000..10af342 --- /dev/null +++ b/packages/pkcs11/src/mechs/sha/sha_256.ts @@ -0,0 +1,5 @@ +import { Sha1Provider } from "./sha_1"; + +export class Sha256Provider extends Sha1Provider { + public override name = "SHA-256"; +} diff --git a/packages/pkcs11/src/mechs/sha/sha_384.ts b/packages/pkcs11/src/mechs/sha/sha_384.ts new file mode 100644 index 0000000..5ffba47 --- /dev/null +++ b/packages/pkcs11/src/mechs/sha/sha_384.ts @@ -0,0 +1,5 @@ +import { Sha1Provider } from "./sha_1"; + +export class Sha384Provider extends Sha1Provider { + public override name = "SHA-384"; +} diff --git a/packages/pkcs11/src/mechs/sha/sha_512.ts b/packages/pkcs11/src/mechs/sha/sha_512.ts new file mode 100644 index 0000000..1b499cc --- /dev/null +++ b/packages/pkcs11/src/mechs/sha/sha_512.ts @@ -0,0 +1,5 @@ +import { Sha1Provider } from "./sha_1"; + +export class Sha512Provider extends Sha1Provider { + public override name = "SHA-512"; +} diff --git a/packages/pkcs11/src/p11_object.ts b/packages/pkcs11/src/p11_object.ts new file mode 100644 index 0000000..f427159 --- /dev/null +++ b/packages/pkcs11/src/p11_object.ts @@ -0,0 +1,17 @@ +import { Storage } from "graphene-pk11"; + +export class Pkcs11Object { + + public static assertStorage(obj: Storage | undefined): asserts obj is Storage { + if (!obj) { + throw new TypeError("PKCS#11 object is empty"); + } + } + + public p11Object?: Storage; + + constructor(object?: Storage) { + this.p11Object = object; + } + +} diff --git a/packages/pkcs11/src/subtle.ts b/packages/pkcs11/src/subtle.ts new file mode 100644 index 0000000..c2db73a --- /dev/null +++ b/packages/pkcs11/src/subtle.ts @@ -0,0 +1,77 @@ +import * as types from "@peculiar/webcrypto-types"; +import * as core from "@peculiar/webcrypto-core"; + +import { ID_DIGEST } from "./const"; +import { CryptoKey, CryptoKey as P11CryptoKey } from "./key"; +import * as mechs from "./mechs"; +import { IContainer, ISessionContainer } from "./types"; +import * as utils from "./utils"; + +export class SubtleCrypto extends core.SubtleCrypto implements IContainer { + + public constructor(public container: ISessionContainer) { + super(); + + //#region AES + this.providers.set(new mechs.AesCbcProvider(this.container)); + this.providers.set(new mechs.AesEcbProvider(this.container)); + this.providers.set(new mechs.AesGcmProvider(this.container)); + //#endregion + // #region RSA + this.providers.set(new mechs.RsaSsaProvider(this.container)); + this.providers.set(new mechs.RsaPssProvider(this.container)); + this.providers.set(new mechs.RsaOaepProvider(this.container)); + // #endregion + // #region EC + this.providers.set(new mechs.EcdsaProvider(this.container)); + this.providers.set(new mechs.EcdhProvider(this.container)); + // #endregion + //#region SHA + this.providers.set(new mechs.Sha1Provider(this.container)); + this.providers.set(new mechs.Sha256Provider(this.container)); + this.providers.set(new mechs.Sha384Provider(this.container)); + this.providers.set(new mechs.Sha512Provider(this.container)); + //#endregion + // #region HMAC + this.providers.set(new mechs.HmacProvider(this.container)); + // #endregion + } + + public override async generateKey(algorithm: types.RsaHashedKeyGenParams | types.EcKeyGenParams, extractable: boolean, keyUsages: types.KeyUsage[]): Promise; + public override async generateKey(algorithm: types.AesKeyGenParams | types.HmacKeyGenParams | types.Pbkdf2Params, extractable: boolean, keyUsages: types.KeyUsage[]): Promise; + public override async generateKey(algorithm: types.AlgorithmIdentifier, extractable: boolean, keyUsages: types.KeyUsage[]): Promise { + const keys = await super.generateKey(algorithm, extractable, keyUsages); + + // Fix ID for generated key pair. It must be hash of public key raw + if (utils.isCryptoKeyPair(keys)) { + const publicKey = keys.publicKey as P11CryptoKey; + const privateKey = keys.privateKey as P11CryptoKey; + + const raw = await this.exportKey("spki", publicKey); + const digest = utils.digest(ID_DIGEST, raw).slice(0, 16); + publicKey.key.id = digest; + publicKey.id = P11CryptoKey.getID(publicKey.key); + privateKey.key.id = digest; + privateKey.id = P11CryptoKey.getID(privateKey.key); + } + + return keys; + } + + public override async importKey(format: types.KeyFormat, keyData: types.JsonWebKey | types.BufferSource, algorithm: types.AlgorithmIdentifier, extractable: boolean, keyUsages: types.KeyUsage[]): Promise { + const key = await super.importKey(format, keyData, algorithm, extractable, keyUsages); + + // Fix ID for generated key pair. It must be hash of public key raw + if (key.type === "public" && extractable) { + const publicKey = key as P11CryptoKey; + + const raw = await this.exportKey("spki", publicKey); + const digest = utils.digest(ID_DIGEST, raw).slice(0, 16); + publicKey.key.id = digest; + publicKey.id = P11CryptoKey.getID(publicKey.key); + } + + return key as CryptoKey; + } + +} diff --git a/packages/pkcs11/src/template_builder.ts b/packages/pkcs11/src/template_builder.ts new file mode 100644 index 0000000..ec97d1a --- /dev/null +++ b/packages/pkcs11/src/template_builder.ts @@ -0,0 +1,101 @@ +import * as graphene from "graphene-pk11"; +import { BufferSourceConverter } from "pvtsutils"; +import * as types from "./types"; + +export class TemplateBuilder implements types.ITemplateBuilder { + + public build(params: types.ITemplateBuildParameters): types.ITemplate { + const { attributes, action, type } = params; + const template: types.ITemplate = { + token: !!attributes.token, + }; + + if (action === "copy") { + if (type === "private") { + if (attributes.token) { + // TODO SafeNET 5110 token requires CKA_SENSITIVE:true and CKA_EXTRACTABLE:false + // Those values must be set in C_GenerateKeyPair, or C_CopyObject, or C_CreateObject + // Object.assign(template, { + // sensitive: true, + // }); + } + } + } else { + if (attributes.label) { + template.label = attributes.label + } + if (attributes.id) { + template.id = Buffer.from(BufferSourceConverter.toArrayBuffer(attributes.id)); + } + + const sign = attributes.usages?.includes("sign"); + const verify = attributes.usages?.includes("verify"); + const wrap = attributes.usages?.includes("wrapKey"); + const unwrap = attributes.usages?.includes("unwrapKey"); + const encrypt = unwrap || attributes.usages?.includes("encrypt"); + const decrypt = wrap || attributes.usages?.includes("decrypt"); + const derive = attributes.usages?.includes("deriveBits") || attributes.usages?.includes("deriveKey"); + + switch (type) { + case "private": + Object.assign(template, { + class: graphene.ObjectClass.PRIVATE_KEY, + sensitive: !!attributes.sensitive, + private: true, + extractable: !!attributes.extractable, + derive, + sign, + decrypt, + unwrap, + }); + break; + case "public": + Object.assign(template, { + token: !!attributes.token, + class: graphene.ObjectClass.PUBLIC_KEY, + private: false, + derive, + verify, + encrypt, + wrap, + }); + break; + case "secret": + Object.assign(template, { + class: graphene.ObjectClass.SECRET_KEY, + sensitive: !!attributes.sensitive, + extractable: !!attributes.extractable, + derive, + sign, + verify, + decrypt, + encrypt, + unwrap, + wrap, + }); + break; + case "request": + if (template.id) { + template.objectId = template.id; + delete template.id; + } + Object.assign(template, { + class: graphene.ObjectClass.DATA, + application: "webcrypto-p11", + private: false, + }); + break; + case "x509": + Object.assign(template, { + class: graphene.ObjectClass.CERTIFICATE, + certType: graphene.CertificateType.X_509, + private: false, + }); + break; + } + } + + return template; + } + +} diff --git a/packages/pkcs11/src/types.ts b/packages/pkcs11/src/types.ts new file mode 100644 index 0000000..0d31df1 --- /dev/null +++ b/packages/pkcs11/src/types.ts @@ -0,0 +1,122 @@ +import * as types from "@peculiar/webcrypto-types"; +import * as graphene from "graphene-pk11"; +import { BufferSource } from "pvtsutils"; + +export type ITemplate = graphene.ITemplate; + +export interface Pkcs11Attributes { + id?: BufferSource; + token?: boolean; + sensitive?: boolean; + label?: string; + extractable?: boolean; + usages?: types.KeyUsage[]; +} + +export type TemplateBuildType = "private" | "public" | "secret" | "x509" | "request"; + +export type TemplateBuildAction = "generate" | "import" | "copy"; + +export interface ITemplateBuildParameters { + type: TemplateBuildType; + action: TemplateBuildAction; + attributes: Pkcs11Attributes; +} + +/** + * export interface of PKCS#11 template builder + */ +export interface ITemplateBuilder { + /** + * Returns a PKCS#11 template + * @param params Template build parameters + */ + build(params: ITemplateBuildParameters): ITemplate; +} + +export interface ISessionContainer { + readonly session: graphene.Session; + templateBuilder: ITemplateBuilder; +} + +export interface IContainer { + readonly container: ISessionContainer; +} + +export interface CryptoParams { + /** + * Path to library + */ + library: string; + /** + * Name of PKCS11 module + */ + name?: string; + /** + * Index of slot + */ + slot?: number; + readWrite?: boolean; + /** + * PIN of slot + */ + pin?: string; + /** + * list of vendor json files + */ + vendors?: string[]; + /** + * NSS library parameters + */ + libraryParameters?: string; +} + +export interface ProviderInfo { + id: string; + name: string; + reader: string; + slot: number; + serialNumber: string; + algorithms: string[]; + isRemovable: boolean; + isHardware: boolean; +} + +export interface Pkcs11Params { + token?: boolean; + sensitive?: boolean; + label?: string; +} +export interface Pkcs11KeyGenParams extends types.Algorithm, Pkcs11Params { } + +export interface Pkcs11AesKeyGenParams extends types.AesKeyGenParams, Pkcs11KeyGenParams { } + +export interface Pkcs11HmacKeyGenParams extends types.HmacKeyGenParams, Pkcs11KeyGenParams { } + +export interface Pkcs11EcKeyGenParams extends types.EcKeyGenParams, Pkcs11KeyGenParams { } + +export interface Pkcs11RsaHashedKeyGenParams extends types.RsaHashedKeyGenParams, Pkcs11KeyGenParams { } + +export interface Pkcs11KeyImportParams extends types.Algorithm, Pkcs11Params { } + +export interface Pkcs11EcKeyImportParams extends types.EcKeyImportParams, Pkcs11KeyImportParams { } + +export interface Pkcs11RsaHashedImportParams extends types.RsaHashedImportParams, Pkcs11KeyImportParams { } + +export interface Pkcs11HmacKeyImportParams extends types.HmacImportParams, Pkcs11KeyImportParams { } + +export interface Pkcs11AesKeyImportParams extends types.Algorithm, Pkcs11KeyImportParams { } + +export interface Pkcs11KeyAlgorithm extends types.KeyAlgorithm { + token: boolean; + sensitive: boolean; + label: string; +} + +export interface Pkcs11RsaHashedKeyAlgorithm extends types.RsaHashedKeyAlgorithm, Pkcs11KeyAlgorithm { } + +export interface Pkcs11EcKeyAlgorithm extends types.EcKeyAlgorithm, Pkcs11KeyAlgorithm { } + +export interface Pkcs11AesKeyAlgorithm extends types.AesKeyAlgorithm, Pkcs11KeyAlgorithm { } + +export interface Pkcs11HmacKeyAlgorithm extends types.HmacKeyAlgorithm, Pkcs11KeyAlgorithm { } diff --git a/packages/pkcs11/src/utils.ts b/packages/pkcs11/src/utils.ts new file mode 100644 index 0000000..353f8fb --- /dev/null +++ b/packages/pkcs11/src/utils.ts @@ -0,0 +1,166 @@ +import * as types from "@peculiar/webcrypto-types"; +import * as crypto from "crypto"; +import { Slot, SlotFlag } from "graphene-pk11"; +import { BufferSourceConverter, Convert } from "pvtsutils"; +import { ID_DIGEST } from "./const"; +import { ProviderInfo } from "./types"; + +export interface HashedAlgorithm extends types.Algorithm { + hash: types.AlgorithmIdentifier; +} + +export function GUID(): Buffer { + return crypto.randomBytes(20); +} + +export function b64UrlDecode(b64url: string): Buffer { + return Buffer.from(Convert.FromBase64Url(b64url)); +} + +/** + * Converts BufferSource to Buffer + * @param data Array which must be prepared + */ +export function prepareData(data: types.BufferSource): Buffer { + return Buffer.from(BufferSourceConverter.toArrayBuffer(data)); +} + +export function isHashedAlgorithm(data: any): data is HashedAlgorithm { + return data instanceof Object + && "name" in data + && "hash" in data; +} + +export function isCryptoKeyPair(data: any): data is types.CryptoKeyPair { + return data && data.privateKey && data.publicKey; +} + +export function prepareAlgorithm(algorithm: types.AlgorithmIdentifier): types.Algorithm { + if (typeof algorithm === "string") { + return { + name: algorithm, + } as types.Algorithm; + } + if (isHashedAlgorithm(algorithm)) { + const preparedAlgorithm = { ...algorithm }; + preparedAlgorithm.hash = prepareAlgorithm(algorithm.hash); + return preparedAlgorithm as HashedAlgorithm; + } + return { ...algorithm }; +} + +/** + * Calculates digest for given data + * @param algorithm + * @param data + */ +export function digest(algorithm: string, data: types.BufferSource): Buffer { + const hash = crypto.createHash(algorithm.replace("-", "")); + hash.update(prepareData(Buffer.from(BufferSourceConverter.toArrayBuffer(data)))); + return hash.digest(); +} + +function calculateProviderID(slot: Slot) { + const str = slot.manufacturerID + slot.slotDescription + slot.getToken().serialNumber + slot.handle.toString("hex"); + return digest(ID_DIGEST, Buffer.from(str)).toString("hex"); +} + +export function getProviderInfo(slot: Slot) { + // get index of slot + const slots = slot.module.getSlots(true); + let index = -1; + for (let i = 0; i < slots.length; i++) { + if (slots.items(i).handle.equals(slot.handle)) { + index = i; + break; + } + } + + const token = slot.getToken(); + const provider: ProviderInfo = { + id: calculateProviderID(slot), + slot: index, + name: token.label, + reader: slot.slotDescription, + serialNumber: slot.getToken().serialNumber, + algorithms: [], + isRemovable: !!(slot.flags & SlotFlag.REMOVABLE_DEVICE), + isHardware: !!(slot.flags & SlotFlag.HW_SLOT), + }; + + const algorithms = slot.getMechanisms(); + for (let i = 0; i < algorithms.length; i++) { + const algorithm = algorithms.tryGetItem(i); + if (!algorithm) { + continue; + } + + let algName = ""; + switch (algorithm.name) { + case "SHA_1": + algName = "SHA-1"; + break; + case "SHA256": + algName = "SHA-256"; + break; + case "SHA384": + algName = "SHA-384"; + break; + case "SHA512": + algName = "SHA-512"; + break; + case "RSA_PKCS": + case "SHA1_RSA_PKCS": + case "SHA256_RSA_PKCS": + case "SHA384_RSA_PKCS": + case "SHA512_RSA_PKCS": + algName = "RSASSA-PKCS1-v1_5"; + break; + case "SHA1_RSA_PSS": + case "SHA256_RSA_PSS": + case "SHA384_RSA_PSS": + case "SHA512_RSA_PSS": + algName = "RSA-PSS"; + break; + case "SHA1_RSA_PKCS_PSS": + case "SHA256_RSA_PKCS_PSS": + case "SHA384_RSA_PKCS_PSS": + case "SHA512_RSA_PKCS_PSS": + algName = "RSA-PSS"; + break; + case "RSA_PKCS_OAEP": + algName = "RSA-OAEP"; + break; + case "ECDSA": + case "ECDSA_SHA1": + case "ECDSA_SHA256": + case "ECDSA_SHA384": + case "ECDSA_SHA512": + algName = "ECDSA"; + break; + case "ECDH1_DERIVE": + algName = "ECDH"; + break; + case "AES_CBC_PAD": + algName = "AES-CBC"; + break; + case "AES_ECB": + case "AES_ECB_PAD": + algName = "AES-ECB"; + break; + case "AES_GCM_PAD": + algName = "AES-GCM"; + break; + case "AES_KEY_WRAP_PAD": + algName = "AES-KW"; + break; + default: + } + if (algName && !provider.algorithms.some((alg) => alg === algName)) { + provider.algorithms.push(algName); + } + + } + + return provider; +} diff --git a/packages/pkcs11/test/aes.ts b/packages/pkcs11/test/aes.ts new file mode 100644 index 0000000..7a10e1b --- /dev/null +++ b/packages/pkcs11/test/aes.ts @@ -0,0 +1,44 @@ +import * as assert from "assert"; +import { AesCryptoKey } from "../src/mechs"; +import { Pkcs11AesKeyGenParams, Pkcs11AesKeyImportParams } from "../src/types"; +import { crypto } from "./config"; + +context("AES", () => { + + context("token", () => { + + it("generate", async () => { + const alg: Pkcs11AesKeyGenParams = { + name: "AES-CBC", + length: 128, + label: "custom", + token: true, + sensitive: true, + }; + + const key = await crypto.subtle.generateKey(alg, false, ["encrypt", "decrypt"]) as AesCryptoKey; + + assert.strictEqual(key.algorithm.token, true); + assert.strictEqual(key.algorithm.label, alg.label); + assert.strictEqual(key.algorithm.sensitive, true); + }); + + it("import", async () => { + const alg: Pkcs11AesKeyImportParams = { + name: "AES-CBC", + label: "custom", + token: true, + sensitive: true, + }; + const raw = Buffer.from("1234567890abcdef1234567809abcdef"); + + const key = await crypto.subtle.importKey("raw", raw, alg, false, ["encrypt", "decrypt"]) as AesCryptoKey; + + assert.strictEqual(key.algorithm.token, true); + assert.strictEqual(key.algorithm.label, alg.label); + assert.strictEqual(key.algorithm.sensitive, true); + }); + + }); + +}); diff --git a/packages/pkcs11/test/cert_storage.ts b/packages/pkcs11/test/cert_storage.ts new file mode 100644 index 0000000..4a79f5a --- /dev/null +++ b/packages/pkcs11/test/cert_storage.ts @@ -0,0 +1,255 @@ +import * as x509 from "@peculiar/x509"; +import * as core from "@peculiar/webcrypto-core"; +import * as types from "@peculiar/webcrypto-types"; +import * as assert from "assert"; +import { X509Certificate, X509CertificateRequest } from "../src"; +import { crypto } from "./config"; +import { isNSS } from "./helper"; +import { Pkcs11RsaHashedKeyAlgorithm } from "../src/types"; + +const X509_RAW = Buffer.from("308203A830820290A003020102020900FEDCE3010FC948FF300D06092A864886F70D01010505003034310B300906035504061302465231123010060355040A0C094468696D796F7469733111300F06035504030C084365727469676E61301E170D3037303632393135313330355A170D3237303632393135313330355A3034310B300906035504061302465231123010060355040A0C094468696D796F7469733111300F06035504030C084365727469676E6130820122300D06092A864886F70D01010105000382010F003082010A0282010100C868F1C9D6D6B3347526821EECB4BEEA5CE126ED114761E1A27C16784021E4609E5AC863E1C4B19692FF186D6923E12B62F7DDE2362F9107B948CF0EEC79B62CE7344B700825A33C871B19F281070F389019D311FE86B4F2D15E1E1E96CD806CCE3B3193B6F2A0D0A995127DA59ACC6BC884568A33A9E722155316F0CC17EC575FE9A20A9809DEE35F9C6FDC48E3850B155AA6BA9FAC48E309B2F7F432DE5E34BE1C785D425BCE0E228F4D90D77D3218B30B2C6ABF8E3F141189200E7714B53D940887F7251ED5B26000EC6F2A28256E2A3E186317253F3E442016F626C825AE054AB4E7632CF38C16537E5CFB111A08C146629F22B8F1C28D69DCFA3A5806DF0203010001A381BC3081B9300F0603551D130101FF040530030101FF301D0603551D0E041604141AEDFE413990B42459BE01F252D545F65A39DC1130640603551D23045D305B80141AEDFE413990B42459BE01F252D545F65A39DC11A138A4363034310B300906035504061302465231123010060355040A0C094468696D796F7469733111300F06035504030C084365727469676E61820900FEDCE3010FC948FF300E0603551D0F0101FF040403020106301106096086480186F8420101040403020007300D06092A864886F70D0101050500038201010085031E9271F642AFE1A3619EEBF3C00FF2A5D4DA95E6D6BE68363D7E6E1F4C8AEFD10F216D5EA55263CE12F8EF2ADA6FEB37FE1302C7CB3B3E226BDA612E7FD4723DDD30E11E4C40198C0FD79CD183307B9859DC7DC6B90C294CA133A2EB673A6584D396E2ED7645708FB52BDEF923D6496E3C14B5C69F351E50D0C18F6A70440262CBAE1D6841A7AA57E853AA07D206F6D514060B9103752C6C72B561959A0D8BB90DE7F5DF54CDDEE6D8D609089763E5C12EB0B74426C026C0AF55309E3BD5362A1904F45C1EFFCF2CB7FFD0FD874011D51123BB48C021A9A4282DFD15F8B04E2BF4305B21FC119134BE41EF7B9D9775FF9795C096582FEABB46D7BBE4D92E", "hex"); +const X509_REQUEST_RAW = Buffer.from("308202BC308201A402003078310B3009060355040613025553311430120603550403130B6D792D737974652E6E6574311430120603550407130B53756E20416E746F6E696F311D301B060355040A13144D7920686F6D65206F7267616E697A6174696F6E310F300D06035504081306546573786173310D300B060355040B13044E6F6E6530820122300D06092A864886F70D01010105000382010F003082010A028201010092323A4560FF7FB0C022B6A9B72FE2F29F544AB8AAA4CFD1A1A71D9D0EB7B89CE85505DE15AC11785EDC5FFE45BC6B39E0688B7680FE1AFA42E36C50070AB52F01C1E86B139D10C9A0729CECDBF3CDF6FF538B6C2AE80498D6EAD5C90AC46131FD542C9EF0F400FCDA341E6CB61BA3C612D17A6CACB6415FBCFBF912E16BDCC3689C8C95BBE0C118884FC8A0F9597CB734B4C84A451FCB511BE6C7FDE0F45FE5B386CD32C675249012C3E2A0F18AB8DC880A960831943747E8C92F1972DDF8C18C59E07D59E98609B62B94FF88172D928D3B14FB8D66B4A6DE8B6DAE3AB6552F5CC8BFD1CF97DFB252EB551DBE2AF33826B3E26190ED48646556068196369DBB0203010001A000300D06092A864886F70D01010B050003820101001EBF4FF997C237C6001D4170BB8FCF64E3B3137D7746F4E08A3F884A127F235665EBBBB497FF8691AED2E1268728FFFF902ED577C86BDA86A59DFED036FEEAF7DE7B766F5AF1F7A08A7432C3B6F99C7223D0B76067A8D789B168F28E8FDEBD8D5F7EFFFE1F38EAAA0DB5BB1F861E9463B1299CC00E5329D24D8D0F049E650FEC4D62143651EBEDFF10795F0B1BC325EAC01951E2344FFD8850BF6A3FC1304FD4C4136CF27FE443A69B39F92F07A7F48BC8AC2AF3C9F3FD8236424DB838806F884677CCD122DE815C400E726A24B8A9E4D50FF75EFBCC2F8DCED7E88C4E727B1BAD84E0FA0F65A91D1D7FF54AF7279A33043ECAF205CDFACD05511E7E0641A970", "hex"); +const X509_PEM = core.PemConverter.fromBufferSource(X509_RAW, "CERTIFICATE"); +const X509_REQUEST_PEM = core.PemConverter.fromBufferSource(X509_REQUEST_RAW, "CERTIFICATE REQUEST"); + +(isNSS("CertStorage. NSS is readonly") + ? context.skip + : context) + ("Certificate storage", () => { + + beforeEach(async () => { + let keys = await crypto.certStorage.keys(); + if (keys.length) { + await crypto.certStorage.clear(); + } + + keys = await crypto.certStorage.keys(); + assert.strictEqual(keys.length, 0); + }); + + context("indexOf", () => { + const vector: { + type: types.CryptoCertificateType; + data: string | types.BufferSource; + format: types.CryptoCertificateFormat; + }[] = [ + { type: "x509", data: X509_RAW, format: "raw" }, + { type: "request", data: X509_REQUEST_RAW, format: "raw" }, + { type: "x509", data: X509_PEM, format: "pem" }, + { type: "request", data: X509_REQUEST_PEM, format: "pem" }, + ]; + vector.forEach((params) => { + it(`${params.type} ${params.format}`, async () => { + const cert = await crypto.certStorage.importCert(params.format, params.data, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" } as types.RsaHashedImportParams, ["verify"]); + const index = await crypto.certStorage.setItem(cert); + const found = await crypto.certStorage.indexOf(cert); + assert.strictEqual(found, null); + const certByIndex = await crypto.certStorage.getItem(index, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" } as types.RsaHashedImportParams, ["verify"]); + assert.strictEqual(!!certByIndex, true, "Cannot get cert item from storage"); + }); + }); + }); + + context("importCert", () => { + + it("x509", async () => { + const item = await crypto.certStorage.importCert("raw", X509_RAW, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" } as types.RsaHashedImportParams, ["verify"]) as X509Certificate; + const json = item.toJSON(); + assert.strictEqual(json.publicKey.algorithm.name, "RSASSA-PKCS1-v1_5"); + assert.strictEqual((json.publicKey.algorithm as Pkcs11RsaHashedKeyAlgorithm).hash.name, "SHA-256"); + assert.strictEqual(json.notBefore.toISOString(), "2007-06-29T15:13:05.000Z"); + assert.strictEqual(json.notAfter.toISOString(), "2027-06-29T15:13:05.000Z"); + assert.strictEqual(json.subjectName, "C=FR, O=Dhimyotis, CN=Certigna"); + assert.strictEqual(json.issuerName, "C=FR, O=Dhimyotis, CN=Certigna"); + assert.strictEqual(json.serialNumber, "00fedce3010fc948ff"); + assert.strictEqual(json.type, "x509"); + + assert.strictEqual(item.label, "Certigna"); + assert.strictEqual(item.token, false); + assert.strictEqual(item.sensitive, false); + }); + + it("x509 to token", async () => { + const item = await crypto.certStorage.importCert( + "raw", + X509_RAW, + { + name: "RSASSA-PKCS1-v1_5", + hash: "SHA-256", + token: true, + label: "custom", + } as types.RsaHashedImportParams, + ["verify"]); + + assert.ok(item instanceof X509Certificate); + assert.strictEqual(item.label, "custom"); + assert.strictEqual(item.token, true); + assert.strictEqual(item.sensitive, false); + }); + + it("request", async () => { + const item = await crypto.certStorage.importCert("raw", X509_REQUEST_RAW, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-384" } as types.RsaHashedImportParams, ["verify"]) as X509CertificateRequest; + const json = item.toJSON(); + assert.strictEqual(json.publicKey.algorithm.name, "RSASSA-PKCS1-v1_5"); + assert.strictEqual((json.publicKey.algorithm as Pkcs11RsaHashedKeyAlgorithm).hash.name, "SHA-384"); + assert.strictEqual(json.subjectName, "C=US, CN=my-syte.net, L=Sun Antonio, O=My home organization, ST=Tesxas, OU=None"); + assert.strictEqual(json.type, "request"); + + assert.strictEqual(item.label, "X509 Request"); + assert.strictEqual(item.token, false); + assert.strictEqual(item.sensitive, false); + }); + + it("request to token", async () => { + const item = await crypto.certStorage.importCert("raw", X509_REQUEST_RAW, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-384", token: true, label: "custom" } as types.RsaHashedImportParams, ["verify"]) as X509CertificateRequest; + + assert.strictEqual(item.label, "custom"); + assert.strictEqual(item.token, true); + assert.strictEqual(item.sensitive, false); + }); + + it("wrong type throws error", async () => { + await assert.rejects(crypto.certStorage.importCert("wrong" as any, X509_REQUEST_RAW, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-384" } as types.RsaHashedImportParams, ["verify"])); + }); + + }); + + context("set/get item", () => { + + it("x509", async () => { + const x509 = await crypto.certStorage.importCert("raw", X509_RAW, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" } as types.RsaHashedImportParams, ["verify"]); + const index = await crypto.certStorage.setItem(x509); + const x5092 = await crypto.certStorage.getItem(index); + assert.strictEqual(!!x5092, true); + }); + + it("request", async () => { + const request = await crypto.certStorage.importCert("raw", X509_REQUEST_RAW, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" } as types.RsaHashedImportParams, ["verify"]); + const index = await crypto.certStorage.setItem(request); + const request2 = await crypto.certStorage.getItem(index, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" } as types.RsaHashedImportParams, ["verify"]); + assert.strictEqual(!!request2, true); + }); + + it("null", async () => { + const item = await crypto.certStorage.getItem("not exist"); + assert.strictEqual(item, null); + }); + + it("set wrong object", async () => { + await assert.rejects(crypto.certStorage.setItem({} as any), Error); + }); + + }); + + context("get value", () => { + + it("x509", async () => { + const x509 = await crypto.certStorage.importCert("raw", X509_RAW, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" } as types.RsaHashedImportParams, ["verify"]); + const index = await crypto.certStorage.setItem(x509); + const raw = await crypto.certStorage.getValue(index); + assert.strictEqual(!!raw, true); + assert.strictEqual(raw!.byteLength > 0, true); + }); + + it("request", async () => { + const request = await crypto.certStorage.importCert("raw", X509_REQUEST_RAW, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" } as types.RsaHashedImportParams, ["verify"]); + const index = await crypto.certStorage.setItem(request); + const raw = await crypto.certStorage.getValue(index); + assert.strictEqual(!!raw, true); + assert.strictEqual(raw!.byteLength > 0, true); + }); + + it("null", async () => { + const item = await crypto.certStorage.getItem("not exist"); + assert.strictEqual(item, null); + }); + + it("set wrong object", async () => { + await assert.rejects(crypto.certStorage.setItem({} as any), Error); + }); + + }); + + it("removeItem", async () => { + let indexes = await crypto.certStorage.keys(); + assert.strictEqual(indexes.length, 0); + + const request = await crypto.certStorage.importCert("raw", X509_REQUEST_RAW, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" } as types.RsaHashedImportParams, ["verify"]); + await crypto.certStorage.setItem(request); + + const x509 = await crypto.certStorage.importCert("raw", X509_RAW, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" } as types.RsaHashedImportParams, ["verify"]); + const x509Index = await crypto.certStorage.setItem(x509); + + indexes = await crypto.certStorage.keys(); + assert.strictEqual(indexes.length, 2); + + await crypto.certStorage.removeItem(x509Index); + + indexes = await crypto.certStorage.keys(); + assert.strictEqual(indexes.length, 1); + }); + + it("exportCert", async () => { + const x509 = await crypto.certStorage.importCert("raw", X509_RAW, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" } as types.RsaHashedImportParams, ["verify"]); + const raw = await crypto.certStorage.exportCert("raw", x509); + assert.strictEqual(Buffer.from(raw).equals(X509_RAW), true); + }); + + it("test", async () => { + const alg = { + name: "RSASSA-PKCS1-v1_5", + hash: "SHA-256", + publicExponent: new Uint8Array([1, 0, 1]), + modulusLength: 2048 + }; + + const keys = await crypto.subtle.generateKey( + { + ...alg, + token: true, + } as any, + false, + [ + "sign", + "verify" + ]); + const keyIndex = await crypto.keyStorage.setItem(keys.privateKey); + + const cert = await x509.X509CertificateGenerator.createSelfSigned( + { + serialNumber: "01", + name: "CN=Test", + notBefore: new Date("2020/01/01"), + notAfter: new Date("2020/01/02"), + signingAlgorithm: alg, + keys, + extensions: [ + new x509.BasicConstraintsExtension(true, 2, true), + new x509.ExtendedKeyUsageExtension( + ["1.2.3.4.5.6.7", "2.3.4.5.6.7.8"], + true + ), + new x509.KeyUsagesExtension( + x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, + true + ) + ] + }, + crypto, + ); + + const fortifyCert = await crypto.certStorage.importCert( + "raw", + cert.rawData, + { + ...alg, + token: true, + }, + ["verify"] + ); + const certIndex = await crypto.certStorage.setItem(fortifyCert); + + assert.strictEqual(keyIndex.split("-")[2], certIndex.split("-")[2]); + }); + + }); diff --git a/packages/pkcs11/test/config.ts b/packages/pkcs11/test/config.ts new file mode 100644 index 0000000..d5d07a9 --- /dev/null +++ b/packages/pkcs11/test/config.ts @@ -0,0 +1,26 @@ +import * as os from "os"; +import { Crypto } from "../src"; + +export const config = process.env.PV_CRYPTO === "nss" ? + { + library: os.platform() === "darwin" ? "/usr/local/opt/nss/lib/libsoftokn3.dylib" : "/usr/lib/x86_64-linux-gnu/nss/libsoftokn3.so", + libraryParameters: `configdir='' certPrefix='' keyPrefix='' secmod='' flags=readOnly,noCertDB,noModDB,forceOpen,optimizeSpace`, + name: "NSS", + slot: 1, + readWrite: true, + } + : + { + library: "/usr/local/lib/softhsm/libsofthsm2.so", + name: "SoftHSMv2", + slot: 0, + readWrite: true, + pin: "12345", + }; + +console.log(`PKCS11 provider: ${config.name} at ${config.library}`); +export const crypto = new Crypto(config); + +process.on("beforeExit", () => { + crypto.close(); +}); diff --git a/packages/pkcs11/test/crypto.ts b/packages/pkcs11/test/crypto.ts new file mode 100644 index 0000000..83d3d87 --- /dev/null +++ b/packages/pkcs11/test/crypto.ts @@ -0,0 +1,70 @@ +import * as types from "@peculiar/webcrypto-types"; +import * as assert from "assert"; +import * as graphene from "graphene-pk11"; +import { ITemplateBuilder, ITemplateBuildParameters, ITemplate, Pkcs11AesKeyAlgorithm } from "../src/types"; +import { config, crypto } from "./config"; + +context("Crypto", () => { + + it("get random values", () => { + const buf = new Uint8Array(16); + const check = Buffer.from(buf).toString("base64"); + assert.notStrictEqual(Buffer.from(crypto.getRandomValues(buf)).toString("base64"), check, "Has no random values"); + }); + + it("get random values with large buffer", () => { + const buf = new Uint8Array(65600); + assert.throws(() => { + crypto.getRandomValues(buf); + }, Error); + }); + + it("reset", () => { + const currentHandle = crypto.session.handle.toString("hex"); + crypto.reset(); + + if (config.pin) { + crypto.login(config.pin); + } + const newHandle = crypto.session.handle.toString("hex"); + assert.strictEqual(currentHandle !== newHandle, true, "handle of session wasn't changed"); + }); + + context("custom template builder", () => { + class CustomTemplateBuilder implements ITemplateBuilder { + + build(params: ITemplateBuildParameters): ITemplate { + return { + label: "CustomTemplate", + token: false, + sensitive: false, + class: graphene.ObjectClass.SECRET_KEY, + encrypt: true, + decrypt: false, + sign: false, + verify: false, + wrap: false, + unwrap: false, + derive: false, + }; + } + + } + + const templateBuilder = crypto.templateBuilder; + before(() => { + crypto.templateBuilder = new CustomTemplateBuilder(); + }); + + after(() => { + crypto.templateBuilder = templateBuilder; + }); + + it("create AES-CBC", async () => { + const key = await crypto.subtle.generateKey({ name: "AES-CBC", length: 128 } as types.AesKeyGenParams, true, ["encrypt", "decrypt"]); + assert.strictEqual((key.algorithm as Pkcs11AesKeyAlgorithm).label, "CustomTemplate"); + assert.deepStrictEqual(key.usages, ["encrypt"]); + }); + }); + +}); diff --git a/packages/pkcs11/test/ec.ts b/packages/pkcs11/test/ec.ts new file mode 100644 index 0000000..93f80e6 --- /dev/null +++ b/packages/pkcs11/test/ec.ts @@ -0,0 +1,138 @@ +import * as types from "@peculiar/webcrypto-types"; +import * as assert from "assert"; +import { Convert } from "pvtsutils"; +import { EcCryptoKey } from "../src/mechs"; +import { Pkcs11EcKeyGenParams } from "../src/types"; +import { crypto } from "./config"; + +context("EC", () => { + + context("token", () => { + + it("generate", async () => { + const alg: Pkcs11EcKeyGenParams = { + name: "ECDSA", + namedCurve: "P-256", + label: "custom", + token: true, + sensitive: true, + }; + + const keys = await crypto.subtle.generateKey(alg, false, ["sign", "verify"]); + + const privateKey = keys.privateKey as EcCryptoKey; + assert.strictEqual(privateKey.algorithm.token, true); + assert.strictEqual(privateKey.algorithm.label, alg.label); + assert.strictEqual(privateKey.algorithm.sensitive, true); + + const publicKey = keys.publicKey as EcCryptoKey; + assert.strictEqual(publicKey.algorithm.token, true); + assert.strictEqual(publicKey.algorithm.label, alg.label); + assert.strictEqual(publicKey.algorithm.sensitive, false); + }); + + it("import", async () => { + const alg: Pkcs11EcKeyGenParams = { + name: "ECDSA", + namedCurve: "P-256", + label: "custom", + token: true, + sensitive: true, + }; + const spki = Convert.FromBase64("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7MvmXG6zXIRe0q6S9IWJxqeiAl++411K6TGJKtAbs32jxVnLvWGR+QElM0CRs/Xgit5g1xGywroh0cN3cJBbA=="); + + const publicKey = await crypto.subtle.importKey("spki", spki, alg, false, ["verify"]) as EcCryptoKey; + + assert.strictEqual(publicKey.algorithm.token, true); + assert.strictEqual(publicKey.algorithm.label, alg.label); + assert.strictEqual(publicKey.algorithm.sensitive, false); + }); + + }); + + context("Extra ECC named curves", () => { + const namedCurves = [ + "brainpoolP160r1", + "brainpoolP160t1", + "brainpoolP192r1", + "brainpoolP192t1", + "brainpoolP224r1", + "brainpoolP224t1", + "brainpoolP256r1", + "brainpoolP256t1", + "brainpoolP320r1", + "brainpoolP320t1", + "brainpoolP384r1", + "brainpoolP384t1", + "brainpoolP512r1", + "brainpoolP512t1", + ]; + + context("sign/verify + pkcs8/spki", () => { + const data = new Uint8Array(10); + + namedCurves.forEach((namedCurve) => { + it(namedCurve, async () => { + const alg: types.EcKeyGenParams = { name: "ECDSA", namedCurve }; + const signAlg = { ...alg, hash: "SHA-256" } as types.EcdsaParams; + + const keys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + + const signature = await crypto.subtle.sign(signAlg, keys.privateKey, data); + + const ok = await crypto.subtle.verify(signAlg, keys.publicKey, signature, data); + assert.ok(ok); + + const pkcs8 = await crypto.subtle.exportKey("pkcs8", keys.privateKey); + const spki = await crypto.subtle.exportKey("spki", keys.publicKey); + + const privateKey = await crypto.subtle.importKey("pkcs8", pkcs8, alg, true, ["sign"]); + const publicKey = await crypto.subtle.importKey("spki", spki, alg, true, ["verify"]); + + const signature2 = await crypto.subtle.sign(signAlg, privateKey, data); + const ok2 = await crypto.subtle.verify(signAlg, keys.publicKey, signature2, data); + assert.ok(ok2); + + const ok3 = await crypto.subtle.verify(signAlg, publicKey, signature, data); + assert.ok(ok3); + }); + }); + }); + + context("deriveBits + jwk", () => { + namedCurves.forEach((namedCurve) => { + const test = [ + // Skip next curves, SoftHSM throws CKR_FUNCTION_FAILED + "brainpoolP160r1", + "brainpoolP160t1", + "brainpoolP192r1", + "brainpoolP192t1", + "brainpoolP224r1", + "brainpoolP224t1", + ].includes(namedCurve) + ? it.skip + : it; + test(namedCurve, async () => { + const alg: types.EcKeyGenParams = { name: "ECDH", namedCurve }; + + const keys = await crypto.subtle.generateKey(alg, true, ["deriveBits", "deriveKey"]); + + const deriveAlg: types.EcdhKeyDeriveParams = { name: "ECDH", public: keys.publicKey }; + const derivedBits = await crypto.subtle.deriveBits(deriveAlg, keys.privateKey, 128); + + const privateJwk = await crypto.subtle.exportKey("jwk", keys.privateKey); + const publicJwk = await crypto.subtle.exportKey("jwk", keys.publicKey); + const privateKey = await crypto.subtle.importKey("jwk", privateJwk, alg, true, ["deriveBits"]); + const publicKey = await crypto.subtle.importKey("jwk", publicJwk, alg, true, []); + + const derivedBits2 = await crypto.subtle.deriveBits({ name: "ECDH", public: keys.publicKey } as types.EcdhKeyDeriveParams, privateKey, 128); + const derivedBits3 = await crypto.subtle.deriveBits({ name: "ECDH", public: publicKey } as types.EcdhKeyDeriveParams, keys.privateKey, 128); + + assert.strictEqual(Convert.ToHex(derivedBits2), Convert.ToHex(derivedBits)); + assert.strictEqual(Convert.ToHex(derivedBits3), Convert.ToHex(derivedBits)); + }); + }); + }); + }); + +}); diff --git a/packages/pkcs11/test/helper.ts b/packages/pkcs11/test/helper.ts new file mode 100644 index 0000000..7b243d5 --- /dev/null +++ b/packages/pkcs11/test/helper.ts @@ -0,0 +1,30 @@ +import { CryptoKey } from "../src"; +import { config } from "./config"; + +/** + * Returns true if blobs from keys are equal + * @param a Crypto key + * @param b Crypto key + */ +export function isKeyEqual(a: CryptoKey, b: CryptoKey) { + if (a instanceof CryptoKey && b instanceof CryptoKey) { + return (a as any).data.equals((b as any).data); + } + return false; +} + +function testManufacturer(manufacturerID: string, message: string) { + if (config.name === manufacturerID) { + console.warn(" \x1b[33mWARN:\x1b[0m Test is not supported for %s. %s", manufacturerID, message || ""); + return true; + } + return false; +} + +export function isSoftHSM(message: string) { + return testManufacturer("SoftHSMv2", message); +} + +export function isNSS(message: string) { + return testManufacturer("NSS", message); +} diff --git a/packages/pkcs11/test/hmac.ts b/packages/pkcs11/test/hmac.ts new file mode 100644 index 0000000..b294db8 --- /dev/null +++ b/packages/pkcs11/test/hmac.ts @@ -0,0 +1,45 @@ +import * as assert from "assert"; +import { HmacCryptoKey } from "../src/mechs"; +import { Pkcs11HmacKeyGenParams, Pkcs11HmacKeyImportParams } from "../src/types"; +import { crypto } from "./config"; + +context("HMAC", () => { + + context("token", () => { + + it("generate", async () => { + const alg: Pkcs11HmacKeyGenParams = { + name: "HMAC", + hash: "SHA-256", + label: "custom", + token: true, + sensitive: true, + }; + + const key = await crypto.subtle.generateKey(alg, false, ["sign", "verify"]); + + assert.strictEqual(key.algorithm.token, true); + assert.strictEqual(key.algorithm.label, alg.label); + assert.strictEqual(key.algorithm.sensitive, true); + }); + + it("import", async () => { + const alg: Pkcs11HmacKeyImportParams = { + name: "HMAC", + hash: "SHA-256", + label: "custom", + token: true, + sensitive: true, + }; + const raw = Buffer.from("1234567890abcdef1234567809abcdef"); + + const key = await crypto.subtle.importKey("raw", raw, alg, false, ["sign", "verify"]) as HmacCryptoKey; + + assert.strictEqual(key.algorithm.token, true); + assert.strictEqual(key.algorithm.label, alg.label); + assert.strictEqual(key.algorithm.sensitive, true); + }); + + }); + +}); diff --git a/packages/pkcs11/test/key_storage.ts b/packages/pkcs11/test/key_storage.ts new file mode 100644 index 0000000..282a30a --- /dev/null +++ b/packages/pkcs11/test/key_storage.ts @@ -0,0 +1,328 @@ +import * as types from "@peculiar/webcrypto-types"; +import * as assert from "assert"; +import { CryptoKey } from "../src"; +import { Pkcs11RsaHashedKeyAlgorithm, Pkcs11EcKeyAlgorithm } from "../src/types"; +import { crypto } from "./config"; +import { isNSS } from "./helper"; + +(isNSS("KeyStorage. NSS is readonly") + ? context.skip + : context) + ("KeyStorage", () => { + + beforeEach(async () => { + let keys = await crypto.keyStorage.keys(); + if (keys.length) { + await crypto.keyStorage.clear(); + } + keys = await crypto.keyStorage.keys(); + assert.strictEqual(keys.length, 0); + }); + + context("indexOf", () => { + ["privateKey", "publicKey"].forEach((type) => { + it(type, async () => { + const algorithm: types.RsaHashedKeyGenParams = { + name: "RSASSA-PKCS1-v1_5", + hash: "SHA-256", + publicExponent: new Uint8Array([1, 0, 1]), + modulusLength: 1024, + }; + const keys = await crypto.subtle.generateKey(algorithm, false, ["sign", "verify"]); + const key = (keys as any)[type] as CryptoKey; + + const index = await crypto.keyStorage.setItem(key); + const found = await crypto.keyStorage.indexOf(key); + assert.strictEqual(found, null); + + const keyByIndex = await crypto.keyStorage.getItem(index); + assert.strictEqual(keyByIndex.key.id.toString("hex"), key.key.id.toString("hex")); + }); + }); + }); + + context("set/get item", () => { + + it("secret key", async () => { + let indexes = await crypto.keyStorage.keys(); + assert.strictEqual(indexes.length, 0); + const algorithm: types.AesKeyGenParams = { + name: "AES-CBC", + length: 256, + }; + const key = await crypto.subtle.generateKey(algorithm, true, ["encrypt", "decrypt"]) as CryptoKey; + assert.strictEqual(!!key, true, "Has no key value"); + + assert.strictEqual(key.algorithm.token, false); + assert.strictEqual(key.algorithm.label, "AES-256"); + assert.strictEqual(key.algorithm.sensitive, false); + + // Set key + const index = await crypto.keyStorage.setItem(key); + + // Check indexes amount + indexes = await crypto.keyStorage.keys(); + assert.strictEqual(indexes.length, 1, "Wrong amount of indexes in storage"); + assert.strictEqual(indexes[0], index, "Wrong index of item in storage"); + + // Get key + const aesKey = await crypto.keyStorage.getItem(index); + assert.strictEqual(!!aesKey, true); + assert.strictEqual(aesKey.key.id.toString("hex"), key.key.id.toString("hex")); + assert.strictEqual(aesKey.algorithm.token, true); + assert.strictEqual(aesKey.algorithm.label, "AES-256"); + assert.strictEqual(aesKey.algorithm.sensitive, false); + }); + + it("public/private keys", async () => { + const indexes = await crypto.keyStorage.keys(); + assert.strictEqual(indexes.length, 0); + const algorithm: types.RsaHashedKeyGenParams = { + name: "RSASSA-PKCS1-v1_5", + hash: "SHA-256", + publicExponent: new Uint8Array([1, 0, 1]), + modulusLength: 2048, + }; + const keys = await crypto.subtle.generateKey(algorithm, false, ["sign", "verify"]); + assert(keys, "Has no keys"); + assert(keys.privateKey, "Has no private key"); + assert(keys.publicKey, "Has no public key"); + assert.strictEqual(keys.privateKey.extractable, false); + assert.strictEqual(keys.publicKey.extractable, true); + + // Set keys + const privateKeyIndex = await crypto.keyStorage.setItem(keys.privateKey); + const publicKeyIndex = await crypto.keyStorage.setItem(keys.publicKey); + + // Get keys + const privateKey = await crypto.keyStorage.getItem(privateKeyIndex); + assert(privateKey); + assert.strictEqual(privateKey.extractable, false); + + const publicKey = await crypto.keyStorage.getItem(publicKeyIndex); + assert(publicKey); + assert.strictEqual(publicKey.extractable, true); + }); + }); + + it("remove item", async () => { + const algorithm: types.RsaHashedKeyGenParams = { + name: "RSASSA-PKCS1-v1_5", + hash: "SHA-256", + publicExponent: new Uint8Array([1, 0, 1]), + modulusLength: 2048, + }; + const keys = await crypto.subtle.generateKey(algorithm, false, ["sign", "verify"]); + + // Set keys to storage + await crypto.keyStorage.setItem(keys.publicKey); + await crypto.keyStorage.setItem(keys.privateKey); + + // Check indexes amount + let indexes = await crypto.keyStorage.keys(); + assert.strictEqual(indexes.length, 2); + + // Remove first item + await crypto.keyStorage.removeItem(indexes[0]); + + // Check indexes amount + indexes = await crypto.keyStorage.keys(); + assert.strictEqual(indexes.length, 1); + }); + + context("getItem", () => { + + it("wrong key identity", async () => { + const key = await crypto.keyStorage.getItem("key not exist"); + assert.strictEqual(key, null); + }); + + context("with algorithm", () => { + it("RSASSA-PKCS1-v1_5", async () => { + const algorithm: types.RsaHashedKeyGenParams = { + name: "RSA-PSS", + hash: "SHA-1", + publicExponent: new Uint8Array([1, 0, 1]), + modulusLength: 2048, + }; + const keys = await crypto.subtle.generateKey(algorithm, true, ["sign", "verify", "encrypt", "decrypt"]); + + // Set key to storage + const index = await crypto.keyStorage.setItem(keys.publicKey); + + // Check indexes + const indexes = await crypto.keyStorage.keys(); + assert.strictEqual(indexes.length, 1); + + // Get key from storage with default algorithm + const keyDefault = await crypto.keyStorage.getItem(index); + assert.strictEqual(keyDefault.algorithm.name, "RSASSA-PKCS1-v1_5"); + assert.strictEqual((keyDefault.algorithm as Pkcs11RsaHashedKeyAlgorithm).hash.name, "SHA-256"); + assert.deepStrictEqual(keyDefault.usages, ["encrypt", "verify"]); + + // Get key from storage and set algorithm + const key = await crypto.keyStorage.getItem( + index, + { name: "RSASSA-PKCS1-v1_5", hash: "SHA-512" } as types.RsaHashedImportParams, + false, + ["verify"], + ); + assert.strictEqual(key.algorithm.name, "RSASSA-PKCS1-v1_5"); + assert.strictEqual((key.algorithm as Pkcs11RsaHashedKeyAlgorithm).hash.name, "SHA-512"); + assert.strictEqual(key.extractable, false); + assert.deepStrictEqual(key.usages, ["verify"]); + }); + + context("with default algorithm", () => { + + it("RSASSA-PKCS1-v1_5", async () => { + const keys = await crypto.subtle.generateKey( + { + name: "RSA-PSS", + hash: "SHA-1", + publicExponent: new Uint8Array([1, 0, 1]), + modulusLength: 2048, + } as types.RsaHashedKeyGenParams, + false, + ["sign", "verify"], + ); + + // Set key to storage + const index = await crypto.keyStorage.setItem(keys.publicKey); + + // Check indexes + const indexes = await crypto.keyStorage.keys(); + assert.strictEqual(indexes.length, 1); + + // Get key from storage with default alg + const key = await crypto.keyStorage.getItem(index); + + assert.strictEqual(key.algorithm.name, "RSASSA-PKCS1-v1_5"); + assert.strictEqual((key.algorithm as Pkcs11RsaHashedKeyAlgorithm).hash.name, "SHA-256"); + assert.strictEqual(key.usages.join(","), "verify"); + }); + + it("ECDSA P-256", async () => { + const keys = await crypto.subtle.generateKey( + { + name: "ECDSA", + namedCurve: "P-256", + } as types.EcKeyGenParams, + false, + ["sign", "verify"], + ); + + // Set key to storage + const index = await crypto.keyStorage.setItem(keys.publicKey); + + // Check indexes + const indexes = await crypto.keyStorage.keys(); + assert.strictEqual(indexes.length, 1); + + // Get key from storage with default alg + const key = await crypto.keyStorage.getItem(index); + assert.strictEqual(key.algorithm.name, "ECDSA"); + assert.strictEqual((key.algorithm as Pkcs11EcKeyAlgorithm).namedCurve, "P-256"); + assert.strictEqual(key.usages.join(","), "verify"); + }); + + it("ECDSA P-521", async () => { + const keys = await crypto.subtle.generateKey({ + name: "ECDSA", + namedCurve: "P-521", + } as types.EcKeyGenParams, + false, + ["sign", "verify"], + ); + + // Set key to storage + const index = await crypto.keyStorage.setItem(keys.publicKey); + + // Check indexes + const indexes = await crypto.keyStorage.keys(); + assert.strictEqual(indexes.length, 1); + + // Get key from storage with default alg + const key = await crypto.keyStorage.getItem(index); + assert.strictEqual(key.algorithm.name, "ECDSA"); + assert.strictEqual((key.algorithm as Pkcs11EcKeyAlgorithm).namedCurve, "P-521"); + assert.strictEqual(key.usages.join(","), "verify"); + }); + + it("RSA-OAEP", async () => { + const keys = await crypto.subtle.generateKey({ + name: "RSA-OAEP", + hash: "SHA-1", + publicExponent: new Uint8Array([1, 0, 1]), + modulusLength: 2048, + } as types.RsaHashedKeyGenParams, + false, + ["encrypt", "decrypt"], + ); + + // Set key to storage + const index = await crypto.keyStorage.setItem(keys.publicKey); + + // Check indexes + const indexes = await crypto.keyStorage.keys(); + assert.strictEqual(indexes.length, 1); + + // Get key from storage we default alg + const key = await crypto.keyStorage.getItem(index); + assert.strictEqual(key.algorithm.name, "RSA-OAEP"); + assert.strictEqual((key.algorithm as Pkcs11RsaHashedKeyAlgorithm).hash.name, "SHA-256"); + assert.strictEqual(key.usages.join(","), "encrypt"); + + }); + + it("AES-CBC", async () => { + const aesKey = await crypto.subtle.generateKey({ + name: "AES-CBC", + length: 256, + } as types.AesKeyGenParams, + false, + ["encrypt", "decrypt"], + ) as CryptoKey; + + // Set key to storage + const index = await crypto.keyStorage.setItem(aesKey); + + // Check indexes + const indexes = await crypto.keyStorage.keys(); + assert.strictEqual(indexes.length, 1); + + // Get key from storage we default alg + const key = await crypto.keyStorage.getItem(index); + assert.strictEqual(key.algorithm.name, "AES-CBC"); + assert.strictEqual(key.usages.join(","), "encrypt,decrypt"); + }); + }); + + it("ECDH", async () => { + const keys = await crypto.subtle.generateKey( + { + name: "ECDH", + namedCurve: "P-384", + } as types.EcKeyGenParams, + false, + ["deriveBits"], + ); + // Set key to storage + const index = await crypto.keyStorage.setItem(keys.publicKey); + + // Check indexes + const indexes = await crypto.keyStorage.keys(); + assert.strictEqual(indexes.length, 1); + + // Get key from storage we default alg + const key = await crypto.keyStorage.getItem(index); + assert.strictEqual(key.algorithm.name, "ECDH"); + assert.strictEqual((key.algorithm as Pkcs11EcKeyAlgorithm).namedCurve, "P-384"); + assert.strictEqual(key.usages.join(","), ""); + }); + + }); + + }); + + }); diff --git a/packages/pkcs11/test/rsa.ts b/packages/pkcs11/test/rsa.ts new file mode 100644 index 0000000..56455b8 --- /dev/null +++ b/packages/pkcs11/test/rsa.ts @@ -0,0 +1,73 @@ +import * as types from "@peculiar/webcrypto-types"; +import * as assert from "assert"; +import { RsaCryptoKey } from "../src/mechs"; +import { Pkcs11RsaHashedImportParams, Pkcs11RsaHashedKeyGenParams } from "../src/types"; +import { crypto } from "./config"; + +context("RSA", () => { + + context("token", () => { + + it("generate", async () => { + const alg: Pkcs11RsaHashedKeyGenParams = { + name: "RSASSA-PKCS1-v1_5", + hash: "SHA-256", + publicExponent: new Uint8Array([1, 0, 1]), + modulusLength: 2048, + label: "custom", + token: true, + sensitive: true, + }; + + const keys = await crypto.subtle.generateKey(alg, false, ["sign", "verify"]); + + const privateKey = keys.privateKey as RsaCryptoKey; + assert.strictEqual(privateKey.algorithm.token, true); + assert.strictEqual(privateKey.algorithm.label, alg.label); + assert.strictEqual(privateKey.algorithm.sensitive, true); + + const publicKey = keys.publicKey as RsaCryptoKey; + assert.strictEqual(publicKey.algorithm.token, true); + assert.strictEqual(publicKey.algorithm.label, alg.label); + assert.strictEqual(publicKey.algorithm.sensitive, false); + }); + + it("import", async () => { + const alg: Pkcs11RsaHashedImportParams = { + name: "RSASSA-PKCS1-v1_5", + hash: "SHA-256", + label: "custom", + token: true, + sensitive: true, + }; + const jwk = { + alg: "RS256", + e: "AQAB", + ext: true, + key_ops: ["verify"], + kty: "RSA", + n: "vqpvdxuyZ6rKYnWTj_ZzDBFZAAAlpe5hpoiYHqa2j5kK7v8U5EaPY2bLib9m4B40j-n3FV9xUCGiplWdqMJJKT-4PjGO5E3S4N9kjFhu57noYT7z7302J0sJXeoFbXxlgE-4G55Oxlm52ID2_RJesP5nzcGTriQwoRbrJP5OEt0", + }; + + const publicKey = await crypto.subtle.importKey("jwk", jwk, alg, true, ["verify"]) as RsaCryptoKey; + + assert.strictEqual(publicKey.algorithm.token, true); + assert.strictEqual(publicKey.algorithm.label, alg.label); + assert.strictEqual(publicKey.algorithm.sensitive, false); + }); + + }); + + it("RSA 3072bits", async () => { + const alg: types.RsaHashedKeyGenParams = { + name: "RSASSA-PKCS1-v1_5", + hash: "SHA-256", + publicExponent: new Uint8Array([1, 0, 1]), + modulusLength: 3072, + }; + const keys = await crypto.subtle.generateKey(alg, false, ["sign", "verify"]); + + assert.strictEqual((keys.privateKey.algorithm as types.RsaHashedKeyAlgorithm).modulusLength, 3072); + }); + +}); diff --git a/packages/pkcs11/test/subtle.ts b/packages/pkcs11/test/subtle.ts new file mode 100644 index 0000000..bb429ab --- /dev/null +++ b/packages/pkcs11/test/subtle.ts @@ -0,0 +1,88 @@ +import * as types from "@peculiar/webcrypto-types"; +import * as assert from "assert"; +import * as graphene from "graphene-pk11"; +import { ID_DIGEST } from "../src/const"; +import { CryptoKey } from "../src/key"; +import { crypto } from "./config"; + +context("Subtle", () => { + + async function getId(publicKey: types.CryptoKey) { + const raw = await crypto.subtle.exportKey("spki", publicKey); + const hash = await (await crypto.subtle.digest(ID_DIGEST, raw)).slice(0, 16); + return Buffer.from(hash).toString("hex"); + } + + context("key must have id equals to SHA-1 of public key raw", () => { + + context("generate key", () => { + + before(async () => { + crypto.keyStorage.clear(); + }); + + after(async () => { + crypto.keyStorage.clear(); + }); + + [ + { name: "RSA-PSS", hash: "SHA-256", publicExponent: new Uint8Array([1, 0, 1]), modulusLength: 1024 }, + { name: "ECDSA", namedCurve: "P-256" }, + ].map((alg) => { + it(alg.name, async () => { + const keys = await crypto.subtle.generateKey(alg, false, ["sign", "verify"]); + + const id = await getId(keys.publicKey); + assert.strictEqual((keys.publicKey as CryptoKey).key.id.toString("hex"), id); + assert.strictEqual((keys.publicKey as CryptoKey).id.includes(id), true); + assert.strictEqual((keys.publicKey as CryptoKey).p11Object.token, false); + assert.strictEqual((keys.privateKey as CryptoKey).p11Object.token, false); + assert.strictEqual(((keys.privateKey as CryptoKey).p11Object as graphene.PrivateKey).sensitive, false); + }); + }); + + context("pkcs11 attributes", () => { + [ + { name: "RSA-PSS", hash: "SHA-256", publicExponent: new Uint8Array([1, 0, 1]), modulusLength: 1024, token: true, sensitive: true, label: "RSA-PSS" }, + { name: "ECDSA", namedCurve: "P-256", token: true, sensitive: true, label: "ECDSA" }, + ].map((alg) => { + it(alg.name, async () => { + const keys = await crypto.subtle.generateKey(alg, false, ["sign", "verify"]); + + const id = await getId(keys.publicKey); + assert.strictEqual((keys.publicKey as CryptoKey).key.id.toString("hex"), id); + assert.strictEqual((keys.publicKey as CryptoKey).id.includes(id), true); + assert.strictEqual((keys.publicKey as CryptoKey).p11Object.token, true); + assert.strictEqual((keys.publicKey as CryptoKey).p11Object.label, alg.name); + assert.strictEqual((keys.privateKey as CryptoKey).p11Object.token, true); + assert.strictEqual(((keys.privateKey as CryptoKey).p11Object as graphene.PrivateKey).sensitive, true); + assert.strictEqual((keys.privateKey as CryptoKey).p11Object.label, alg.name); + }); + }); + }); + + }); + + context("import key", () => { + + const spki = Buffer.from("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoZMMqyfA16N6bvloFHmalk/SGMisr3zSXFZdR8F9UkaY7hF13hHiQtwp2YO+1zd7jwYi1Y7SMA9iUrC+ap2OCw==", "base64"); + + it("extractable public key", async () => { + const key = await crypto.subtle.importKey("spki", spki, { name: "ECDSA", namedCurve: "P-256" } as types.EcKeyImportParams, true, ["verify"]); + + const id = await getId(key); + assert.strictEqual((key as CryptoKey).key.id.toString("hex"), id); + assert.strictEqual((key as CryptoKey).id.includes(id), true); + }); + + it("don't try to update id if key is not extractable", async () => { + const key = await crypto.subtle.importKey("spki", spki, { name: "ECDSA", namedCurve: "P-256" } as types.EcKeyImportParams, false, ["verify"]); + + assert.notStrictEqual((key as CryptoKey).key.id.toString("hex"), "69e4556056c8d300eff3d4523fc6515d9f833fe6"); + }); + + }); + + }); + +}); diff --git a/packages/pkcs11/test/vectors.ts b/packages/pkcs11/test/vectors.ts new file mode 100644 index 0000000..2c48aa4 --- /dev/null +++ b/packages/pkcs11/test/vectors.ts @@ -0,0 +1,153 @@ +import * as types from "@peculiar/webcrypto-types"; +import * as test from "@peculiar/webcrypto-test"; +import * as config from "./config"; +import { isNSS, isSoftHSM } from "./helper"; + +function fixEcImport(item: test.ITestImportAction) { + if (item.name?.startsWith("JWK private key")) { + const jwk = item.data as types.JsonWebKey; + delete jwk.x; + delete jwk.y; + } + if (item.name?.startsWith("PKCS8 P-256")) { + item.data = Buffer.from("3041020100301306072a8648ce3d020106082a8648ce3d030107042730250201010420895118e4e168dc9ee0d419d2c3f5845b2918fda96b84d9a91012f2ffb70d9ee1", "hex"); + } + if (item.name?.startsWith("PKCS8 P-384")) { + item.data = Buffer.from("304e020100301006072a8648ce3d020106052b8104002204373035020101043098d7c6a318f0a02efe1a17552492884c11a079314d4cc9f92e1504905436072d61539fc7fd73371eeda4c80e3902c743", "hex"); + } + if (item.name?.startsWith("PKCS8 P-521")) { + item.data = Buffer.from("3060020100301006072a8648ce3d020106052b81040023044930470201010442006c71a419f8a4e6ad25f99308ef475ba5319678acb5f9cde61bdf301e69e953e7766c0adc603397728aa0e4873fa679ad1efc6693e125df7bb75e880638d28f968b", "hex"); + } +} + +// Fix EC import tests. +// PKCS#11 doesn't return public key from private key +test.vectors.ECDSA.actions.import?.forEach(fixEcImport); +test.vectors.ECDH.actions.import?.forEach(fixEcImport); +test.vectors.ECDH.actions.deriveKey?.forEach((item) => { + if (item.name === "P-521 256") { + // module doesn't support AES-CTR + item.derivedKeyType.name = "AES-CBC"; + } +}); + +// WebcryptoTest.check(config.crypto, [ +// vectors.AES128CBC, +// ]); +test.WebcryptoTest.check(config.crypto, { + AES128KW: true, + AES192KW: true, + AES256KW: true, + RSAOAEP: true, + PBKDF2: true, + HKDF: true, + DESCBC: true, + DESEDE3CBC: true, + RSAESPKCS1: true, + AES128CMAC: true, + AES192CMAC: true, + AES256CMAC: true, + AES128CTR: true, + AES192CTR: true, + AES256CTR: true, +}); + +test.WebcryptoTest.add(config.crypto, { + name: "RSA-OAEP-SHA1", + actions: { + encrypt: [ + { + skip: isNSS("RSA-OAEP-SHA1 throws CKR_DEVICE_ERROR"), + name: "without label", + algorithm: { + name: "RSA-OAEP", + } as types.Algorithm, + data: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]), + encData: Buffer.from("MAKiRseL08AlR8Fmn1uVz/lDDdrDiRyI6KUW3mcE/0kxwW7/VizQJP+jiTSWyHexhQ+Sp0ugm6Doa/jahajuVf0aFkqJCcEKlSeMGvu4QdDc9tJzeNJVqSbPovFy60Criyjei4ganw2RQM2Umav//HfQEyqGTcyftMxXzkDDBQU=", "base64"), + key: { + publicKey: { + format: "jwk", + algorithm: { name: "RSA-OAEP", hash: "SHA-1" } as types.RsaHashedImportParams, + data: { + alg: "RSA-OAEP", + e: "AQAB", + ext: true, + key_ops: ["encrypt"], + kty: "RSA", + n: "vqpvdxuyZ6rKYnWTj_ZzDBFZAAAlpe5hpoiYHqa2j5kK7v8U5EaPY2bLib9m4B40j-n3FV9xUCGiplWdqMJJKT-4PjGO5E3S4N9kjFhu57noYT7z7302J0sJXeoFbXxlgE-4G55Oxlm52ID2_RJesP5nzcGTriQwoRbrJP5OEt0", + }, + extractable: true, + keyUsages: ["encrypt"], + }, + privateKey: { + format: "jwk", + algorithm: { name: "RSA-OAEP", hash: "SHA-1" } as types.RsaHashedImportParams, + data: { + alg: "RSA-OAEP", + d: "AkeIWJywp9OfYsj0ECsKmhDVBw55ZL_yU-rbIrashQ_31P6gsc_0I-SVN1rd8Hz79OJ_rTY8ZRBZ4PIyFdPoyvuo5apHdAHH6riJKxDHWPxhE-ReNVEPSTiF1ry8DSe5zC7w9BLnH_QM8bkN4cOnvgqrg7EbrGWomAGJVvoRwOM", + dp: "pOolqL7HwnmWLn7GDX8zGkm0Q1IAj-ouBL7ZZbaTm3wETLtwu-dGsQheEdzP_mfL_CTiCAwGuQBcSItimD0DdQ", + dq: "FTSY59AnkgmB7TsErWNBE3xlVB_pMpE2xWyCBCz96gyDOUOFDz8vlSV-clhjawJeRd1n30nZOPSBtOHozhwZmQ", + e: "AQAB", + ext: true, + key_ops: ["decrypt"], + kty: "RSA", + n: "vqpvdxuyZ6rKYnWTj_ZzDBFZAAAlpe5hpoiYHqa2j5kK7v8U5EaPY2bLib9m4B40j-n3FV9xUCGiplWdqMJJKT-4PjGO5E3S4N9kjFhu57noYT7z7302J0sJXeoFbXxlgE-4G55Oxlm52ID2_RJesP5nzcGTriQwoRbrJP5OEt0", + p: "6jFtmBJJQFIlQUXXZYIgvH70Y9a03oWKjNuF2veb5Zf09EtLNE86NpnIm463OnoHJPW0m8wHFXZZfcYVTIPR_w", + q: "0GttDMl1kIzSV2rNzGXpOS8tUqr5Lz0EtVZwIb9GJPMmJ0P3gZ801zEgZZ4-esU7cLUf-BSZEAmfnKA80G2jIw", + qi: "FByTxX4G2eXkk1xe0IuiEv7I5NS-CnFyp8iB4XLG0rabnfcIZFKpf__X0sNyVOAVo5-jJMuUYjCRTdaXNAWhkg", + }, + extractable: true, + keyUsages: ["decrypt"], + }, + }, + }, + { + skip: isSoftHSM("RSA-OAEP-SHA1 supports encryption without label only") + || isNSS("RSA-OAEP-SHA1 throws CKR_DEVICE_ERROR"), + name: "with label", + algorithm: { + name: "RSA-OAEP", + label: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]), + } as types.RsaOaepParams, + data: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]), + encData: Buffer.from("YLtmJDT8Y4Z2Y/VoGHUvhgs5kptNShFRUCcsKpUgI9A+YCYXL3K8fnEkbzO/Nkd4/0RsvfnmXkUJg3JdzPslwO1bOdlNsd2hRi0qi4cpxVmHDjuI3EHMb7FI3Pb9cF/kMFeEQzttpIDqh/UQJnoyh4d/RyZS1w37Vk0sNer7xw0=", "base64"), + key: { + publicKey: { + format: "jwk" as types.KeyFormat, + algorithm: { name: "RSA-OAEP", hash: "SHA-1" } as types.RsaHashedImportParams, + data: { + alg: "RSA-OAEP", + e: "AQAB", + ext: true, + key_ops: ["encrypt"], + kty: "RSA", + n: "vqpvdxuyZ6rKYnWTj_ZzDBFZAAAlpe5hpoiYHqa2j5kK7v8U5EaPY2bLib9m4B40j-n3FV9xUCGiplWdqMJJKT-4PjGO5E3S4N9kjFhu57noYT7z7302J0sJXeoFbXxlgE-4G55Oxlm52ID2_RJesP5nzcGTriQwoRbrJP5OEt0", + }, + extractable: true, + keyUsages: ["encrypt"], + }, + privateKey: { + format: "jwk", + algorithm: { name: "RSA-OAEP", hash: "SHA-1" } as types.RsaHashedImportParams, + data: { + alg: "RSA-OAEP", + d: "AkeIWJywp9OfYsj0ECsKmhDVBw55ZL_yU-rbIrashQ_31P6gsc_0I-SVN1rd8Hz79OJ_rTY8ZRBZ4PIyFdPoyvuo5apHdAHH6riJKxDHWPxhE-ReNVEPSTiF1ry8DSe5zC7w9BLnH_QM8bkN4cOnvgqrg7EbrGWomAGJVvoRwOM", + dp: "pOolqL7HwnmWLn7GDX8zGkm0Q1IAj-ouBL7ZZbaTm3wETLtwu-dGsQheEdzP_mfL_CTiCAwGuQBcSItimD0DdQ", + dq: "FTSY59AnkgmB7TsErWNBE3xlVB_pMpE2xWyCBCz96gyDOUOFDz8vlSV-clhjawJeRd1n30nZOPSBtOHozhwZmQ", + e: "AQAB", + ext: true, + key_ops: ["decrypt"], + kty: "RSA", + n: "vqpvdxuyZ6rKYnWTj_ZzDBFZAAAlpe5hpoiYHqa2j5kK7v8U5EaPY2bLib9m4B40j-n3FV9xUCGiplWdqMJJKT-4PjGO5E3S4N9kjFhu57noYT7z7302J0sJXeoFbXxlgE-4G55Oxlm52ID2_RJesP5nzcGTriQwoRbrJP5OEt0", + p: "6jFtmBJJQFIlQUXXZYIgvH70Y9a03oWKjNuF2veb5Zf09EtLNE86NpnIm463OnoHJPW0m8wHFXZZfcYVTIPR_w", + q: "0GttDMl1kIzSV2rNzGXpOS8tUqr5Lz0EtVZwIb9GJPMmJ0P3gZ801zEgZZ4-esU7cLUf-BSZEAmfnKA80G2jIw", + qi: "FByTxX4G2eXkk1xe0IuiEv7I5NS-CnFyp8iB4XLG0rabnfcIZFKpf__X0sNyVOAVo5-jJMuUYjCRTdaXNAWhkg", + }, + extractable: true, + keyUsages: ["decrypt"], + }, + }, + }, + ], + }, +});