Support RSA-OAEP
This commit is contained in:
parent
61fc0c7394
commit
365ddf745f
|
@ -144,10 +144,10 @@ export class RsaCrypto {
|
||||||
|
|
||||||
const key = new RsaPrivateKey();
|
const key = new RsaPrivateKey();
|
||||||
key.data = Buffer.from(AsnSerializer.serialize(keyInfo));
|
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 = Object.assign({}, algorithm) as RsaHashedKeyAlgorithm;
|
||||||
|
key.algorithm.publicExponent = new Uint8Array(asnKey.publicExponent);
|
||||||
|
key.algorithm.modulusLength = asnKey.modulus.byteLength << 3;
|
||||||
key.extractable = extractable;
|
key.extractable = extractable;
|
||||||
key.usages = keyUsages;
|
key.usages = keyUsages;
|
||||||
|
|
||||||
|
@ -162,10 +162,10 @@ export class RsaCrypto {
|
||||||
|
|
||||||
const key = new RsaPublicKey();
|
const key = new RsaPublicKey();
|
||||||
key.data = Buffer.from(AsnSerializer.serialize(keyInfo));
|
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 = Object.assign({}, algorithm) as RsaHashedKeyAlgorithm;
|
||||||
|
key.algorithm.publicExponent = new Uint8Array(asnKey.publicExponent);
|
||||||
|
key.algorithm.modulusLength = asnKey.modulus.byteLength << 3;
|
||||||
key.extractable = extractable;
|
key.extractable = extractable;
|
||||||
key.usages = keyUsages;
|
key.usages = keyUsages;
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,18 @@
|
||||||
|
import crypto from "crypto";
|
||||||
import * as core from "webcrypto-core";
|
import * as core from "webcrypto-core";
|
||||||
import { CryptoKey } from "../../keys";
|
import { CryptoKey } from "../../keys";
|
||||||
|
import { ShaCrypto } from "../sha/crypto";
|
||||||
import { RsaCrypto } from "./crypto";
|
import { RsaCrypto } from "./crypto";
|
||||||
import { RsaPrivateKey } from "./private_key";
|
import { RsaPrivateKey } from "./private_key";
|
||||||
import { RsaPublicKey } from "./public_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 {
|
export class RsaOaepProvider extends core.RsaOaepProvider {
|
||||||
|
|
||||||
public async onGenerateKey(algorithm: RsaHashedKeyGenParams, extractable: boolean, keyUsages: KeyUsage[]): Promise<CryptoKeyPair> {
|
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> {
|
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> {
|
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> {
|
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> {
|
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;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,26 @@ import crypto from "crypto";
|
||||||
|
|
||||||
export class ShaCrypto {
|
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) {
|
public static digest(algorithm: Algorithm, data: ArrayBuffer) {
|
||||||
const hash = crypto.createHash(algorithm.name.replace("-", ""))
|
const hash = crypto.createHash(algorithm.name.replace("-", ""))
|
||||||
.update(Buffer.from(data)).digest();
|
.update(Buffer.from(data)).digest();
|
||||||
|
|
|
@ -28,7 +28,7 @@ export class SubtleCrypto extends core.SubtleCrypto {
|
||||||
//#region RSA
|
//#region RSA
|
||||||
this.providers.set(new RsaSsaProvider());
|
this.providers.set(new RsaSsaProvider());
|
||||||
this.providers.set(new RsaPssProvider());
|
this.providers.set(new RsaPssProvider());
|
||||||
// this.providers.set(new RsaOaepProvider()); // TODO: Fix encrypt/decrypt
|
this.providers.set(new RsaOaepProvider());
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region EC
|
//#region EC
|
||||||
|
|
|
@ -197,7 +197,6 @@ context("RSA", () => {
|
||||||
generateKey: ["SHA-1", "SHA-256", "SHA-384", "SHA-512"].map((hash) => {
|
generateKey: ["SHA-1", "SHA-256", "SHA-384", "SHA-512"].map((hash) => {
|
||||||
return {
|
return {
|
||||||
name: hash,
|
name: hash,
|
||||||
skip: true,
|
|
||||||
algorithm: {
|
algorithm: {
|
||||||
name: "RSA-OAEP",
|
name: "RSA-OAEP",
|
||||||
hash,
|
hash,
|
||||||
|
@ -211,7 +210,6 @@ context("RSA", () => {
|
||||||
encrypt: [
|
encrypt: [
|
||||||
{
|
{
|
||||||
name: "with label",
|
name: "with label",
|
||||||
skip: true,
|
|
||||||
algorithm: {
|
algorithm: {
|
||||||
name: "RSA-OAEP",
|
name: "RSA-OAEP",
|
||||||
label: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]),
|
label: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]),
|
||||||
|
@ -257,12 +255,11 @@ context("RSA", () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "without label",
|
name: "without label",
|
||||||
skip: true,
|
|
||||||
algorithm: {
|
algorithm: {
|
||||||
name: "RSA-OAEP",
|
name: "RSA-OAEP",
|
||||||
} as RsaOaepParams,
|
} as RsaOaepParams,
|
||||||
data: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]),
|
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: {
|
key: {
|
||||||
publicKey: {
|
publicKey: {
|
||||||
format: "jwk",
|
format: "jwk",
|
||||||
|
|
Reference in New Issue