@bsv/sdk
Version:
BSV Blockchain Software Development Kit
228 lines • 15.2 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MasterCertificate = void 0;
const Certificate_js_1 = __importDefault(require("./Certificate.js"));
const Utils = __importStar(require("../../primitives/utils.js"));
const SymmetricKey_js_1 = __importDefault(require("../../primitives/SymmetricKey.js"));
const Random_js_1 = __importDefault(require("../../primitives/Random.js"));
/**
* MasterCertificate extends the base Certificate class to manage a master keyring, enabling the creation of verifiable certificates.
*
* It allows for the selective disclosure of certificate fields by creating a `VerifiableCertificate` for a specific verifier.
* The `MasterCertificate` can securely decrypt each master key and re-encrypt it for a verifier, creating a customized
* keyring containing only the keys necessary for the verifier to access designated fields.
*
*/
class MasterCertificate extends Certificate_js_1.default {
constructor(type, serialNumber, subject, certifier, revocationOutpoint, fields, masterKeyring, signature) {
super(type, serialNumber, subject, certifier, revocationOutpoint, fields, signature);
// Ensure every field in `fields` is a string and has a corresponding key in `masterKeyring`
for (const fieldName of Object.keys(fields)) {
if (masterKeyring[fieldName] === undefined || masterKeyring[fieldName] === '') {
throw new Error(`Master keyring must contain a value for every field. Missing or empty key for field: "${fieldName}".`);
}
}
this.masterKeyring = masterKeyring;
}
/**
* Encrypts certificate fields for a subject and generates a master keyring.
* This method returns a master keyring tied to a specific certifier or subject who will validate
* and sign off on the fields, along with the encrypted certificate fields.
*
* @param {ProtoWallet} creatorWallet - The wallet of the creator responsible for encrypting the fields.
* @param {WalletCounterparty} certifierOrSubject - The certifier or subject who will validate the certificate fields.
* @param {Record<CertificateFieldNameUnder50Bytes, string>} fields - A record of certificate field names (under 50 bytes) mapped to their values.
* @param {BooleanDefaultFalse} [privileged] - Whether this is a privileged request.
* @param {DescriptionString5to50Bytes} [privilegedReason] - Reason provided for privileged access, required if this is a privileged operation. *
* @returns {Promise<CreateCertificateFieldsResult>} A promise resolving to an object containing:
* - `certificateFields` {Record<CertificateFieldNameUnder50Bytes, Base64String>}:
* The encrypted certificate fields.
* - `masterKeyring` {Record<CertificateFieldNameUnder50Bytes, Base64String>}:
* The master keyring containing encrypted revelation keys for each field.
*/
static async createCertificateFields(creatorWallet, certifierOrSubject, fields, privileged, privilegedReason) {
const certificateFields = {};
const masterKeyring = {};
for (const [fieldName, fieldValue] of Object.entries(fields)) {
const fieldSymmetricKey = SymmetricKey_js_1.default.fromRandom();
const encryptedFieldValue = fieldSymmetricKey.encrypt(Utils.toArray(fieldValue, 'utf8'));
certificateFields[fieldName] = Utils.toBase64(encryptedFieldValue);
const { ciphertext: encryptedFieldRevelationKey } = await creatorWallet.encrypt({
plaintext: fieldSymmetricKey.toArray(),
...Certificate_js_1.default.getCertificateFieldEncryptionDetails(fieldName),
counterparty: certifierOrSubject,
privileged,
privilegedReason
});
masterKeyring[fieldName] = Utils.toBase64(encryptedFieldRevelationKey);
}
return {
certificateFields,
masterKeyring
};
}
/**
* Creates a keyring for a verifier, enabling them to decrypt specific certificate fields.
* This method decrypts the master field keys for the specified fields and re-encrypts them
* for the verifier's identity key. The result is a keyring containing the keys necessary
* for the verifier to access the designated fields.
*
* @param {ProtoWallet} subjectWallet - The wallet instance of the subject, used to decrypt and re-encrypt field keys.
* @param {WalletCounterparty} verifier - The verifier who will receive access to the selectively revealed fields. Can be an identity key as hex, 'anyone', or 'self'.
* @param {string[]} fieldsToReveal - An array of field names to be revealed to the verifier. Must be a subset of the certificate's fields.
* @param {string} [originator] - Optional originator identifier, used if additional context is needed for decryption and encryption operations.
* @returns {Promise<Record<CertificateFieldNameUnder50Bytes, string>>} - A keyring mapping field names to encrypted field revelation keys, allowing the verifier to decrypt specified fields.
* @param {BooleanDefaultFalse} [privileged] - Whether this is a privileged request.
* @param {DescriptionString5to50Bytes} [privilegedReason] - Reason provided for privileged access, required if this is a privileged operation. *
* @throws {Error} Throws an error if:
* - fieldsToReveal is not an array of strings.
* - A field in `fieldsToReveal` does not exist in the certificate.
* - The decrypted master field key fails to decrypt the corresponding field (indicating an invalid key).
*/
static async createKeyringForVerifier(subjectWallet, certifier, verifier, fields, fieldsToReveal, masterKeyring, serialNumber, privileged, privilegedReason) {
if (!Array.isArray(fieldsToReveal)) {
throw new Error('fieldsToReveal must be an array of strings');
}
const fieldRevelationKeyring = {};
for (const fieldName of fieldsToReveal) {
// Make sure that fields to reveal is a subset of the certificate fields
if (fields[fieldName] === undefined || fields[fieldName] === null || fields[fieldName] === '') {
throw new Error(`Fields to reveal must be a subset of the certificate fields. Missing the "${fieldName}" field.`);
}
// Decrypt the master field key and verify that derived key actually decrypts requested field
const masterFieldKey = (await this.decryptField(subjectWallet, masterKeyring, fieldName, fields[fieldName], certifier, privileged, privilegedReason)).fieldRevelationKey;
// Encrypt derived fieldRevelationKey for verifier
const { ciphertext: encryptedFieldRevelationKey } = await subjectWallet.encrypt({
plaintext: masterFieldKey,
...Certificate_js_1.default.getCertificateFieldEncryptionDetails(fieldName, serialNumber),
counterparty: verifier,
privileged,
privilegedReason
});
// Add encryptedFieldRevelationKey to fieldRevelationKeyring
fieldRevelationKeyring[fieldName] = Utils.toBase64(encryptedFieldRevelationKey);
}
// Return the field revelation keyring which can be used to create a verifiable certificate for a verifier.
return fieldRevelationKeyring;
}
/**
* Issues a new MasterCertificate for a specified subject.
*
* This method generates a certificate containing encrypted fields and a keyring
* for the subject to decrypt all fields. Each field is encrypted with a randomly
* generated symmetric key, which is then encrypted for the subject. The certificate
* can also includes a revocation outpoint to manage potential revocation.
*
* @param {ProtoWallet} certifierWallet - The wallet of the certifier, used to sign the certificate and encrypt field keys.
* @param {WalletCounterparty} subject - The subject for whom the certificate is issued.
* @param {Record<CertificateFieldNameUnder50Bytes, string>} fields - Unencrypted certificate fields to include, with their names and values.
* @param {string} certificateType - The type of certificate being issued.
* @param {function(string, Record<CertificateFieldNameUnder50Bytes, string>?): Promise<string>} getRevocationOutpoint -
* Optional function to obtain a revocation outpoint for the certificate. Defaults to a placeholder.
* @param {function(string): Promise<void>} updateProgress - Optional callback for reporting progress updates during the operation. Defaults to a no-op.
* @returns {Promise<MasterCertificate>} - A signed MasterCertificate instance containing the encrypted fields and subject specific keyring.
*
* @throws {Error} Throws an error if any operation (e.g., encryption, signing) fails during certificate issuance.
*/
static async issueCertificateForSubject(certifierWallet, subject, fields, certificateType, getRevocationOutpoint = async (_serial) => {
void _serial; // Explicitly acknowledge unused parameter
return 'Certificate revocation not tracked.';
}, serialNumber) {
// 1. Generate a random serialNumber if not provided
const finalSerialNumber = serialNumber ?? Utils.toBase64((0, Random_js_1.default)(32));
// 2. Create encrypted certificate fields and associated master keyring
const { certificateFields, masterKeyring } = await this.createCertificateFields(certifierWallet, subject, fields);
// 3. Obtain a revocation outpoint
const revocationOutpoint = await getRevocationOutpoint(finalSerialNumber);
// 4. Create new MasterCertificate instance
const certificate = new MasterCertificate(certificateType, finalSerialNumber, subject, (await certifierWallet.getPublicKey({ identityKey: true })).publicKey, revocationOutpoint, certificateFields, masterKeyring);
// 5. Sign and return the new MasterCertificate certifying the subject.
await certificate.sign(certifierWallet);
return certificate;
}
/**
* Decrypts all fields in the MasterCertificate using the subject's or certifier's wallet.
*
* This method allows the subject or certifier to decrypt the `masterKeyring` and retrieve
* the encryption keys for each field, which are then used to decrypt the corresponding field values.
* The counterparty used for decryption depends on how the certificate fields were created:
* - If the certificate is self-signed, the counterparty should be set to 'self'.
* - Otherwise, the counterparty should always be the other party involved in the certificate issuance process (the subject or certifier).
*
* @param {ProtoWallet} subjectOrCertifierWallet - The wallet of the subject or certifier, used to decrypt the master keyring and field values.
* @param {Record<CertificateFieldNameUnder50Bytes, Base64String>} masterKeyring - A record containing encrypted keys for each field.
* @param {Record<CertificateFieldNameUnder50Bytes, Base64String>} fields - A record of encrypted field names and their values.
* @param {WalletCounterparty} counterparty - The counterparty responsible for creating or signing the certificate. For self-signed certificates, use 'self'.
* @param {BooleanDefaultFalse} [privileged] - Whether this is a privileged request.
* @param {DescriptionString5to50Bytes} [privilegedReason] - Reason provided for privileged access, required if this is a privileged operation.
* @returns {Promise<Record<CertificateFieldNameUnder50Bytes, string>>} A promise resolving to a record of field names and their decrypted values in plaintext.
*
* @throws {Error} Throws an error if the `masterKeyring` is invalid or if decryption fails for any field.
*/
static async decryptFields(subjectOrCertifierWallet, masterKeyring, fields, counterparty, privileged, privilegedReason) {
if (masterKeyring == null || Object.keys(masterKeyring).length === 0) {
throw new Error('A MasterCertificate must have a valid masterKeyring!');
}
try {
const decryptedFields = {};
// Note: we want to iterate through all fields, not just masterKeyring keys/value pairs.
for (const fieldName of Object.keys(fields)) {
decryptedFields[fieldName] = (await this.decryptField(subjectOrCertifierWallet, masterKeyring, fieldName, fields[fieldName], counterparty, privileged, privilegedReason)).decryptedFieldValue;
}
return decryptedFields;
}
catch {
throw new Error('Failed to decrypt all master certificate fields.');
}
}
static async decryptField(subjectOrCertifierWallet, masterKeyring, fieldName, fieldValue, counterparty, privileged, privilegedReason) {
if (masterKeyring == null || Object.keys(masterKeyring).length === 0) {
throw new Error('A MasterCertificate must have a valid masterKeyring!');
}
try {
const { plaintext: fieldRevelationKey } = await subjectOrCertifierWallet.decrypt({
ciphertext: Utils.toArray(masterKeyring[fieldName], 'base64'),
...Certificate_js_1.default.getCertificateFieldEncryptionDetails(fieldName),
counterparty,
privileged,
privilegedReason
});
const decryptedFieldValue = new SymmetricKey_js_1.default(fieldRevelationKey).decrypt(Utils.toArray(fieldValue, 'base64'));
return {
fieldRevelationKey,
decryptedFieldValue: Utils.toUTF8(decryptedFieldValue)
};
}
catch {
throw new Error('Failed to decrypt certificate field!');
}
}
}
exports.MasterCertificate = MasterCertificate;
//# sourceMappingURL=MasterCertificate.js.map