diff --git a/package-lock.json b/package-lock.json index 764f667..91aabc2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -166,9 +166,10 @@ "dev": true }, "@types/node": { - "version": "10.14.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.6.tgz", - "integrity": "sha512-Fvm24+u85lGmV4hT5G++aht2C5I4Z4dYlWZIh62FAfFO/TfzXtPpoLI6I7AuBWkIFqZCnhFOoTT7RjjaIL5Fjg==" + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.2.tgz", + "integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==", + "dev": true }, "acorn": { "version": "6.1.1", @@ -2169,7 +2170,7 @@ "dependencies": { "glob": { "version": "7.1.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "dev": true, "requires": { @@ -2485,6 +2486,13 @@ "requires": { "@types/node": "^10.12.15", "tslib": "^1.9.3" + }, + "dependencies": { + "@types/node": { + "version": "10.14.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.7.tgz", + "integrity": "sha512-on4MmIDgHXiuJDELPk1NFaKVUxxCFr37tm8E9yN6rAiF5Pzp/9bBfBHkoexqRiY+hk/Z04EJU9kKEb59YqJ82A==" + } } }, "qs": { diff --git a/package.json b/package.json index da28f45..282c14b 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "banner": "// Copyright (c) 2019, Peculiar Ventures, All rights reserved.", "devDependencies": { "@types/mocha": "^5.2.6", - "@types/node": "^10.14.6", + "@types/node": "^12.0.2", "coveralls": "^3.0.3", "mocha": "^6.1.4", "nyc": "^14.1.0", diff --git a/src/mechs/rsa/crypto.ts b/src/mechs/rsa/crypto.ts index 897fe7f..137118c 100644 --- a/src/mechs/rsa/crypto.ts +++ b/src/mechs/rsa/crypto.ts @@ -144,10 +144,10 @@ export class RsaCrypto { const key = new RsaPrivateKey(); key.data = Buffer.from(AsnSerializer.serialize(keyInfo)); - key.algorithm.publicExponent = new Uint8Array(asnKey.publicExponent); - key.algorithm.modulusLength = asnKey.modulus.byteLength << 3; key.algorithm = Object.assign({}, algorithm) as RsaHashedKeyAlgorithm; + key.algorithm.publicExponent = new Uint8Array(asnKey.publicExponent); + key.algorithm.modulusLength = asnKey.modulus.byteLength << 3; key.extractable = extractable; key.usages = keyUsages; @@ -162,10 +162,10 @@ export class RsaCrypto { const key = new RsaPublicKey(); key.data = Buffer.from(AsnSerializer.serialize(keyInfo)); - key.algorithm.publicExponent = new Uint8Array(asnKey.publicExponent); - key.algorithm.modulusLength = asnKey.modulus.byteLength << 3; key.algorithm = Object.assign({}, algorithm) as RsaHashedKeyAlgorithm; + key.algorithm.publicExponent = new Uint8Array(asnKey.publicExponent); + key.algorithm.modulusLength = asnKey.modulus.byteLength << 3; key.extractable = extractable; key.usages = keyUsages; diff --git a/src/mechs/rsa/rsa_oaep.ts b/src/mechs/rsa/rsa_oaep.ts index 2c1c29f..99c58d0 100644 --- a/src/mechs/rsa/rsa_oaep.ts +++ b/src/mechs/rsa/rsa_oaep.ts @@ -1,9 +1,18 @@ +import crypto from "crypto"; import * as core from "webcrypto-core"; import { CryptoKey } from "../../keys"; +import { ShaCrypto } from "../sha/crypto"; import { RsaCrypto } from "./crypto"; import { RsaPrivateKey } from "./private_key"; import { RsaPublicKey } from "./public_key"; +/** + * Source code for decrypt, encrypt, mgf1 functions is from asmcrypto module + * https://github.com/asmcrypto/asmcrypto.js/blob/master/src/rsa/pkcs1.ts + * + * This code can be removed after https://github.com/nodejs/help/issues/1726 fixed + */ + export class RsaOaepProvider extends core.RsaOaepProvider { public async onGenerateKey(algorithm: RsaHashedKeyGenParams, extractable: boolean, keyUsages: KeyUsage[]): Promise { @@ -19,11 +28,113 @@ export class RsaOaepProvider extends core.RsaOaepProvider { } public async onEncrypt(algorithm: RsaOaepParams, key: RsaPublicKey, data: ArrayBuffer): Promise { - return RsaCrypto.encrypt(algorithm, key, new Uint8Array(data)); + const dataView = new Uint8Array(data); + const keySize = Math.ceil(key.algorithm.modulusLength >> 3); + const hashSize = ShaCrypto.size(key.algorithm.hash) >> 3; + const dataLength = dataView.byteLength; + const psLength = keySize - dataLength - 2 * hashSize - 2; + + if (dataLength > keySize - 2 * hashSize - 2) { + throw new Error("Data too large"); + } + + const message = new Uint8Array(keySize); + const seed = message.subarray(1, hashSize + 1); + const dataBlock = message.subarray(hashSize + 1); + + dataBlock.set(dataView, hashSize + psLength + 1); + + const labelHash = crypto.createHash(key.algorithm.hash.name.replace("-", "")) + .update(core.BufferSourceConverter.toUint8Array(algorithm.label || new Uint8Array(0))) + .digest(); + dataBlock.set(labelHash, 0); + dataBlock[hashSize + psLength] = 1; + + crypto.randomFillSync(seed); + + const dataBlockMask = this.mgf1(key.algorithm.hash, seed, dataBlock.length); + for (let i = 0; i < dataBlock.length; i++) { + dataBlock[i] ^= dataBlockMask[i]; + } + + const seedMask = this.mgf1(key.algorithm.hash, dataBlock, seed.length); + for (let i = 0; i < seed.length; i++) { + seed[i] ^= seedMask[i]; + } + + if (!key.pem) { + key.pem = `-----BEGIN PUBLIC KEY-----\n${key.data.toString("base64")}\n-----END PUBLIC KEY-----`; + } + + const pkcs0 = crypto.publicEncrypt({ + key: key.pem, + padding: crypto.constants.RSA_NO_PADDING, + }, Buffer.from(message)); + + return new Uint8Array(pkcs0).buffer; } public async onDecrypt(algorithm: RsaOaepParams, key: RsaPrivateKey, data: ArrayBuffer): Promise { - return RsaCrypto.decrypt(algorithm, key, new Uint8Array(data)); + const keySize = Math.ceil(key.algorithm.modulusLength >> 3); + const hashSize = ShaCrypto.size(key.algorithm.hash) >> 3; + const dataLength = data.byteLength; + + if (dataLength !== keySize) { + throw new Error("Bad data"); + } + + if (!key.pem) { + key.pem = `-----BEGIN PRIVATE KEY-----\n${key.data.toString("base64")}\n-----END PRIVATE KEY-----`; + } + + let pkcs0 = crypto.privateDecrypt({ + key: key.pem, + padding: crypto.constants.RSA_NO_PADDING, + }, Buffer.from(data)); + const z = pkcs0[0]; + const seed = pkcs0.subarray(1, hashSize + 1); + const dataBlock = pkcs0.subarray(hashSize + 1); + + if (z !== 0) { + throw new Error("Decryption failed"); + } + + const seedMask = this.mgf1(key.algorithm.hash, dataBlock, seed.length); + for (let i = 0; i < seed.length; i++) { + seed[i] ^= seedMask[i]; + } + + const dataBlockMask = this.mgf1(key.algorithm.hash, seed, dataBlock.length); + for (let i = 0; i < dataBlock.length; i++) { + dataBlock[i] ^= dataBlockMask[i]; + } + + const labelHash = crypto.createHash(key.algorithm.hash.name.replace("-", "")) + .update(core.BufferSourceConverter.toUint8Array(algorithm.label || new Uint8Array(0))) + .digest(); + for (let i = 0; i < hashSize; i++) { + if (labelHash[i] !== dataBlock[i]) { + throw new Error("Decryption failed"); + } + } + + let psEnd = hashSize; + for (; psEnd < dataBlock.length; psEnd++) { + const psz = dataBlock[psEnd]; + if (psz === 1) { + break; + } + if (psz !== 0) { + throw new Error("Decryption failed"); + } + } + if (psEnd === dataBlock.length) { + throw new Error("Decryption failed"); + } + + pkcs0 = dataBlock.subarray(psEnd + 1); + + return new Uint8Array(pkcs0).buffer; } public async onExportKey(format: KeyFormat, key: CryptoKey): Promise { @@ -31,7 +142,7 @@ export class RsaOaepProvider extends core.RsaOaepProvider { } public async onImportKey(format: KeyFormat, keyData: JsonWebKey | ArrayBuffer, algorithm: RsaHashedImportParams, extractable: boolean, keyUsages: KeyUsage[]): Promise { - const key = await RsaCrypto.importKey(format, keyData, {...algorithm, name: this.name}, extractable, keyUsages); + const key = await RsaCrypto.importKey(format, keyData, { ...algorithm, name: this.name }, extractable, keyUsages); return key; } @@ -42,4 +153,37 @@ export class RsaOaepProvider extends core.RsaOaepProvider { } } + /** + * RSA MGF1 + * @param algorithm Hash algorithm + * @param seed + * @param length + */ + protected mgf1(algorithm: Algorithm, seed: Uint8Array, length: number = 0) { + const hashSize = ShaCrypto.size(algorithm) >> 3; + const mask = new Uint8Array(length); + const counter = new Uint8Array(4); + const chunks = Math.ceil(length / hashSize); + for (let i = 0; i < chunks; i++) { + counter[0] = i >>> 24; + counter[1] = (i >>> 16) & 255; + counter[2] = (i >>> 8) & 255; + counter[3] = i & 255; + + const submask = mask.subarray(i * hashSize); + + let chunk = crypto.createHash(algorithm.name.replace("-", "")) + .update(seed) + .update(counter) + .digest() as Uint8Array; + if (chunk.length > submask.length) { + chunk = chunk.subarray(0, submask.length); + } + + submask.set(chunk); + } + + return mask; + } + } diff --git a/src/mechs/sha/crypto.ts b/src/mechs/sha/crypto.ts index 19ec115..e0bb1be 100644 --- a/src/mechs/sha/crypto.ts +++ b/src/mechs/sha/crypto.ts @@ -2,6 +2,26 @@ import crypto from "crypto"; export class ShaCrypto { + /** + * Returns size of the hash algorithm in bits + * @param algorithm Hash algorithm + * @throws Throws Error if an unrecognized name + */ + public static size(algorithm: Algorithm) { + switch (algorithm.name.toUpperCase()) { + case "SHA-1": + return 160; + case "SHA-256": + return 256; + case "SHA-384": + return 384; + case "SHA-512": + return 512; + default: + throw new Error("Unrecognized name"); + } + } + public static digest(algorithm: Algorithm, data: ArrayBuffer) { const hash = crypto.createHash(algorithm.name.replace("-", "")) .update(Buffer.from(data)).digest(); diff --git a/src/subtle.ts b/src/subtle.ts index 60ce49f..28f317e 100644 --- a/src/subtle.ts +++ b/src/subtle.ts @@ -28,7 +28,7 @@ export class SubtleCrypto extends core.SubtleCrypto { //#region RSA this.providers.set(new RsaSsaProvider()); this.providers.set(new RsaPssProvider()); - // this.providers.set(new RsaOaepProvider()); // TODO: Fix encrypt/decrypt + this.providers.set(new RsaOaepProvider()); //#endregion //#region EC diff --git a/test/rsa.ts b/test/rsa.ts index 5ed7d35..b09c857 100644 --- a/test/rsa.ts +++ b/test/rsa.ts @@ -197,7 +197,6 @@ context("RSA", () => { generateKey: ["SHA-1", "SHA-256", "SHA-384", "SHA-512"].map((hash) => { return { name: hash, - skip: true, algorithm: { name: "RSA-OAEP", hash, @@ -211,7 +210,6 @@ context("RSA", () => { encrypt: [ { name: "with label", - skip: true, algorithm: { name: "RSA-OAEP", label: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]), @@ -257,12 +255,11 @@ context("RSA", () => { }, { name: "without label", - skip: true, algorithm: { name: "RSA-OAEP", } as RsaOaepParams, data: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]), - encData: Convert.FromBase64("aHu8PBZuctYecfINKgUdB8gBoLyUUFxTZDTzTHUk9KKxtYywYml48HoijBG5DyaIWUUbOIdPgap9C8pFG2iYShQnE9Aj3gzKLHacBbFw1P79+Ei/Tm0j/THiXqCplBZC4dIp4jhTDepmdrlXZcY0slmjG+h8h8TpSmWKP3pEGGk="), + encData: Convert.FromBase64("NcsyyVE/y4Z1K5bWGElWAkvlN+jWpfgPtcytlydWUUz4RqFeW5w6KA1cQMHy3eNh920YXDjsLSYHe6Dz1CEqjIKkHS9HBuOhLA39yUArOu/fmn1lMnwb9N9roTxHDxpgY3y98DXEVkAKU4Py0rlzJLVazDV/+1YcbzFLCSKUNaI="), key: { publicKey: { format: "jwk",