From 365ddf745fe21e166ae1404a9d82612c79814a27 Mon Sep 17 00:00:00 2001 From: microshine Date: Wed, 22 May 2019 21:26:25 +0300 Subject: [PATCH] Support RSA-OAEP --- src/mechs/rsa/crypto.ts | 8 +- src/mechs/rsa/rsa_oaep.ts | 151 +++++++++++++++++++++++++++++++++++++- src/mechs/sha/crypto.ts | 20 +++++ src/subtle.ts | 2 +- test/rsa.ts | 5 +- 5 files changed, 174 insertions(+), 12 deletions(-) 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..b49e0c7 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,114 @@ 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]) { + console.log(labelHash, dataBlock); + 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 +143,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 +154,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",