Merge branch 'crypto-cleanup-fallback' of https://github.com/pdlan/noVNC
This commit is contained in:
commit
a0e6e7b1d8
|
@ -0,0 +1,178 @@
|
||||||
|
export class AESECBCipher {
|
||||||
|
constructor() {
|
||||||
|
this._key = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get algorithm() {
|
||||||
|
return { name: "AES-ECB" };
|
||||||
|
}
|
||||||
|
|
||||||
|
static async importKey(key, _algorithm, extractable, keyUsages) {
|
||||||
|
const cipher = new AESECBCipher;
|
||||||
|
await cipher._importKey(key, extractable, keyUsages);
|
||||||
|
return cipher;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _importKey(key, extractable, keyUsages) {
|
||||||
|
this._key = await window.crypto.subtle.importKey(
|
||||||
|
"raw", key, {name: "AES-CBC"}, extractable, keyUsages);
|
||||||
|
}
|
||||||
|
|
||||||
|
async encrypt(_algorithm, plaintext) {
|
||||||
|
const x = new Uint8Array(plaintext);
|
||||||
|
if (x.length % 16 !== 0 || this._key === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const n = x.length / 16;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const y = new Uint8Array(await window.crypto.subtle.encrypt({
|
||||||
|
name: "AES-CBC",
|
||||||
|
iv: new Uint8Array(16),
|
||||||
|
}, this._key, x.slice(i * 16, i * 16 + 16))).slice(0, 16);
|
||||||
|
x.set(y, i * 16);
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AESEAXCipher {
|
||||||
|
constructor() {
|
||||||
|
this._rawKey = null;
|
||||||
|
this._ctrKey = null;
|
||||||
|
this._cbcKey = null;
|
||||||
|
this._zeroBlock = new Uint8Array(16);
|
||||||
|
this._prefixBlock0 = this._zeroBlock;
|
||||||
|
this._prefixBlock1 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
|
||||||
|
this._prefixBlock2 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
get algorithm() {
|
||||||
|
return { name: "AES-EAX" };
|
||||||
|
}
|
||||||
|
|
||||||
|
async _encryptBlock(block) {
|
||||||
|
const encrypted = await window.crypto.subtle.encrypt({
|
||||||
|
name: "AES-CBC",
|
||||||
|
iv: this._zeroBlock,
|
||||||
|
}, this._cbcKey, block);
|
||||||
|
return new Uint8Array(encrypted).slice(0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _initCMAC() {
|
||||||
|
const k1 = await this._encryptBlock(this._zeroBlock);
|
||||||
|
const k2 = new Uint8Array(16);
|
||||||
|
const v = k1[0] >>> 6;
|
||||||
|
for (let i = 0; i < 15; i++) {
|
||||||
|
k2[i] = (k1[i + 1] >> 6) | (k1[i] << 2);
|
||||||
|
k1[i] = (k1[i + 1] >> 7) | (k1[i] << 1);
|
||||||
|
}
|
||||||
|
const lut = [0x0, 0x87, 0x0e, 0x89];
|
||||||
|
k2[14] ^= v >>> 1;
|
||||||
|
k2[15] = (k1[15] << 2) ^ lut[v];
|
||||||
|
k1[15] = (k1[15] << 1) ^ lut[v >> 1];
|
||||||
|
this._k1 = k1;
|
||||||
|
this._k2 = k2;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _encryptCTR(data, counter) {
|
||||||
|
const encrypted = await window.crypto.subtle.encrypt({
|
||||||
|
name: "AES-CTR",
|
||||||
|
counter: counter,
|
||||||
|
length: 128
|
||||||
|
}, this._ctrKey, data);
|
||||||
|
return new Uint8Array(encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _decryptCTR(data, counter) {
|
||||||
|
const decrypted = await window.crypto.subtle.decrypt({
|
||||||
|
name: "AES-CTR",
|
||||||
|
counter: counter,
|
||||||
|
length: 128
|
||||||
|
}, this._ctrKey, data);
|
||||||
|
return new Uint8Array(decrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _computeCMAC(data, prefixBlock) {
|
||||||
|
if (prefixBlock.length !== 16) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const n = Math.floor(data.length / 16);
|
||||||
|
const m = Math.ceil(data.length / 16);
|
||||||
|
const r = data.length - n * 16;
|
||||||
|
const cbcData = new Uint8Array((m + 1) * 16);
|
||||||
|
cbcData.set(prefixBlock);
|
||||||
|
cbcData.set(data, 16);
|
||||||
|
if (r === 0) {
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
cbcData[n * 16 + i] ^= this._k1[i];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cbcData[(n + 1) * 16 + r] = 0x80;
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
cbcData[(n + 1) * 16 + i] ^= this._k2[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let cbcEncrypted = await window.crypto.subtle.encrypt({
|
||||||
|
name: "AES-CBC",
|
||||||
|
iv: this._zeroBlock,
|
||||||
|
}, this._cbcKey, cbcData);
|
||||||
|
|
||||||
|
cbcEncrypted = new Uint8Array(cbcEncrypted);
|
||||||
|
const mac = cbcEncrypted.slice(cbcEncrypted.length - 32, cbcEncrypted.length - 16);
|
||||||
|
return mac;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async importKey(key, _algorithm, _extractable, _keyUsages) {
|
||||||
|
const cipher = new AESEAXCipher;
|
||||||
|
await cipher._importKey(key);
|
||||||
|
return cipher;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _importKey(key) {
|
||||||
|
this._rawKey = key;
|
||||||
|
this._ctrKey = await window.crypto.subtle.importKey(
|
||||||
|
"raw", key, {name: "AES-CTR"}, false, ["encrypt", "decrypt"]);
|
||||||
|
this._cbcKey = await window.crypto.subtle.importKey(
|
||||||
|
"raw", key, {name: "AES-CBC"}, false, ["encrypt"]);
|
||||||
|
await this._initCMAC();
|
||||||
|
}
|
||||||
|
|
||||||
|
async encrypt(algorithm, message) {
|
||||||
|
const ad = algorithm.additionalData;
|
||||||
|
const nonce = algorithm.iv;
|
||||||
|
const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0);
|
||||||
|
const encrypted = await this._encryptCTR(message, nCMAC);
|
||||||
|
const adCMAC = await this._computeCMAC(ad, this._prefixBlock1);
|
||||||
|
const mac = await this._computeCMAC(encrypted, this._prefixBlock2);
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
mac[i] ^= nCMAC[i] ^ adCMAC[i];
|
||||||
|
}
|
||||||
|
const res = new Uint8Array(16 + encrypted.length);
|
||||||
|
res.set(encrypted);
|
||||||
|
res.set(mac, encrypted.length);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
async decrypt(algorithm, data) {
|
||||||
|
const encrypted = data.slice(0, data.length - 16);
|
||||||
|
const ad = algorithm.additionalData;
|
||||||
|
const nonce = algorithm.iv;
|
||||||
|
const mac = data.slice(data.length - 16);
|
||||||
|
const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0);
|
||||||
|
const adCMAC = await this._computeCMAC(ad, this._prefixBlock1);
|
||||||
|
const computedMac = await this._computeCMAC(encrypted, this._prefixBlock2);
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
computedMac[i] ^= nCMAC[i] ^ adCMAC[i];
|
||||||
|
}
|
||||||
|
if (computedMac.length !== mac.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < mac.length; i++) {
|
||||||
|
if (computedMac[i] !== mac[i]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const res = await this._decryptCTR(encrypted, nCMAC);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
export function modPow(b, e, m) {
|
||||||
|
let r = 1n;
|
||||||
|
b = b % m;
|
||||||
|
while (e > 0n) {
|
||||||
|
if ((e & 1n) === 1n) {
|
||||||
|
r = (r * b) % m;
|
||||||
|
}
|
||||||
|
e = e >> 1n;
|
||||||
|
b = (b * b) % m;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bigIntToU8Array(bigint, padLength=0) {
|
||||||
|
let hex = bigint.toString(16);
|
||||||
|
if (padLength === 0) {
|
||||||
|
padLength = Math.ceil(hex.length / 2);
|
||||||
|
}
|
||||||
|
hex = hex.padStart(padLength * 2, '0');
|
||||||
|
const length = hex.length / 2;
|
||||||
|
const arr = new Uint8Array(length);
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
arr[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function u8ArrayToBigInt(arr) {
|
||||||
|
let hex = '0x';
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
hex += arr[i].toString(16).padStart(2, '0');
|
||||||
|
}
|
||||||
|
return BigInt(hex);
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { AESECBCipher, AESEAXCipher } from "./aes.js";
|
||||||
|
import { DESCBCCipher, DESECBCipher } from "./des.js";
|
||||||
|
import { RSACipher } from "./rsa.js";
|
||||||
|
import { DHCipher } from "./dh.js";
|
||||||
|
import { MD5 } from "./md5.js";
|
||||||
|
|
||||||
|
// A single interface for the cryptographic algorithms not supported by SubtleCrypto.
|
||||||
|
// Both synchronous and asynchronous implmentations are allowed.
|
||||||
|
class LegacyCrypto {
|
||||||
|
constructor() {
|
||||||
|
this._algorithms = {
|
||||||
|
"AES-ECB": AESECBCipher,
|
||||||
|
"AES-EAX": AESEAXCipher,
|
||||||
|
"DES-ECB": DESECBCipher,
|
||||||
|
"DES-CBC": DESCBCCipher,
|
||||||
|
"RSA-PKCS1-v1_5": RSACipher,
|
||||||
|
"DH": DHCipher,
|
||||||
|
"MD5": MD5,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
encrypt(algorithm, key, data) {
|
||||||
|
if (key.algorithm.name !== algorithm.name) {
|
||||||
|
throw new Error("algorithm does not match");
|
||||||
|
}
|
||||||
|
if (typeof key.encrypt !== "function") {
|
||||||
|
throw new Error("key does not support encryption");
|
||||||
|
}
|
||||||
|
return key.encrypt(algorithm, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
decrypt(algorithm, key, data) {
|
||||||
|
if (key.algorithm.name !== algorithm.name) {
|
||||||
|
throw new Error("algorithm does not match");
|
||||||
|
}
|
||||||
|
if (typeof key.decrypt !== "function") {
|
||||||
|
throw new Error("key does not support encryption");
|
||||||
|
}
|
||||||
|
return key.decrypt(algorithm, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
importKey(format, keyData, algorithm, extractable, keyUsages) {
|
||||||
|
if (format !== "raw") {
|
||||||
|
throw new Error("key format is not supported");
|
||||||
|
}
|
||||||
|
const alg = this._algorithms[algorithm.name];
|
||||||
|
if (typeof alg === "undefined" || typeof alg.importKey !== "function") {
|
||||||
|
throw new Error("algorithm is not supported");
|
||||||
|
}
|
||||||
|
return alg.importKey(keyData, algorithm, extractable, keyUsages);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateKey(algorithm, extractable, keyUsages) {
|
||||||
|
const alg = this._algorithms[algorithm.name];
|
||||||
|
if (typeof alg === "undefined" || typeof alg.generateKey !== "function") {
|
||||||
|
throw new Error("algorithm is not supported");
|
||||||
|
}
|
||||||
|
return alg.generateKey(algorithm, extractable, keyUsages);
|
||||||
|
}
|
||||||
|
|
||||||
|
exportKey(format, key) {
|
||||||
|
if (format !== "raw") {
|
||||||
|
throw new Error("key format is not supported");
|
||||||
|
}
|
||||||
|
if (typeof key.exportKey !== "function") {
|
||||||
|
throw new Error("key does not support exportKey");
|
||||||
|
}
|
||||||
|
return key.exportKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
digest(algorithm, data) {
|
||||||
|
const alg = this._algorithms[algorithm];
|
||||||
|
if (typeof alg !== "function") {
|
||||||
|
throw new Error("algorithm is not supported");
|
||||||
|
}
|
||||||
|
return alg(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
deriveBits(algorithm, key, length) {
|
||||||
|
if (key.algorithm.name !== algorithm.name) {
|
||||||
|
throw new Error("algorithm does not match");
|
||||||
|
}
|
||||||
|
if (typeof key.deriveBits !== "function") {
|
||||||
|
throw new Error("key does not support deriveBits");
|
||||||
|
}
|
||||||
|
return key.deriveBits(algorithm, length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new LegacyCrypto;
|
|
@ -128,7 +128,7 @@ const SP8 = [b|f,z|e,a|z,c|f,b|z,b|f,z|d,b|z,a|d,c|z,c|f,a|e,c|e,a|f,z|e,z|d,
|
||||||
|
|
||||||
/* eslint-enable comma-spacing */
|
/* eslint-enable comma-spacing */
|
||||||
|
|
||||||
export default class DES {
|
class DES {
|
||||||
constructor(password) {
|
constructor(password) {
|
||||||
this.keys = [];
|
this.keys = [];
|
||||||
|
|
||||||
|
@ -258,9 +258,73 @@ export default class DES {
|
||||||
}
|
}
|
||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Encrypt 16 bytes of text using passwd as key
|
export class DESECBCipher {
|
||||||
encrypt(t) {
|
constructor() {
|
||||||
return this.enc8(t.slice(0, 8)).concat(this.enc8(t.slice(8, 16)));
|
this._cipher = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get algorithm() {
|
||||||
|
return { name: "DES-ECB" };
|
||||||
|
}
|
||||||
|
|
||||||
|
static importKey(key, _algorithm, _extractable, _keyUsages) {
|
||||||
|
const cipher = new DESECBCipher;
|
||||||
|
cipher._importKey(key);
|
||||||
|
return cipher;
|
||||||
|
}
|
||||||
|
|
||||||
|
_importKey(key, _extractable, _keyUsages) {
|
||||||
|
this._cipher = new DES(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
encrypt(_algorithm, plaintext) {
|
||||||
|
const x = new Uint8Array(plaintext);
|
||||||
|
if (x.length % 8 !== 0 || this._cipher === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const n = x.length / 8;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
x.set(this._cipher.enc8(x.slice(i * 8, i * 8 + 8)), i * 8);
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DESCBCCipher {
|
||||||
|
constructor() {
|
||||||
|
this._cipher = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get algorithm() {
|
||||||
|
return { name: "DES-CBC" };
|
||||||
|
}
|
||||||
|
|
||||||
|
static importKey(key, _algorithm, _extractable, _keyUsages) {
|
||||||
|
const cipher = new DESCBCCipher;
|
||||||
|
cipher._importKey(key);
|
||||||
|
return cipher;
|
||||||
|
}
|
||||||
|
|
||||||
|
_importKey(key) {
|
||||||
|
this._cipher = new DES(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
encrypt(algorithm, plaintext) {
|
||||||
|
const x = new Uint8Array(plaintext);
|
||||||
|
let y = new Uint8Array(algorithm.iv);
|
||||||
|
if (x.length % 8 !== 0 || this._cipher === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const n = x.length / 8;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
for (let j = 0; j < 8; j++) {
|
||||||
|
y[j] ^= plaintext[i * 8 + j];
|
||||||
|
}
|
||||||
|
y = this._cipher.enc8(y);
|
||||||
|
x.set(y, i * 8);
|
||||||
|
}
|
||||||
|
return x;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { modPow, bigIntToU8Array, u8ArrayToBigInt } from "./bigint.js";
|
||||||
|
|
||||||
|
class DHPublicKey {
|
||||||
|
constructor(key) {
|
||||||
|
this._key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
get algorithm() {
|
||||||
|
return { name: "DH" };
|
||||||
|
}
|
||||||
|
|
||||||
|
exportKey() {
|
||||||
|
return this._key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DHCipher {
|
||||||
|
constructor() {
|
||||||
|
this._g = null;
|
||||||
|
this._p = null;
|
||||||
|
this._gBigInt = null;
|
||||||
|
this._pBigInt = null;
|
||||||
|
this._privateKey = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get algorithm() {
|
||||||
|
return { name: "DH" };
|
||||||
|
}
|
||||||
|
|
||||||
|
static generateKey(algorithm, _extractable) {
|
||||||
|
const cipher = new DHCipher;
|
||||||
|
cipher._generateKey(algorithm);
|
||||||
|
return { privateKey: cipher, publicKey: new DHPublicKey(cipher._publicKey) };
|
||||||
|
}
|
||||||
|
|
||||||
|
_generateKey(algorithm) {
|
||||||
|
const g = algorithm.g;
|
||||||
|
const p = algorithm.p;
|
||||||
|
this._keyBytes = p.length;
|
||||||
|
this._gBigInt = u8ArrayToBigInt(g);
|
||||||
|
this._pBigInt = u8ArrayToBigInt(p);
|
||||||
|
this._privateKey = window.crypto.getRandomValues(new Uint8Array(this._keyBytes));
|
||||||
|
this._privateKeyBigInt = u8ArrayToBigInt(this._privateKey);
|
||||||
|
this._publicKey = bigIntToU8Array(modPow(
|
||||||
|
this._gBigInt, this._privateKeyBigInt, this._pBigInt), this._keyBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
deriveBits(algorithm, length) {
|
||||||
|
const bytes = Math.ceil(length / 8);
|
||||||
|
const pkey = new Uint8Array(algorithm.public);
|
||||||
|
const len = bytes > this._keyBytes ? bytes : this._keyBytes;
|
||||||
|
const secret = modPow(u8ArrayToBigInt(pkey), this._privateKeyBigInt, this._pBigInt);
|
||||||
|
return bigIntToU8Array(secret, len).slice(0, len);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,12 +7,15 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Performs MD5 hashing on a string of binary characters, returns an array of bytes
|
* Performs MD5 hashing on an array of bytes, returns an array of bytes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function MD5(d) {
|
export async function MD5(d) {
|
||||||
let r = M(V(Y(X(d), 8 * d.length)));
|
let s = "";
|
||||||
return r;
|
for (let i = 0; i < d.length; i++) {
|
||||||
|
s += String.fromCharCode(d[i]);
|
||||||
|
}
|
||||||
|
return M(V(Y(X(s), 8 * s.length)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function M(d) {
|
function M(d) {
|
|
@ -0,0 +1,132 @@
|
||||||
|
import Base64 from "../base64.js";
|
||||||
|
import { modPow, bigIntToU8Array, u8ArrayToBigInt } from "./bigint.js";
|
||||||
|
|
||||||
|
export class RSACipher {
|
||||||
|
constructor() {
|
||||||
|
this._keyLength = 0;
|
||||||
|
this._keyBytes = 0;
|
||||||
|
this._n = null;
|
||||||
|
this._e = null;
|
||||||
|
this._d = null;
|
||||||
|
this._nBigInt = null;
|
||||||
|
this._eBigInt = null;
|
||||||
|
this._dBigInt = null;
|
||||||
|
this._extractable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get algorithm() {
|
||||||
|
return { name: "RSA-PKCS1-v1_5" };
|
||||||
|
}
|
||||||
|
|
||||||
|
_base64urlDecode(data) {
|
||||||
|
data = data.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
data = data.padEnd(Math.ceil(data.length / 4) * 4, "=");
|
||||||
|
return Base64.decode(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
_padArray(arr, length) {
|
||||||
|
const res = new Uint8Array(length);
|
||||||
|
res.set(arr, length - arr.length);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async generateKey(algorithm, extractable, _keyUsages) {
|
||||||
|
const cipher = new RSACipher;
|
||||||
|
await cipher._generateKey(algorithm, extractable);
|
||||||
|
return { privateKey: cipher };
|
||||||
|
}
|
||||||
|
|
||||||
|
async _generateKey(algorithm, extractable) {
|
||||||
|
this._keyLength = algorithm.modulusLength;
|
||||||
|
this._keyBytes = Math.ceil(this._keyLength / 8);
|
||||||
|
const key = await window.crypto.subtle.generateKey(
|
||||||
|
{
|
||||||
|
name: "RSA-OAEP",
|
||||||
|
modulusLength: algorithm.modulusLength,
|
||||||
|
publicExponent: algorithm.publicExponent,
|
||||||
|
hash: {name: "SHA-256"},
|
||||||
|
},
|
||||||
|
true, ["encrypt", "decrypt"]);
|
||||||
|
const privateKey = await window.crypto.subtle.exportKey("jwk", key.privateKey);
|
||||||
|
this._n = this._padArray(this._base64urlDecode(privateKey.n), this._keyBytes);
|
||||||
|
this._nBigInt = u8ArrayToBigInt(this._n);
|
||||||
|
this._e = this._padArray(this._base64urlDecode(privateKey.e), this._keyBytes);
|
||||||
|
this._eBigInt = u8ArrayToBigInt(this._e);
|
||||||
|
this._d = this._padArray(this._base64urlDecode(privateKey.d), this._keyBytes);
|
||||||
|
this._dBigInt = u8ArrayToBigInt(this._d);
|
||||||
|
this._extractable = extractable;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async importKey(key, _algorithm, extractable, keyUsages) {
|
||||||
|
if (keyUsages.length !== 1 || keyUsages[0] !== "encrypt") {
|
||||||
|
throw new Error("only support importing RSA public key");
|
||||||
|
}
|
||||||
|
const cipher = new RSACipher;
|
||||||
|
await cipher._importKey(key, extractable);
|
||||||
|
return cipher;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _importKey(key, extractable) {
|
||||||
|
const n = key.n;
|
||||||
|
const e = key.e;
|
||||||
|
if (n.length !== e.length) {
|
||||||
|
throw new Error("the sizes of modulus and public exponent do not match");
|
||||||
|
}
|
||||||
|
this._keyBytes = n.length;
|
||||||
|
this._keyLength = this._keyBytes * 8;
|
||||||
|
this._n = new Uint8Array(this._keyBytes);
|
||||||
|
this._e = new Uint8Array(this._keyBytes);
|
||||||
|
this._n.set(n);
|
||||||
|
this._e.set(e);
|
||||||
|
this._nBigInt = u8ArrayToBigInt(this._n);
|
||||||
|
this._eBigInt = u8ArrayToBigInt(this._e);
|
||||||
|
this._extractable = extractable;
|
||||||
|
}
|
||||||
|
|
||||||
|
async encrypt(_algorithm, message) {
|
||||||
|
if (message.length > this._keyBytes - 11) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const ps = new Uint8Array(this._keyBytes - message.length - 3);
|
||||||
|
window.crypto.getRandomValues(ps);
|
||||||
|
for (let i = 0; i < ps.length; i++) {
|
||||||
|
ps[i] = Math.floor(ps[i] * 254 / 255 + 1);
|
||||||
|
}
|
||||||
|
const em = new Uint8Array(this._keyBytes);
|
||||||
|
em[1] = 0x02;
|
||||||
|
em.set(ps, 2);
|
||||||
|
em.set(message, ps.length + 3);
|
||||||
|
const emBigInt = u8ArrayToBigInt(em);
|
||||||
|
const c = modPow(emBigInt, this._eBigInt, this._nBigInt);
|
||||||
|
return bigIntToU8Array(c, this._keyBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
async decrypt(_algorithm, message) {
|
||||||
|
if (message.length !== this._keyBytes) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const msgBigInt = u8ArrayToBigInt(message);
|
||||||
|
const emBigInt = modPow(msgBigInt, this._dBigInt, this._nBigInt);
|
||||||
|
const em = bigIntToU8Array(emBigInt, this._keyBytes);
|
||||||
|
if (em[0] !== 0x00 || em[1] !== 0x02) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let i = 2;
|
||||||
|
for (; i < em.length; i++) {
|
||||||
|
if (em[i] === 0x00) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i === em.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return em.slice(i + 1, em.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
async exportKey() {
|
||||||
|
if (!this._extractable) {
|
||||||
|
throw new Error("key is not extractable");
|
||||||
|
}
|
||||||
|
return { n: this._n, e: this._e, d: this._d };
|
||||||
|
}
|
||||||
|
}
|
325
core/ra2.js
325
core/ra2.js
|
@ -1,146 +1,25 @@
|
||||||
import Base64 from './base64.js';
|
|
||||||
import { encodeUTF8 } from './util/strings.js';
|
import { encodeUTF8 } from './util/strings.js';
|
||||||
import EventTargetMixin from './util/eventtarget.js';
|
import EventTargetMixin from './util/eventtarget.js';
|
||||||
|
import legacyCrypto from './crypto/crypto.js';
|
||||||
|
|
||||||
export class AESEAXCipher {
|
class RA2Cipher {
|
||||||
constructor() {
|
constructor() {
|
||||||
this._rawKey = null;
|
this._cipher = null;
|
||||||
this._ctrKey = null;
|
|
||||||
this._cbcKey = null;
|
|
||||||
this._zeroBlock = new Uint8Array(16);
|
|
||||||
this._prefixBlock0 = this._zeroBlock;
|
|
||||||
this._prefixBlock1 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
|
|
||||||
this._prefixBlock2 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async _encryptBlock(block) {
|
|
||||||
const encrypted = await window.crypto.subtle.encrypt({
|
|
||||||
name: "AES-CBC",
|
|
||||||
iv: this._zeroBlock,
|
|
||||||
}, this._cbcKey, block);
|
|
||||||
return new Uint8Array(encrypted).slice(0, 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
async _initCMAC() {
|
|
||||||
const k1 = await this._encryptBlock(this._zeroBlock);
|
|
||||||
const k2 = new Uint8Array(16);
|
|
||||||
const v = k1[0] >>> 6;
|
|
||||||
for (let i = 0; i < 15; i++) {
|
|
||||||
k2[i] = (k1[i + 1] >> 6) | (k1[i] << 2);
|
|
||||||
k1[i] = (k1[i + 1] >> 7) | (k1[i] << 1);
|
|
||||||
}
|
|
||||||
const lut = [0x0, 0x87, 0x0e, 0x89];
|
|
||||||
k2[14] ^= v >>> 1;
|
|
||||||
k2[15] = (k1[15] << 2) ^ lut[v];
|
|
||||||
k1[15] = (k1[15] << 1) ^ lut[v >> 1];
|
|
||||||
this._k1 = k1;
|
|
||||||
this._k2 = k2;
|
|
||||||
}
|
|
||||||
|
|
||||||
async _encryptCTR(data, counter) {
|
|
||||||
const encrypted = await window.crypto.subtle.encrypt({
|
|
||||||
"name": "AES-CTR",
|
|
||||||
counter: counter,
|
|
||||||
length: 128
|
|
||||||
}, this._ctrKey, data);
|
|
||||||
return new Uint8Array(encrypted);
|
|
||||||
}
|
|
||||||
|
|
||||||
async _decryptCTR(data, counter) {
|
|
||||||
const decrypted = await window.crypto.subtle.decrypt({
|
|
||||||
"name": "AES-CTR",
|
|
||||||
counter: counter,
|
|
||||||
length: 128
|
|
||||||
}, this._ctrKey, data);
|
|
||||||
return new Uint8Array(decrypted);
|
|
||||||
}
|
|
||||||
|
|
||||||
async _computeCMAC(data, prefixBlock) {
|
|
||||||
if (prefixBlock.length !== 16) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const n = Math.floor(data.length / 16);
|
|
||||||
const m = Math.ceil(data.length / 16);
|
|
||||||
const r = data.length - n * 16;
|
|
||||||
const cbcData = new Uint8Array((m + 1) * 16);
|
|
||||||
cbcData.set(prefixBlock);
|
|
||||||
cbcData.set(data, 16);
|
|
||||||
if (r === 0) {
|
|
||||||
for (let i = 0; i < 16; i++) {
|
|
||||||
cbcData[n * 16 + i] ^= this._k1[i];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cbcData[(n + 1) * 16 + r] = 0x80;
|
|
||||||
for (let i = 0; i < 16; i++) {
|
|
||||||
cbcData[(n + 1) * 16 + i] ^= this._k2[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let cbcEncrypted = await window.crypto.subtle.encrypt({
|
|
||||||
name: "AES-CBC",
|
|
||||||
iv: this._zeroBlock,
|
|
||||||
}, this._cbcKey, cbcData);
|
|
||||||
|
|
||||||
cbcEncrypted = new Uint8Array(cbcEncrypted);
|
|
||||||
const mac = cbcEncrypted.slice(cbcEncrypted.length - 32, cbcEncrypted.length - 16);
|
|
||||||
return mac;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setKey(key) {
|
|
||||||
this._rawKey = key;
|
|
||||||
this._ctrKey = await window.crypto.subtle.importKey(
|
|
||||||
"raw", key, {"name": "AES-CTR"}, false, ["encrypt", "decrypt"]);
|
|
||||||
this._cbcKey = await window.crypto.subtle.importKey(
|
|
||||||
"raw", key, {"name": "AES-CBC"}, false, ["encrypt", "decrypt"]);
|
|
||||||
await this._initCMAC();
|
|
||||||
}
|
|
||||||
|
|
||||||
async encrypt(message, associatedData, nonce) {
|
|
||||||
const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0);
|
|
||||||
const encrypted = await this._encryptCTR(message, nCMAC);
|
|
||||||
const adCMAC = await this._computeCMAC(associatedData, this._prefixBlock1);
|
|
||||||
const mac = await this._computeCMAC(encrypted, this._prefixBlock2);
|
|
||||||
for (let i = 0; i < 16; i++) {
|
|
||||||
mac[i] ^= nCMAC[i] ^ adCMAC[i];
|
|
||||||
}
|
|
||||||
const res = new Uint8Array(16 + encrypted.length);
|
|
||||||
res.set(encrypted);
|
|
||||||
res.set(mac, encrypted.length);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
async decrypt(encrypted, associatedData, nonce, mac) {
|
|
||||||
const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0);
|
|
||||||
const adCMAC = await this._computeCMAC(associatedData, this._prefixBlock1);
|
|
||||||
const computedMac = await this._computeCMAC(encrypted, this._prefixBlock2);
|
|
||||||
for (let i = 0; i < 16; i++) {
|
|
||||||
computedMac[i] ^= nCMAC[i] ^ adCMAC[i];
|
|
||||||
}
|
|
||||||
if (computedMac.length !== mac.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
for (let i = 0; i < mac.length; i++) {
|
|
||||||
if (computedMac[i] !== mac[i]) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const res = await this._decryptCTR(encrypted, nCMAC);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RA2Cipher {
|
|
||||||
constructor() {
|
|
||||||
this._cipher = new AESEAXCipher();
|
|
||||||
this._counter = new Uint8Array(16);
|
this._counter = new Uint8Array(16);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setKey(key) {
|
async setKey(key) {
|
||||||
await this._cipher.setKey(key);
|
this._cipher = await legacyCrypto.importKey(
|
||||||
|
"raw", key, { name: "AES-EAX" }, false, ["encrypt, decrypt"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async makeMessage(message) {
|
async makeMessage(message) {
|
||||||
const ad = new Uint8Array([(message.length & 0xff00) >>> 8, message.length & 0xff]);
|
const ad = new Uint8Array([(message.length & 0xff00) >>> 8, message.length & 0xff]);
|
||||||
const encrypted = await this._cipher.encrypt(message, ad, this._counter);
|
const encrypted = await legacyCrypto.encrypt({
|
||||||
|
name: "AES-EAX",
|
||||||
|
iv: this._counter,
|
||||||
|
additionalData: ad,
|
||||||
|
}, this._cipher, message);
|
||||||
for (let i = 0; i < 16 && this._counter[i]++ === 255; i++);
|
for (let i = 0; i < 16 && this._counter[i]++ === 255; i++);
|
||||||
const res = new Uint8Array(message.length + 2 + 16);
|
const res = new Uint8Array(message.length + 2 + 16);
|
||||||
res.set(ad);
|
res.set(ad);
|
||||||
|
@ -148,164 +27,18 @@ export class RA2Cipher {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
async receiveMessage(length, encrypted, mac) {
|
async receiveMessage(length, encrypted) {
|
||||||
const ad = new Uint8Array([(length & 0xff00) >>> 8, length & 0xff]);
|
const ad = new Uint8Array([(length & 0xff00) >>> 8, length & 0xff]);
|
||||||
const res = await this._cipher.decrypt(encrypted, ad, this._counter, mac);
|
const res = await legacyCrypto.decrypt({
|
||||||
|
name: "AES-EAX",
|
||||||
|
iv: this._counter,
|
||||||
|
additionalData: ad,
|
||||||
|
}, this._cipher, encrypted);
|
||||||
for (let i = 0; i < 16 && this._counter[i]++ === 255; i++);
|
for (let i = 0; i < 16 && this._counter[i]++ === 255; i++);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RSACipher {
|
|
||||||
constructor(keyLength) {
|
|
||||||
this._key = null;
|
|
||||||
this._keyLength = keyLength;
|
|
||||||
this._keyBytes = Math.ceil(keyLength / 8);
|
|
||||||
this._n = null;
|
|
||||||
this._e = null;
|
|
||||||
this._d = null;
|
|
||||||
this._nBigInt = null;
|
|
||||||
this._eBigInt = null;
|
|
||||||
this._dBigInt = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_base64urlDecode(data) {
|
|
||||||
data = data.replace(/-/g, "+").replace(/_/g, "/");
|
|
||||||
data = data.padEnd(Math.ceil(data.length / 4) * 4, "=");
|
|
||||||
return Base64.decode(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
_u8ArrayToBigInt(arr) {
|
|
||||||
let hex = '0x';
|
|
||||||
for (let i = 0; i < arr.length; i++) {
|
|
||||||
hex += arr[i].toString(16).padStart(2, '0');
|
|
||||||
}
|
|
||||||
return BigInt(hex);
|
|
||||||
}
|
|
||||||
|
|
||||||
_padArray(arr, length) {
|
|
||||||
const res = new Uint8Array(length);
|
|
||||||
res.set(arr, length - arr.length);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
_bigIntToU8Array(bigint, padLength=0) {
|
|
||||||
let hex = bigint.toString(16);
|
|
||||||
if (padLength === 0) {
|
|
||||||
padLength = Math.ceil(hex.length / 2) * 2;
|
|
||||||
}
|
|
||||||
hex = hex.padStart(padLength * 2, '0');
|
|
||||||
const length = hex.length / 2;
|
|
||||||
const arr = new Uint8Array(length);
|
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
arr[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
||||||
}
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
_modPow(b, e, m) {
|
|
||||||
if (m === 1n) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
let r = 1n;
|
|
||||||
b = b % m;
|
|
||||||
while (e > 0) {
|
|
||||||
if (e % 2n === 1n) {
|
|
||||||
r = (r * b) % m;
|
|
||||||
}
|
|
||||||
e = e / 2n;
|
|
||||||
b = (b * b) % m;
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
async generateKey() {
|
|
||||||
this._key = await window.crypto.subtle.generateKey(
|
|
||||||
{
|
|
||||||
name: "RSA-OAEP",
|
|
||||||
modulusLength: this._keyLength,
|
|
||||||
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
|
|
||||||
hash: {name: "SHA-256"},
|
|
||||||
},
|
|
||||||
true, ["encrypt", "decrypt"]);
|
|
||||||
const privateKey = await window.crypto.subtle.exportKey("jwk", this._key.privateKey);
|
|
||||||
this._n = this._padArray(this._base64urlDecode(privateKey.n), this._keyBytes);
|
|
||||||
this._nBigInt = this._u8ArrayToBigInt(this._n);
|
|
||||||
this._e = this._padArray(this._base64urlDecode(privateKey.e), this._keyBytes);
|
|
||||||
this._eBigInt = this._u8ArrayToBigInt(this._e);
|
|
||||||
this._d = this._padArray(this._base64urlDecode(privateKey.d), this._keyBytes);
|
|
||||||
this._dBigInt = this._u8ArrayToBigInt(this._d);
|
|
||||||
}
|
|
||||||
|
|
||||||
setPublicKey(n, e) {
|
|
||||||
if (n.length !== this._keyBytes || e.length !== this._keyBytes) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._n = new Uint8Array(this._keyBytes);
|
|
||||||
this._e = new Uint8Array(this._keyBytes);
|
|
||||||
this._n.set(n);
|
|
||||||
this._e.set(e);
|
|
||||||
this._nBigInt = this._u8ArrayToBigInt(this._n);
|
|
||||||
this._eBigInt = this._u8ArrayToBigInt(this._e);
|
|
||||||
}
|
|
||||||
|
|
||||||
encrypt(message) {
|
|
||||||
if (message.length > this._keyBytes - 11) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const ps = new Uint8Array(this._keyBytes - message.length - 3);
|
|
||||||
window.crypto.getRandomValues(ps);
|
|
||||||
for (let i = 0; i < ps.length; i++) {
|
|
||||||
ps[i] = Math.floor(ps[i] * 254 / 255 + 1);
|
|
||||||
}
|
|
||||||
const em = new Uint8Array(this._keyBytes);
|
|
||||||
em[1] = 0x02;
|
|
||||||
em.set(ps, 2);
|
|
||||||
em.set(message, ps.length + 3);
|
|
||||||
const emBigInt = this._u8ArrayToBigInt(em);
|
|
||||||
const c = this._modPow(emBigInt, this._eBigInt, this._nBigInt);
|
|
||||||
return this._bigIntToU8Array(c, this._keyBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
decrypt(message) {
|
|
||||||
if (message.length !== this._keyBytes) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const msgBigInt = this._u8ArrayToBigInt(message);
|
|
||||||
const emBigInt = this._modPow(msgBigInt, this._dBigInt, this._nBigInt);
|
|
||||||
const em = this._bigIntToU8Array(emBigInt, this._keyBytes);
|
|
||||||
if (em[0] !== 0x00 || em[1] !== 0x02) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
let i = 2;
|
|
||||||
for (; i < em.length; i++) {
|
|
||||||
if (em[i] === 0x00) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (i === em.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return em.slice(i + 1, em.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
get keyLength() {
|
|
||||||
return this._keyLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
get n() {
|
|
||||||
return this._n;
|
|
||||||
}
|
|
||||||
|
|
||||||
get e() {
|
|
||||||
return this._e;
|
|
||||||
}
|
|
||||||
|
|
||||||
get d() {
|
|
||||||
return this._d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class RSAAESAuthenticationState extends EventTargetMixin {
|
export default class RSAAESAuthenticationState extends EventTargetMixin {
|
||||||
constructor(sock, getCredentials) {
|
constructor(sock, getCredentials) {
|
||||||
super();
|
super();
|
||||||
|
@ -417,8 +150,8 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
|
||||||
await this._waitSockAsync(serverKeyBytes * 2);
|
await this._waitSockAsync(serverKeyBytes * 2);
|
||||||
const serverN = this._sock.rQshiftBytes(serverKeyBytes);
|
const serverN = this._sock.rQshiftBytes(serverKeyBytes);
|
||||||
const serverE = this._sock.rQshiftBytes(serverKeyBytes);
|
const serverE = this._sock.rQshiftBytes(serverKeyBytes);
|
||||||
const serverRSACipher = new RSACipher(serverKeyLength);
|
const serverRSACipher = await legacyCrypto.importKey(
|
||||||
serverRSACipher.setPublicKey(serverN, serverE);
|
"raw", { n: serverN, e: serverE }, { name: "RSA-PKCS1-v1_5" }, false, ["encrypt"]);
|
||||||
const serverPublickey = new Uint8Array(4 + serverKeyBytes * 2);
|
const serverPublickey = new Uint8Array(4 + serverKeyBytes * 2);
|
||||||
serverPublickey.set(serverKeyLengthBuffer);
|
serverPublickey.set(serverKeyLengthBuffer);
|
||||||
serverPublickey.set(serverN, 4);
|
serverPublickey.set(serverN, 4);
|
||||||
|
@ -433,10 +166,14 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
|
||||||
// 2: Send client public key
|
// 2: Send client public key
|
||||||
const clientKeyLength = 2048;
|
const clientKeyLength = 2048;
|
||||||
const clientKeyBytes = Math.ceil(clientKeyLength / 8);
|
const clientKeyBytes = Math.ceil(clientKeyLength / 8);
|
||||||
const clientRSACipher = new RSACipher(clientKeyLength);
|
const clientRSACipher = (await legacyCrypto.generateKey({
|
||||||
await clientRSACipher.generateKey();
|
name: "RSA-PKCS1-v1_5",
|
||||||
const clientN = clientRSACipher.n;
|
modulusLength: clientKeyLength,
|
||||||
const clientE = clientRSACipher.e;
|
publicExponent: new Uint8Array([1, 0, 1]),
|
||||||
|
}, true, ["encrypt"])).privateKey;
|
||||||
|
const clientExportedRSAKey = await legacyCrypto.exportKey("raw", clientRSACipher);
|
||||||
|
const clientN = clientExportedRSAKey.n;
|
||||||
|
const clientE = clientExportedRSAKey.e;
|
||||||
const clientPublicKey = new Uint8Array(4 + clientKeyBytes * 2);
|
const clientPublicKey = new Uint8Array(4 + clientKeyBytes * 2);
|
||||||
clientPublicKey[0] = (clientKeyLength & 0xff000000) >>> 24;
|
clientPublicKey[0] = (clientKeyLength & 0xff000000) >>> 24;
|
||||||
clientPublicKey[1] = (clientKeyLength & 0xff0000) >>> 16;
|
clientPublicKey[1] = (clientKeyLength & 0xff0000) >>> 16;
|
||||||
|
@ -449,7 +186,8 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
|
||||||
// 3: Send client random
|
// 3: Send client random
|
||||||
const clientRandom = new Uint8Array(16);
|
const clientRandom = new Uint8Array(16);
|
||||||
window.crypto.getRandomValues(clientRandom);
|
window.crypto.getRandomValues(clientRandom);
|
||||||
const clientEncryptedRandom = serverRSACipher.encrypt(clientRandom);
|
const clientEncryptedRandom = await legacyCrypto.encrypt(
|
||||||
|
{ name: "RSA-PKCS1-v1_5" }, serverRSACipher, clientRandom);
|
||||||
const clientRandomMessage = new Uint8Array(2 + serverKeyBytes);
|
const clientRandomMessage = new Uint8Array(2 + serverKeyBytes);
|
||||||
clientRandomMessage[0] = (serverKeyBytes & 0xff00) >>> 8;
|
clientRandomMessage[0] = (serverKeyBytes & 0xff00) >>> 8;
|
||||||
clientRandomMessage[1] = serverKeyBytes & 0xff;
|
clientRandomMessage[1] = serverKeyBytes & 0xff;
|
||||||
|
@ -462,7 +200,8 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
|
||||||
throw new Error("RA2: wrong encrypted message length");
|
throw new Error("RA2: wrong encrypted message length");
|
||||||
}
|
}
|
||||||
const serverEncryptedRandom = this._sock.rQshiftBytes(clientKeyBytes);
|
const serverEncryptedRandom = this._sock.rQshiftBytes(clientKeyBytes);
|
||||||
const serverRandom = clientRSACipher.decrypt(serverEncryptedRandom);
|
const serverRandom = await legacyCrypto.decrypt(
|
||||||
|
{ name: "RSA-PKCS1-v1_5" }, clientRSACipher, serverEncryptedRandom);
|
||||||
if (serverRandom === null || serverRandom.length !== 16) {
|
if (serverRandom === null || serverRandom.length !== 16) {
|
||||||
throw new Error("RA2: corrupted server encrypted random");
|
throw new Error("RA2: corrupted server encrypted random");
|
||||||
}
|
}
|
||||||
|
@ -500,7 +239,7 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
|
||||||
throw new Error("RA2: wrong server hash");
|
throw new Error("RA2: wrong server hash");
|
||||||
}
|
}
|
||||||
const serverHashReceived = await serverCipher.receiveMessage(
|
const serverHashReceived = await serverCipher.receiveMessage(
|
||||||
20, this._sock.rQshiftBytes(20), this._sock.rQshiftBytes(16));
|
20, this._sock.rQshiftBytes(20 + 16));
|
||||||
if (serverHashReceived === null) {
|
if (serverHashReceived === null) {
|
||||||
throw new Error("RA2: failed to authenticate the message");
|
throw new Error("RA2: failed to authenticate the message");
|
||||||
}
|
}
|
||||||
|
@ -516,7 +255,7 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
|
||||||
throw new Error("RA2: wrong subtype");
|
throw new Error("RA2: wrong subtype");
|
||||||
}
|
}
|
||||||
let subtype = (await serverCipher.receiveMessage(
|
let subtype = (await serverCipher.receiveMessage(
|
||||||
1, this._sock.rQshiftBytes(1), this._sock.rQshiftBytes(16)));
|
1, this._sock.rQshiftBytes(1 + 16)));
|
||||||
if (subtype === null) {
|
if (subtype === null) {
|
||||||
throw new Error("RA2: failed to authenticate the message");
|
throw new Error("RA2: failed to authenticate the message");
|
||||||
}
|
}
|
||||||
|
|
122
core/rfb.js
122
core/rfb.js
|
@ -21,12 +21,11 @@ import Keyboard from "./input/keyboard.js";
|
||||||
import GestureHandler from "./input/gesturehandler.js";
|
import GestureHandler from "./input/gesturehandler.js";
|
||||||
import Cursor from "./util/cursor.js";
|
import Cursor from "./util/cursor.js";
|
||||||
import Websock from "./websock.js";
|
import Websock from "./websock.js";
|
||||||
import DES from "./des.js";
|
|
||||||
import KeyTable from "./input/keysym.js";
|
import KeyTable from "./input/keysym.js";
|
||||||
import XtScancode from "./input/xtscancodes.js";
|
import XtScancode from "./input/xtscancodes.js";
|
||||||
import { encodings } from "./encodings.js";
|
import { encodings } from "./encodings.js";
|
||||||
import RSAAESAuthenticationState from "./ra2.js";
|
import RSAAESAuthenticationState from "./ra2.js";
|
||||||
import { MD5 } from "./util/md5.js";
|
import legacyCrypto from "./crypto/crypto.js";
|
||||||
|
|
||||||
import RawDecoder from "./decoders/raw.js";
|
import RawDecoder from "./decoders/raw.js";
|
||||||
import CopyRectDecoder from "./decoders/copyrect.js";
|
import CopyRectDecoder from "./decoders/copyrect.js";
|
||||||
|
@ -1681,77 +1680,35 @@ export default class RFB extends EventTargetMixin {
|
||||||
let prime = this._sock.rQshiftBytes(keyLength); // predetermined prime modulus
|
let prime = this._sock.rQshiftBytes(keyLength); // predetermined prime modulus
|
||||||
let serverPublicKey = this._sock.rQshiftBytes(keyLength); // other party's public key
|
let serverPublicKey = this._sock.rQshiftBytes(keyLength); // other party's public key
|
||||||
|
|
||||||
let clientPrivateKey = window.crypto.getRandomValues(new Uint8Array(keyLength));
|
let clientKey = legacyCrypto.generateKey(
|
||||||
let padding = Array.from(window.crypto.getRandomValues(new Uint8Array(64)), byte => String.fromCharCode(65+byte%26)).join('');
|
{ name: "DH", g: generator, p: prime }, false, ["deriveBits"]);
|
||||||
|
this._negotiateARDAuthAsync(keyLength, serverPublicKey, clientKey);
|
||||||
this._negotiateARDAuthAsync(generator, keyLength, prime, serverPublicKey, clientPrivateKey, padding);
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_modPow(base, exponent, modulus) {
|
async _negotiateARDAuthAsync(keyLength, serverPublicKey, clientKey) {
|
||||||
|
const clientPublicKey = legacyCrypto.exportKey("raw", clientKey.publicKey);
|
||||||
|
const sharedKey = legacyCrypto.deriveBits(
|
||||||
|
{ name: "DH", public: serverPublicKey }, clientKey.privateKey, keyLength * 8);
|
||||||
|
|
||||||
let baseHex = "0x"+Array.from(base, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join('');
|
const username = encodeUTF8(this._rfbCredentials.username).substring(0, 63);
|
||||||
let exponentHex = "0x"+Array.from(exponent, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join('');
|
const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
|
||||||
let modulusHex = "0x"+Array.from(modulus, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join('');
|
|
||||||
|
|
||||||
let b = BigInt(baseHex);
|
const credentials = window.crypto.getRandomValues(new Uint8Array(128));
|
||||||
let e = BigInt(exponentHex);
|
for (let i = 0; i < username.length; i++) {
|
||||||
let m = BigInt(modulusHex);
|
credentials[i] = username.charCodeAt(i);
|
||||||
let r = 1n;
|
|
||||||
b = b % m;
|
|
||||||
while (e > 0) {
|
|
||||||
if (e % 2n === 1n) {
|
|
||||||
r = (r * b) % m;
|
|
||||||
}
|
}
|
||||||
e = e / 2n;
|
credentials[username.length] = 0;
|
||||||
b = (b * b) % m;
|
for (let i = 0; i < password.length; i++) {
|
||||||
|
credentials[64 + i] = password.charCodeAt(i);
|
||||||
}
|
}
|
||||||
let hexResult = r.toString(16);
|
credentials[64 + password.length] = 0;
|
||||||
|
|
||||||
while (hexResult.length/2<exponent.length || (hexResult.length%2 != 0)) {
|
const key = await legacyCrypto.digest("MD5", sharedKey);
|
||||||
hexResult = "0"+hexResult;
|
const cipher = await legacyCrypto.importKey(
|
||||||
}
|
"raw", key, { name: "AES-ECB" }, false, ["encrypt"]);
|
||||||
|
const encrypted = await legacyCrypto.encrypt({ name: "AES-ECB" }, cipher, credentials);
|
||||||
let bytesResult = [];
|
|
||||||
for (let c = 0; c < hexResult.length; c += 2) {
|
|
||||||
bytesResult.push(parseInt(hexResult.substr(c, 2), 16));
|
|
||||||
}
|
|
||||||
return bytesResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
async _aesEcbEncrypt(string, key) {
|
|
||||||
// perform AES-ECB blocks
|
|
||||||
let keyString = Array.from(key, byte => String.fromCharCode(byte)).join('');
|
|
||||||
let aesKey = await window.crypto.subtle.importKey("raw", MD5(keyString), {name: "AES-CBC"}, false, ["encrypt"]);
|
|
||||||
let data = new Uint8Array(string.length);
|
|
||||||
for (let i = 0; i < string.length; ++i) {
|
|
||||||
data[i] = string.charCodeAt(i);
|
|
||||||
}
|
|
||||||
let encrypted = new Uint8Array(data.length);
|
|
||||||
for (let i=0;i<data.length;i+=16) {
|
|
||||||
let block = data.slice(i, i+16);
|
|
||||||
let encryptedBlock = await window.crypto.subtle.encrypt({name: "AES-CBC", iv: block},
|
|
||||||
aesKey, new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
|
|
||||||
);
|
|
||||||
encrypted.set((new Uint8Array(encryptedBlock)).slice(0, 16), i);
|
|
||||||
}
|
|
||||||
return encrypted;
|
|
||||||
}
|
|
||||||
|
|
||||||
async _negotiateARDAuthAsync(generator, keyLength, prime, serverPublicKey, clientPrivateKey, padding) {
|
|
||||||
// calculate the DH keys
|
|
||||||
let clientPublicKey = this._modPow(generator, clientPrivateKey, prime);
|
|
||||||
let sharedKey = this._modPow(serverPublicKey, clientPrivateKey, prime);
|
|
||||||
|
|
||||||
let username = encodeUTF8(this._rfbCredentials.username).substring(0, 63);
|
|
||||||
let password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
|
|
||||||
|
|
||||||
let paddedUsername = username + '\0' + padding.substring(0, 63);
|
|
||||||
let paddedPassword = password + '\0' + padding.substring(0, 63);
|
|
||||||
let credentials = paddedUsername.substring(0, 64) + paddedPassword.substring(0, 64);
|
|
||||||
|
|
||||||
let encrypted = await this._aesEcbEncrypt(credentials, sharedKey);
|
|
||||||
|
|
||||||
this._rfbCredentials.ardCredentials = encrypted;
|
this._rfbCredentials.ardCredentials = encrypted;
|
||||||
this._rfbCredentials.ardPublicKey = clientPublicKey;
|
this._rfbCredentials.ardPublicKey = clientPublicKey;
|
||||||
|
@ -1905,7 +1862,8 @@ export default class RFB extends EventTargetMixin {
|
||||||
if (e.message !== "disconnect normally") {
|
if (e.message !== "disconnect normally") {
|
||||||
this._fail(e.message);
|
this._fail(e.message);
|
||||||
}
|
}
|
||||||
}).then(() => {
|
})
|
||||||
|
.then(() => {
|
||||||
this.dispatchEvent(new CustomEvent('securityresult'));
|
this.dispatchEvent(new CustomEvent('securityresult'));
|
||||||
this._rfbInitState = "SecurityResult";
|
this._rfbInitState = "SecurityResult";
|
||||||
return true;
|
return true;
|
||||||
|
@ -1934,15 +1892,15 @@ export default class RFB extends EventTargetMixin {
|
||||||
const g = this._sock.rQshiftBytes(8);
|
const g = this._sock.rQshiftBytes(8);
|
||||||
const p = this._sock.rQshiftBytes(8);
|
const p = this._sock.rQshiftBytes(8);
|
||||||
const A = this._sock.rQshiftBytes(8);
|
const A = this._sock.rQshiftBytes(8);
|
||||||
const b = window.crypto.getRandomValues(new Uint8Array(8));
|
const dhKey = legacyCrypto.generateKey({ name: "DH", g: g, p: p }, true, ["deriveBits"]);
|
||||||
const B = new Uint8Array(this._modPow(g, b, p));
|
const B = legacyCrypto.exportKey("raw", dhKey.publicKey);
|
||||||
const secret = new Uint8Array(this._modPow(A, b, p));
|
const secret = legacyCrypto.deriveBits({ name: "DH", public: A }, dhKey.privateKey, 64);
|
||||||
|
|
||||||
const des = new DES(secret);
|
const key = legacyCrypto.importKey("raw", secret, { name: "DES-CBC" }, false, ["encrypt"]);
|
||||||
const username = encodeUTF8(this._rfbCredentials.username).substring(0, 255);
|
const username = encodeUTF8(this._rfbCredentials.username).substring(0, 255);
|
||||||
const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
|
const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
|
||||||
const usernameBytes = new Uint8Array(256);
|
let usernameBytes = new Uint8Array(256);
|
||||||
const passwordBytes = new Uint8Array(64);
|
let passwordBytes = new Uint8Array(64);
|
||||||
window.crypto.getRandomValues(usernameBytes);
|
window.crypto.getRandomValues(usernameBytes);
|
||||||
window.crypto.getRandomValues(passwordBytes);
|
window.crypto.getRandomValues(passwordBytes);
|
||||||
for (let i = 0; i < username.length; i++) {
|
for (let i = 0; i < username.length; i++) {
|
||||||
|
@ -1953,22 +1911,8 @@ export default class RFB extends EventTargetMixin {
|
||||||
passwordBytes[i] = password.charCodeAt(i);
|
passwordBytes[i] = password.charCodeAt(i);
|
||||||
}
|
}
|
||||||
passwordBytes[password.length] = 0;
|
passwordBytes[password.length] = 0;
|
||||||
let x = new Uint8Array(secret);
|
usernameBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, usernameBytes);
|
||||||
for (let i = 0; i < 32; i++) {
|
passwordBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, passwordBytes);
|
||||||
for (let j = 0; j < 8; j++) {
|
|
||||||
x[j] ^= usernameBytes[i * 8 + j];
|
|
||||||
}
|
|
||||||
x = des.enc8(x);
|
|
||||||
usernameBytes.set(x, i * 8);
|
|
||||||
}
|
|
||||||
x = new Uint8Array(secret);
|
|
||||||
for (let i = 0; i < 8; i++) {
|
|
||||||
for (let j = 0; j < 8; j++) {
|
|
||||||
x[j] ^= passwordBytes[i * 8 + j];
|
|
||||||
}
|
|
||||||
x = des.enc8(x);
|
|
||||||
passwordBytes.set(x, i * 8);
|
|
||||||
}
|
|
||||||
this._sock.send(B);
|
this._sock.send(B);
|
||||||
this._sock.send(usernameBytes);
|
this._sock.send(usernameBytes);
|
||||||
this._sock.send(passwordBytes);
|
this._sock.send(passwordBytes);
|
||||||
|
@ -2937,7 +2881,9 @@ export default class RFB extends EventTargetMixin {
|
||||||
|
|
||||||
static genDES(password, challenge) {
|
static genDES(password, challenge) {
|
||||||
const passwordChars = password.split('').map(c => c.charCodeAt(0));
|
const passwordChars = password.split('').map(c => c.charCodeAt(0));
|
||||||
return (new DES(passwordChars)).encrypt(challenge);
|
const key = legacyCrypto.importKey(
|
||||||
|
"raw", passwordChars, { name: "DES-ECB" }, false, ["encrypt"]);
|
||||||
|
return legacyCrypto.encrypt({ name: "DES-ECB" }, key, challenge);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { encodings } from '../core/encodings.js';
|
||||||
import { toUnsigned32bit } from '../core/util/int.js';
|
import { toUnsigned32bit } from '../core/util/int.js';
|
||||||
import { encodeUTF8 } from '../core/util/strings.js';
|
import { encodeUTF8 } from '../core/util/strings.js';
|
||||||
import KeyTable from '../core/input/keysym.js';
|
import KeyTable from '../core/input/keysym.js';
|
||||||
|
import legacyCrypto from '../core/crypto/crypto.js';
|
||||||
|
|
||||||
import FakeWebSocket from './fake.websocket.js';
|
import FakeWebSocket from './fake.websocket.js';
|
||||||
|
|
||||||
|
@ -1270,6 +1271,19 @@ describe('Remote Frame Buffer Protocol Client', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('ARD Authentication (type 30) Handler', function () {
|
describe('ARD Authentication (type 30) Handler', function () {
|
||||||
|
let byteArray = new Uint8Array(Array.from(new Uint8Array(128).keys()));
|
||||||
|
function fakeGetRandomValues(arr) {
|
||||||
|
if (arr.length == 128) {
|
||||||
|
arr.set(byteArray);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
before(() => {
|
||||||
|
sinon.stub(window.crypto, "getRandomValues").callsFake(fakeGetRandomValues);
|
||||||
|
});
|
||||||
|
after(() => {
|
||||||
|
window.crypto.getRandomValues.restore();
|
||||||
|
});
|
||||||
it('should fire the credentialsrequired event if all credentials are missing', function () {
|
it('should fire the credentialsrequired event if all credentials are missing', function () {
|
||||||
const spy = sinon.spy();
|
const spy = sinon.spy();
|
||||||
client.addEventListener("credentialsrequired", spy);
|
client.addEventListener("credentialsrequired", spy);
|
||||||
|
@ -1298,35 +1312,30 @@ describe('Remote Frame Buffer Protocol Client', function () {
|
||||||
|
|
||||||
expect(client._sock).to.have.sent([30]);
|
expect(client._sock).to.have.sent([30]);
|
||||||
|
|
||||||
function byteArray(length) {
|
const generator = new Uint8Array([127, 255]);
|
||||||
return Array.from(new Uint8Array(length).keys());
|
const prime = new Uint8Array(byteArray);
|
||||||
}
|
const serverKey = legacyCrypto.generateKey(
|
||||||
|
{ name: "DH", g: generator, p: prime }, false, ["deriveBits"]);
|
||||||
|
const clientKey = legacyCrypto.generateKey(
|
||||||
|
{ name: "DH", g: generator, p: prime }, false, ["deriveBits"]);
|
||||||
|
const serverPublicKey = legacyCrypto.exportKey("raw", serverKey.publicKey);
|
||||||
|
const clientPublicKey = legacyCrypto.exportKey("raw", clientKey.publicKey);
|
||||||
|
|
||||||
let generator = [127, 255];
|
await client._negotiateARDAuthAsync(128, serverPublicKey, clientKey);
|
||||||
let prime = byteArray(128);
|
|
||||||
let serverPrivateKey = byteArray(128);
|
|
||||||
let serverPublicKey = client._modPow(generator, serverPrivateKey, prime);
|
|
||||||
|
|
||||||
let clientPrivateKey = byteArray(128);
|
|
||||||
let clientPublicKey = client._modPow(generator, clientPrivateKey, prime);
|
|
||||||
|
|
||||||
let padding = Array.from(byteArray(64), byte => String.fromCharCode(65+byte%26)).join('');
|
|
||||||
|
|
||||||
await client._negotiateARDAuthAsync(generator, 128, prime, serverPublicKey, clientPrivateKey, padding);
|
|
||||||
|
|
||||||
client._negotiateARDAuth();
|
client._negotiateARDAuth();
|
||||||
|
|
||||||
expect(client._rfbInitState).to.equal('SecurityResult');
|
expect(client._rfbInitState).to.equal('SecurityResult');
|
||||||
|
|
||||||
let expectEncrypted = new Uint8Array([
|
let expectEncrypted = new Uint8Array([
|
||||||
232, 234, 159, 162, 170, 180, 138, 104, 164, 49, 53, 96, 20, 36, 21, 15,
|
199, 39, 204, 95, 190, 70, 127, 66, 5, 106, 153, 228, 123, 236, 150, 206,
|
||||||
217, 219, 107, 173, 196, 60, 96, 142, 215, 71, 13, 185, 185, 47, 5, 175,
|
62, 107, 11, 4, 21, 242, 92, 184, 9, 81, 35, 125, 56, 167, 1, 215,
|
||||||
151, 30, 194, 55, 173, 214, 141, 161, 36, 138, 146, 3, 178, 89, 43, 248,
|
182, 145, 183, 75, 245, 197, 47, 19, 122, 94, 64, 76, 77, 163, 222, 143,
|
||||||
131, 134, 205, 174, 9, 150, 171, 74, 222, 201, 20, 2, 30, 168, 162, 123,
|
186, 174, 84, 39, 244, 179, 227, 114, 83, 231, 42, 106, 205, 43, 159, 110,
|
||||||
46, 86, 81, 221, 44, 211, 180, 247, 221, 61, 95, 155, 157, 241, 76, 76,
|
209, 240, 157, 246, 237, 206, 134, 153, 195, 112, 92, 60, 28, 234, 91, 66,
|
||||||
49, 217, 234, 75, 147, 237, 199, 159, 93, 140, 191, 174, 52, 90, 133, 58,
|
131, 38, 187, 195, 110, 167, 212, 241, 32, 250, 212, 213, 202, 89, 180, 21,
|
||||||
243, 81, 112, 182, 64, 62, 149, 7, 151, 28, 36, 161, 247, 247, 36, 96,
|
71, 217, 209, 81, 42, 61, 118, 248, 65, 123, 98, 78, 139, 111, 202, 137,
|
||||||
230, 95, 58, 207, 46, 183, 100, 139, 143, 155, 224, 43, 219, 3, 71, 139]);
|
50, 185, 37, 173, 58, 99, 187, 53, 42, 125, 13, 165, 232, 163, 151, 42, 0]);
|
||||||
|
|
||||||
let output = new Uint8Array(256);
|
let output = new Uint8Array(256);
|
||||||
output.set(expectEncrypted, 0);
|
output.set(expectEncrypted, 0);
|
||||||
|
|
Loading…
Reference in New Issue