Support RSA-OAEP

This commit is contained in:
microshine 2019-05-22 21:26:25 +03:00
parent 61fc0c7394
commit 365ddf745f
5 changed files with 174 additions and 12 deletions

View File

@ -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;

View File

@ -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<CryptoKeyPair> {
@ -19,11 +28,114 @@ export class RsaOaepProvider extends core.RsaOaepProvider {
}
public async onEncrypt(algorithm: RsaOaepParams, key: RsaPublicKey, data: ArrayBuffer): Promise<ArrayBuffer> {
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<ArrayBuffer> {
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<JsonWebKey | ArrayBuffer> {
@ -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<CryptoKey> {
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;
}
}

View File

@ -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();

View File

@ -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

View File

@ -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",