@mobile-wallet-protocol/client
Version:
Client SDK for the Mobile Wallet Protocol
185 lines (184 loc) • 6.39 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateKeyPair = generateKeyPair;
exports.deriveSharedSecret = deriveSharedSecret;
exports.encrypt = encrypt;
exports.decrypt = decrypt;
exports.exportKeyToHexString = exportKeyToHexString;
exports.importKeyFromHexString = importKeyFromHexString;
exports.encryptContent = encryptContent;
exports.decryptContent = decryptContent;
const aes_1 = require("@noble/ciphers/aes");
const webcrypto_1 = require("@noble/ciphers/webcrypto");
const p256_1 = require("@noble/curves/p256");
const buffer_1 = require("buffer");
const fflate_1 = require("fflate");
const util_1 = require("../type/util");
async function generateKeyPair() {
const privateKey = p256_1.secp256r1.utils.randomPrivateKey();
const publicKey = p256_1.secp256r1.getPublicKey(privateKey, false);
return {
privateKey: {
type: 'private',
algorithm: {
name: 'ECDH',
namedCurve: 'P-256',
},
extractable: true,
usages: ['deriveKey'],
_key: privateKey,
},
publicKey: {
type: 'public',
algorithm: {
name: 'ECDH',
namedCurve: 'P-256',
},
extractable: true,
usages: [],
_key: publicKey,
},
};
}
async function deriveSharedSecret(ownPrivateKey, peerPublicKey) {
const privateKeyRaw = ownPrivateKey._key;
const publicKeyRaw = peerPublicKey._key;
const sharedSecret = p256_1.secp256r1.getSharedSecret(privateKeyRaw, publicKeyRaw);
const aesKey = sharedSecret.slice(1, 33);
return {
type: 'secret',
algorithm: {
name: 'AES-GCM',
length: 256,
},
extractable: false,
usages: ['encrypt', 'decrypt'],
_key: aesKey,
};
}
async function encrypt(sharedSecret, plainText) {
const iv = (0, webcrypto_1.randomBytes)(12);
const stream = (0, aes_1.gcm)(sharedSecret._key, iv);
const plainTextBytes = new Uint8Array(buffer_1.Buffer.from(plainText, 'utf8'));
const compressedBytes = (0, fflate_1.zlibSync)(plainTextBytes);
const cipherText = stream.encrypt(compressedBytes);
return {
iv,
cipherText,
};
}
async function decrypt(sharedSecret, { iv, cipherText }) {
const stream = (0, aes_1.gcm)(new Uint8Array(sharedSecret._key), new Uint8Array(iv));
const compressedBytes = stream.decrypt(new Uint8Array(cipherText));
const plainTextBytes = (0, fflate_1.unzlibSync)(compressedBytes);
return buffer_1.Buffer.from(plainTextBytes).toString('utf8');
}
async function exportKeyToHexString(type, key) {
let exported;
if (type === 'public') {
exported = encodeECPublicKey(key._key);
}
else {
exported = key._key;
}
return (0, util_1.uint8ArrayToHex)(exported);
}
async function importKeyFromHexString(type, hexString) {
const data = (0, util_1.hexStringToUint8Array)(hexString);
const [, sequenceContent] = decodeDER(data, 0);
let importedKey;
if (type === 'public') {
// The public key is the last 65 bytes of the sequence
importedKey = sequenceContent.slice(-65);
}
else {
if (!p256_1.secp256r1.utils.isValidPrivateKey(data)) {
throw new Error('Invalid private key');
}
importedKey = data;
}
return {
type,
algorithm: {
name: 'ECDH',
namedCurve: 'P-256',
},
extractable: true,
usages: type === 'private' ? ['deriveKey'] : [],
_key: importedKey,
};
}
async function encryptContent(content, sharedSecret) {
const serialized = JSON.stringify(content, (_, value) => {
if (!(value instanceof Error))
return value;
const error = value;
return Object.assign(Object.assign({}, (error.code ? { code: error.code } : {})), { message: error.message });
});
return encrypt(sharedSecret, serialized);
}
async function decryptContent(encryptedData, sharedSecret) {
return JSON.parse(await decrypt(sharedSecret, encryptedData));
}
function encodeLength(length) {
if (length < 128) {
return new Uint8Array([length]);
}
const lengthBytes = [];
while (length > 0) {
lengthBytes.unshift(length & 0xff);
length >>= 8;
}
return new Uint8Array([0x80 | lengthBytes.length, ...lengthBytes]);
}
function encodeDER(tag, value) {
const length = encodeLength(value.length);
return new Uint8Array([tag, ...length, ...value]);
}
function encodeSEQUENCE(...children) {
const totalLength = children.reduce((sum, child) => sum + child.length, 0);
const length = encodeLength(totalLength);
return new Uint8Array([0x30, ...length, ...children.flatMap((child) => Array.from(child))]);
}
function encodeOID(oid) {
const parts = oid.split('.').map(Number);
const firstByte = 40 * parts[0] + parts[1];
const restBytes = parts.slice(2).flatMap((part) => {
const bytes = [];
do {
bytes.unshift(part & 0x7f);
part >>= 7;
} while (part > 0);
bytes[0] |= 0x80;
bytes[bytes.length - 1] &= 0x7f;
return bytes;
});
return encodeDER(0x06, new Uint8Array([firstByte, ...restBytes]));
}
function encodeBITSTRING(data) {
return encodeDER(0x03, new Uint8Array([0x00, ...data]));
}
function encodeECPublicKey(publicKey) {
const algorithmIdentifier = encodeSEQUENCE(encodeOID('1.2.840.10045.2.1'), // ecPublicKey OID
encodeOID('1.2.840.10045.3.1.7') // P-256 curve OID
);
return encodeSEQUENCE(algorithmIdentifier, encodeBITSTRING(publicKey));
}
function decodeLength(data, startIndex) {
if (data[startIndex] < 128) {
return [data[startIndex], 1];
}
const octets = data[startIndex] & 0x7f;
let length = 0;
for (let i = 0; i < octets; i++) {
length = (length << 8) | data[startIndex + 1 + i];
}
return [length, 1 + octets];
}
function decodeDER(data, startIndex) {
const tag = data[startIndex];
const [length, lengthSize] = decodeLength(data, startIndex + 1);
const contentStart = startIndex + 1 + lengthSize;
const content = data.slice(contentStart, contentStart + length);
return [tag, content, 1 + lengthSize + length];
}