Skip to content

Commit

Permalink
Use WebCrypto for RSA and fallback to JS if not available.
Browse files Browse the repository at this point in the history
  • Loading branch information
invisal committed Aug 16, 2020
1 parent 9c06d5d commit 149cb22
Show file tree
Hide file tree
Showing 9 changed files with 242 additions and 98 deletions.
22 changes: 10 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ A pure Javascript/Typescript cryptography implementation for Deno. We will try t
|    `AES-ECB` ||| ✔️ |
| **RSA** | | | |
|    `RSA-PKCS1 v1.5` ||| ✔️ |
|    `RSA-OAEP` | || ✔️ |
|    `RSA-OAEP` | ✔️ || ✔️ |

More algorithm supports is one the way

Expand Down Expand Up @@ -56,17 +56,15 @@ new AES(key, {
import { RSA } from "https://deno.land/x/god_crypto/mod.ts";

const publicKey = RSA.parseKey(Deno.readTextFileSync("./public.pem"));
RSA.encrypt("Hello World", publicKey); // Default OAEP SHA1
RSA.encrypt("Hello World", publicKey, { padding: "oaep", hash: "sha256" });
RSA.encrypt("Hello World", publicKey, { padding: "pkcs1" });
const ciper = await new RSA(publicKey).encrypt("Hello World");
console.log(ciper.base64());

const privateKey = RSA.parseKey(Deno.readTextFileSync("./private.pem"));
RSA.decrypt(ciperText, privateKey);
```

---

## References
const plain = await new RSA(privateKey).decrypt(ciper);
console.log(plain.toString());

- [Announcing the Advanced Encryption Standard (AES)](https://csrc.nist.gov/csrc/media/publications/fips/197/final/documents/fips-197.pdf)
- [Public-Key Cryptography Standards (PKCS) #1: RSA Cryptography Specifications Version 2.1](https://tools.ietf.org/html/rfc3447)
// More examples:
new RSA(publicKey);
new RSA(publicKey, { padding: "oaep", hash: "sha256" });
new RSA(publicKey, { padding: "pkcs1" });
```
2 changes: 0 additions & 2 deletions src/aes/aes_wc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ export class WebCryptoAES implements AESBase {
const key = await this.loadKey();
const option = { name: "AES-CBC", iv: this.config.iv };

console.log(this.config.iv);

// @ts-ignore
const data = await crypto.subtle.encrypt(option, key, m);
return new Uint8Array(data);
Expand Down
16 changes: 16 additions & 0 deletions src/rsa/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export interface RSAKey {
n: bigint;
e?: bigint;
d?: bigint;
p?: bigint;
q?: bigint;
dp?: bigint;
dq?: bigint;
qi?: bigint;
length: number;
}

export interface RSAOption {
hash: "sha1" | "sha256";
padding: "oaep" | "pkcs1";
}
93 changes: 26 additions & 67 deletions src/rsa/mod.ts
Original file line number Diff line number Diff line change
@@ -1,87 +1,41 @@
import {
rsa_oaep_encrypt,
rsa_pkcs1_encrypt,
rsa_oaep_decrypt,
rsa_pkcs1_decrypt,
} from "./rsa.ts";
import { RSAKey, RSAOption } from "./common.ts";
import { ber_decode, ber_simple } from "./basic_encoding_rule.ts";
import { base64_to_binary, get_key_size, str2bytes } from "./../helper.ts";
import { RawBinary } from "./../binary.ts";
import { RSABase } from "./rsa_base.ts";
import { WebCryptoRSA } from "./rsa_wc.ts";
import { PureRSA } from "./rsa_js.ts";
import { RawBinary } from "../binary.ts";

interface RSAKey {
n: bigint;
e?: bigint;
d?: bigint;
length: number;
}
type RSAPublicKeyFormat = [[string, null], [[bigint, bigint]]];

interface RSAOption {
hash: "sha1" | "sha256";
padding: "oaep" | "pkcs1";
function computeMessage(m: Uint8Array | string) {
return typeof m === "string" ? new TextEncoder().encode(m) : m;
}

type RSAPublicKeyFormat = [[string, null], [[bigint, bigint]]];

export class RSA {
static encrypt(
message: Uint8Array | string,
key: RSAKey,
options?: Partial<RSAOption>,
): Uint8Array {
if (!key.e) throw "Invalid RSA key";
options: RSAOption;
lib: RSABase;

const computedOptions: RSAOption = {
constructor(key: RSAKey, options?: Partial<RSAOption>) {
this.options = {
hash: "sha1",
padding: "oaep",
...options,
};
const computedMessage = typeof message === "string"
? str2bytes(message)
: message;

if (computedOptions.padding === "oaep") {
return new RawBinary(rsa_oaep_encrypt(
key.length,
key.n,
key.e,
computedMessage,
computedOptions.hash,
));
} else if (computedOptions.padding === "pkcs1") {
return new RawBinary(
rsa_pkcs1_encrypt(key.length, key.n, key.e, computedMessage),
);
if (crypto.subtle && this.options.padding === "oaep") {
this.lib = new WebCryptoRSA(key, this.options);
} else {
this.lib = new PureRSA(key, this.options);
}

throw "Invalid parameters";
}

static decrypt(
ciper: Uint8Array,
key: RSAKey,
options?: Partial<RSAOption>,
): Uint8Array {
if (!key.d) throw "Invalid RSA key";

const computedOptions: RSAOption = {
hash: "sha1",
padding: "oaep",
...options,
};

if (computedOptions.padding === "oaep") {
return new RawBinary(rsa_oaep_decrypt(
key.length,
key.n,
key.d,
ciper,
computedOptions.hash,
));
} else if (computedOptions.padding === "pkcs1") {
return new RawBinary(rsa_pkcs1_decrypt(key.length, key.n, key.d, ciper));
}
async encrypt(m: Uint8Array | string) {
return new RawBinary(await this.lib.encrypt(computeMessage(m)));
}

throw "Invalid parameters";
async decrypt(m: Uint8Array | string) {
return new RawBinary(await this.lib.decrypt(computeMessage(m)));
}

static parseKey(key: string): RSAKey {
Expand All @@ -95,6 +49,11 @@ export class RSA {
n: parseKey[1],
d: parseKey[3],
e: parseKey[2],
p: parseKey[4],
q: parseKey[5],
dp: parseKey[6],
dq: parseKey[7],
qi: parseKey[8],
length: get_key_size(parseKey[1]),
};
} else if (key.indexOf("-----BEGIN PUBLIC KEY-----") === 0) {
Expand Down
4 changes: 4 additions & 0 deletions src/rsa/rsa_base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export abstract class RSABase {
public abstract async encrypt(m: Uint8Array): Promise<Uint8Array>;
public abstract async decrypt(m: Uint8Array): Promise<Uint8Array>;
}
File renamed without changes.
59 changes: 59 additions & 0 deletions src/rsa/rsa_js.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {
rsa_oaep_encrypt,
rsa_pkcs1_encrypt,
rsa_oaep_decrypt,
rsa_pkcs1_decrypt,
} from "./rsa_internal.ts";
import { RawBinary } from "./../binary.ts";
import { RSAKey, RSAOption } from "./common.ts";
import { RSABase } from "./rsa_base.ts";

export class PureRSA implements RSABase {
key: RSAKey;
options: RSAOption;

constructor(key: RSAKey, options: RSAOption) {
this.key = key;
this.options = options;
}

async encrypt(message: Uint8Array) {
if (!this.key.e) throw "Invalid RSA key";

if (this.options.padding === "oaep") {
return new RawBinary(rsa_oaep_encrypt(
this.key.length,
this.key.n,
this.key.e,
message,
this.options.hash,
));
} else if (this.options.padding === "pkcs1") {
return new RawBinary(
rsa_pkcs1_encrypt(this.key.length, this.key.n, this.key.e, message),
);
}

throw "Invalid parameters";
}

async decrypt(ciper: Uint8Array) {
if (!this.key.d) throw "Invalid RSA key";

if (this.options.padding === "oaep") {
return new RawBinary(rsa_oaep_decrypt(
this.key.length,
this.key.n,
this.key.d,
ciper,
this.options.hash,
));
} else if (this.options.padding === "pkcs1") {
return new RawBinary(
rsa_pkcs1_decrypt(this.key.length, this.key.n, this.key.d, ciper),
);
}

throw "Invalid parameters";
}
}
114 changes: 114 additions & 0 deletions src/rsa/rsa_wc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { RSABase } from "./rsa_base.ts";
import { RSAKey, RSAOption } from "./common.ts";

function big_base64(m: bigint) {
const bytes = [];

while (m > 0n) {
bytes.push(Number(m & 255n));
m = m >> 8n;
}

bytes.reverse();
let a = btoa(String.fromCharCode.apply(null, bytes)).replace(/=/g, "");
a = a.replace(/\+/g, "-");
a = a.replace(/\//g, "_");
return a;
}

export class WebCryptoRSA implements RSABase {
key: RSAKey;
options: RSAOption;
encryptedKey: any = null;
decryptedKey: any = null;

constructor(key: RSAKey, options: RSAOption) {
this.key = key;
this.options = options;
}

protected getHashFunctionName() {
if (this.options.hash === "sha1") return "SHA-1";
if (this.options.hash === "sha256") return "SHA-256";
return "";
}

protected async loadKeyForDecrypt() {
if (!this.key.e) return null;
if (!this.key.d) return null;

if (this.decryptedKey === null) {
const jwk = {
kty: "RSA",
n: big_base64(this.key.n),
d: big_base64(this.key.d),
e: big_base64(this.key.e),
p: this.key.p ? big_base64(this.key.p) : undefined,
q: this.key.q ? big_base64(this.key.q) : undefined,
dp: this.key.dp ? big_base64(this.key.dp) : undefined,
dq: this.key.dq ? big_base64(this.key.dq) : undefined,
qi: this.key.qi ? big_base64(this.key.qi) : undefined,
ext: true,
};

// @ts-ignore
this.decryptedKey = await crypto.subtle.importKey(
"jwk",
jwk,
{
name: "RSA-OAEP",
hash: { name: this.getHashFunctionName() },
},
false,
["decrypt"],
);
}

return this.decryptedKey;
}

protected async loadKeyForEncrypt() {
if (!this.key.e) return null;

if (this.encryptedKey === null) {
const jwk = {
kty: "RSA",
e: big_base64(this.key.e),
n: big_base64(this.key.n),
ext: true,
};

// @ts-ignore
this.encryptedKey = await crypto.subtle.importKey(
"jwk",
jwk,
{
name: "RSA-OAEP",
hash: { name: this.getHashFunctionName() },
},
false,
["encrypt"],
);
}

return this.encryptedKey;
}

async encrypt(m: Uint8Array) {
// @ts-ignore
return await crypto.subtle.encrypt(
{ name: "RSA-OAEP" },
await this.loadKeyForEncrypt(),
m,
);
}

async decrypt(m: Uint8Array) {
// @ts-ignore
return await crypto.subtle.decrypt(
{ name: "RSA-OAEP" },
await this.loadKeyForDecrypt(),
m,
);
}
}
Loading

0 comments on commit 149cb22

Please sign in to comment.