2017-08-15 13:54:22 +02:00
|
|
|
// @flow
|
2019-08-22 18:24:32 +02:00
|
|
|
// $FlowIgnore[untyped-import]
|
2019-09-13 13:49:11 +02:00
|
|
|
import sjcl from "./lib/sjcl"
|
2017-08-15 13:54:22 +02:00
|
|
|
import {random} from "./Randomizer"
|
2018-11-08 14:17:30 +01:00
|
|
|
import {bitArrayToUint8Array, uint8ArrayToBitArray} from "./CryptoUtils"
|
|
|
|
|
import {arrayEquals, concat} from "../../common/utils/ArrayUtils"
|
2017-08-15 13:54:22 +02:00
|
|
|
import {uint8ArrayToBase64} from "../../common/utils/Encoding"
|
|
|
|
|
import {CryptoError} from "../../common/error/CryptoError"
|
2021-02-03 17:13:38 +01:00
|
|
|
import {assertWorkerOrNode} from "../../common/Env"
|
2017-09-25 16:22:38 +02:00
|
|
|
import {hash} from "./Sha256"
|
2018-11-08 14:17:30 +01:00
|
|
|
import * as Sha512 from "./Sha512"
|
2017-08-15 13:54:22 +02:00
|
|
|
|
|
|
|
|
assertWorkerOrNode()
|
|
|
|
|
|
2017-10-09 17:00:46 +02:00
|
|
|
export const ENABLE_MAC = true
|
2017-09-25 16:22:38 +02:00
|
|
|
|
2017-08-15 13:54:22 +02:00
|
|
|
export const IV_BYTE_LENGTH = 16
|
|
|
|
|
|
|
|
|
|
const KEY_LENGTH_BYTES_AES_256 = 32
|
|
|
|
|
const KEY_LENGTH_BITS_AES_256 = KEY_LENGTH_BYTES_AES_256 * 8
|
|
|
|
|
const KEY_LENGTH_BYTES_AES_128 = 16
|
|
|
|
|
const KEY_LENGTH_BITS_AES_128 = KEY_LENGTH_BYTES_AES_128 * 8
|
2017-09-25 16:22:38 +02:00
|
|
|
const MAC_ENABLED_PREFIX = 1
|
2017-12-12 14:54:35 +01:00
|
|
|
const MAC_LENGTH_BYTES = 32
|
2017-08-15 13:54:22 +02:00
|
|
|
|
|
|
|
|
export function aes256RandomKey(): Aes256Key {
|
|
|
|
|
return uint8ArrayToBitArray(random.generateRandomData(KEY_LENGTH_BYTES_AES_256))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2017-12-12 14:54:35 +01:00
|
|
|
* Encrypts bytes with AES 256 in CBC mode.
|
2017-08-15 13:54:22 +02:00
|
|
|
* @param key The key to use for the encryption.
|
|
|
|
|
* @param bytes The plain text.
|
|
|
|
|
* @param iv The initialization vector.
|
|
|
|
|
* @param usePadding If true, padding is used, otherwise no padding is used and the encrypted data must have the key size.
|
2017-12-12 14:54:35 +01:00
|
|
|
* @param useMac If true, a 256 bit HMAC is appended to the encrypted data.
|
2017-08-15 13:54:22 +02:00
|
|
|
* @return The encrypted text as words (sjcl internal structure)..
|
|
|
|
|
*/
|
2017-09-26 10:34:13 +02:00
|
|
|
export function aes256Encrypt(key: Aes256Key, bytes: Uint8Array, iv: Uint8Array, usePadding: boolean = true, useMac: boolean = true): Uint8Array {
|
2017-12-12 14:54:35 +01:00
|
|
|
verifyKeySize(key, KEY_LENGTH_BITS_AES_256)
|
2017-08-15 13:54:22 +02:00
|
|
|
if (iv.length !== IV_BYTE_LENGTH) {
|
|
|
|
|
throw new CryptoError(`Illegal IV length: ${iv.length} (expected: ${IV_BYTE_LENGTH}): ${uint8ArrayToBase64(iv)} `)
|
|
|
|
|
}
|
2017-12-12 14:54:35 +01:00
|
|
|
|
|
|
|
|
let subKeys = getAes256SubKeys(key, useMac)
|
|
|
|
|
|
|
|
|
|
let encryptedBits = sjcl.mode.cbc.encrypt(new sjcl.cipher.aes(subKeys.cKey), uint8ArrayToBitArray(bytes), uint8ArrayToBitArray(iv), [], usePadding);
|
|
|
|
|
|
|
|
|
|
let data = concat(iv, bitArrayToUint8Array(encryptedBits))
|
|
|
|
|
|
|
|
|
|
if (useMac) {
|
|
|
|
|
let hmac = new sjcl.misc.hmac(subKeys.mKey, sjcl.hash.sha256)
|
|
|
|
|
let macBytes = bitArrayToUint8Array(hmac.encrypt(uint8ArrayToBitArray(data)))
|
2021-03-29 16:33:36 +02:00
|
|
|
data = concat(new Uint8Array([MAC_ENABLED_PREFIX]), data, macBytes)
|
2017-12-12 14:54:35 +01:00
|
|
|
}
|
|
|
|
|
return data
|
2017-08-15 13:54:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2017-12-12 14:54:35 +01:00
|
|
|
* Decrypts the given words with AES 256 in CBC mode.
|
2017-08-15 13:54:22 +02:00
|
|
|
* @param key The key to use for the decryption.
|
2019-08-29 09:17:05 +02:00
|
|
|
* @param encryptedBytes The ciphertext.
|
2017-08-15 13:54:22 +02:00
|
|
|
* @param usePadding If true, padding is used, otherwise no padding is used and the encrypted data must have the key size.
|
2017-12-12 14:54:35 +01:00
|
|
|
* @param useMac If true, a 256 bit HMAC is assumed to be appended to the encrypted data and it is checked before decryption.
|
2017-08-15 13:54:22 +02:00
|
|
|
* @return The decrypted bytes.
|
|
|
|
|
*/
|
2017-12-12 14:54:35 +01:00
|
|
|
export function aes256Decrypt(key: Aes256Key, encryptedBytes: Uint8Array, usePadding: boolean = true, useMac: boolean = true): Uint8Array {
|
2017-08-15 13:54:22 +02:00
|
|
|
verifyKeySize(key, KEY_LENGTH_BITS_AES_256)
|
|
|
|
|
|
2017-12-12 14:54:35 +01:00
|
|
|
let subKeys = getAes256SubKeys(key, useMac)
|
|
|
|
|
let cipherTextWithoutMac
|
|
|
|
|
if (useMac) {
|
|
|
|
|
cipherTextWithoutMac = encryptedBytes.subarray(1, encryptedBytes.length - MAC_LENGTH_BYTES)
|
|
|
|
|
let providedMacBytes = encryptedBytes.subarray(encryptedBytes.length - MAC_LENGTH_BYTES)
|
|
|
|
|
let hmac = new sjcl.misc.hmac(subKeys.mKey, sjcl.hash.sha256)
|
|
|
|
|
let computedMacBytes = bitArrayToUint8Array(hmac.encrypt(uint8ArrayToBitArray(cipherTextWithoutMac)))
|
|
|
|
|
if (!arrayEquals(providedMacBytes, computedMacBytes)) {
|
|
|
|
|
throw new CryptoError("invalid mac")
|
2017-08-15 13:54:22 +02:00
|
|
|
}
|
2017-12-12 14:54:35 +01:00
|
|
|
} else {
|
|
|
|
|
cipherTextWithoutMac = encryptedBytes
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// take the iv from the front of the encrypted data
|
2019-04-10 16:42:38 +02:00
|
|
|
const iv = cipherTextWithoutMac.slice(0, IV_BYTE_LENGTH)
|
|
|
|
|
if (iv.length !== IV_BYTE_LENGTH) {
|
|
|
|
|
throw new CryptoError(`Invalid IV length in AES256Decrypt: ${iv.length} bytes, must be 16 bytes (128 bits)`)
|
|
|
|
|
}
|
2017-12-12 14:54:35 +01:00
|
|
|
let ciphertext = cipherTextWithoutMac.slice(IV_BYTE_LENGTH)
|
|
|
|
|
try {
|
|
|
|
|
let decrypted = sjcl.mode.cbc.decrypt(new sjcl.cipher.aes(subKeys.cKey), uint8ArrayToBitArray(ciphertext), uint8ArrayToBitArray(iv), [], usePadding)
|
2019-08-29 09:17:05 +02:00
|
|
|
return new Uint8Array(bitArrayToUint8Array(decrypted))
|
2017-08-15 13:54:22 +02:00
|
|
|
} catch (e) {
|
|
|
|
|
throw new CryptoError("aes decryption failed", e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2018-07-26 17:37:44 +02:00
|
|
|
function verifyKeySize(key: Aes128Key | Aes256Key, bitLength: number) {
|
2017-08-15 13:54:22 +02:00
|
|
|
if (sjcl.bitArray.bitLength(key) !== bitLength) {
|
|
|
|
|
throw new CryptoError(`Illegal key length: ${sjcl.bitArray.bitLength(key)} (expected: ${bitLength})`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/************************ Legacy AES128 ************************/
|
|
|
|
|
|
|
|
|
|
export function aes128RandomKey(): Aes128Key {
|
|
|
|
|
return uint8ArrayToBitArray(random.generateRandomData(KEY_LENGTH_BYTES_AES_128))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Encrypts bytes with AES128 in CBC mode.
|
|
|
|
|
* @param key The key to use for the encryption.
|
|
|
|
|
* @param bytes The plain text.
|
|
|
|
|
* @param iv The initialization vector.
|
|
|
|
|
* @param usePadding If true, padding is used, otherwise no padding is used and the encrypted data must have the key size.
|
|
|
|
|
* @return The encrypted bytes
|
|
|
|
|
*/
|
2017-09-25 16:22:38 +02:00
|
|
|
export function aes128Encrypt(key: Aes128Key, bytes: Uint8Array, iv: Uint8Array, usePadding: boolean, useMac: boolean): Uint8Array {
|
2017-08-15 13:54:22 +02:00
|
|
|
verifyKeySize(key, KEY_LENGTH_BITS_AES_128)
|
|
|
|
|
if (iv.length !== IV_BYTE_LENGTH) {
|
|
|
|
|
throw new CryptoError(`Illegal IV length: ${iv.length} (expected: ${IV_BYTE_LENGTH}): ${uint8ArrayToBase64(iv)} `)
|
|
|
|
|
}
|
2017-09-25 16:22:38 +02:00
|
|
|
|
2017-12-12 14:54:35 +01:00
|
|
|
let subKeys = getAes128SubKeys(key, useMac)
|
2017-09-25 16:22:38 +02:00
|
|
|
|
|
|
|
|
let encryptedBits = sjcl.mode.cbc.encrypt(new sjcl.cipher.aes(subKeys.cKey), uint8ArrayToBitArray(bytes), uint8ArrayToBitArray(iv), [], usePadding);
|
|
|
|
|
|
|
|
|
|
let data = concat(iv, bitArrayToUint8Array(encryptedBits))
|
|
|
|
|
|
|
|
|
|
if (useMac) {
|
|
|
|
|
let hmac = new sjcl.misc.hmac(subKeys.mKey, sjcl.hash.sha256)
|
|
|
|
|
let macBytes = bitArrayToUint8Array(hmac.encrypt(uint8ArrayToBitArray(data)))
|
|
|
|
|
data = concat(new Uint8Array([MAC_ENABLED_PREFIX]), data, macBytes)
|
|
|
|
|
}
|
|
|
|
|
return data
|
2017-08-15 13:54:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Decrypts the given words with AES128 in CBC mode.
|
|
|
|
|
* @param key The key to use for the decryption.
|
2019-08-29 09:17:05 +02:00
|
|
|
* @param encryptedBytes The ciphertext encoded as bytes.
|
2017-08-15 13:54:22 +02:00
|
|
|
* @param usePadding If true, padding is used, otherwise no padding is used and the encrypted data must have the key size.
|
|
|
|
|
* @return The decrypted bytes.
|
|
|
|
|
*/
|
2018-07-26 17:37:44 +02:00
|
|
|
export function aes128Decrypt(key: Aes128Key, encryptedBytes: Uint8Array, usePadding: boolean = true): Uint8Array {
|
2017-08-15 13:54:22 +02:00
|
|
|
verifyKeySize(key, KEY_LENGTH_BITS_AES_128)
|
2017-09-25 16:22:38 +02:00
|
|
|
|
2019-08-29 09:17:05 +02:00
|
|
|
let useMac = encryptedBytes.length % 2 === 1
|
2017-12-12 14:54:35 +01:00
|
|
|
let subKeys = getAes128SubKeys(key, useMac)
|
2017-09-25 16:22:38 +02:00
|
|
|
let cipherTextWithoutMac
|
|
|
|
|
if (useMac) {
|
2017-12-12 14:54:35 +01:00
|
|
|
cipherTextWithoutMac = encryptedBytes.subarray(1, encryptedBytes.length - MAC_LENGTH_BYTES)
|
|
|
|
|
let providedMacBytes = encryptedBytes.subarray(encryptedBytes.length - MAC_LENGTH_BYTES)
|
2017-09-25 16:22:38 +02:00
|
|
|
let hmac = new sjcl.misc.hmac(subKeys.mKey, sjcl.hash.sha256)
|
|
|
|
|
let computedMacBytes = bitArrayToUint8Array(hmac.encrypt(uint8ArrayToBitArray(cipherTextWithoutMac)))
|
|
|
|
|
if (!arrayEquals(providedMacBytes, computedMacBytes)) {
|
|
|
|
|
throw new CryptoError("invalid mac")
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
cipherTextWithoutMac = encryptedBytes
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-15 13:54:22 +02:00
|
|
|
// take the iv from the front of the encrypted data
|
2019-04-10 16:42:38 +02:00
|
|
|
const iv = cipherTextWithoutMac.slice(0, IV_BYTE_LENGTH)
|
|
|
|
|
if (iv.length !== IV_BYTE_LENGTH) {
|
|
|
|
|
throw new CryptoError(`Invalid IV length in AES128Decrypt: ${iv.length} bytes, must be 16 bytes (128 bits)`)
|
|
|
|
|
}
|
2017-09-25 16:22:38 +02:00
|
|
|
let ciphertext = cipherTextWithoutMac.slice(IV_BYTE_LENGTH)
|
2017-08-15 13:54:22 +02:00
|
|
|
try {
|
2017-09-25 16:22:38 +02:00
|
|
|
let decrypted = sjcl.mode.cbc.decrypt(new sjcl.cipher.aes(subKeys.cKey), uint8ArrayToBitArray(ciphertext), uint8ArrayToBitArray(iv), [], usePadding)
|
2019-08-29 09:17:05 +02:00
|
|
|
return new Uint8Array(bitArrayToUint8Array(decrypted))
|
2017-08-15 13:54:22 +02:00
|
|
|
} catch (e) {
|
|
|
|
|
throw new CryptoError("aes decryption failed", e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-26 17:37:44 +02:00
|
|
|
function getAes128SubKeys(key: Aes128Key, mac: boolean): {mKey: ?Aes128Key, cKey: Aes128Key} {
|
2017-09-25 16:22:38 +02:00
|
|
|
if (mac) {
|
|
|
|
|
let hashedKey = hash(bitArrayToUint8Array(key));
|
|
|
|
|
return {
|
2017-12-12 14:54:35 +01:00
|
|
|
cKey: uint8ArrayToBitArray(hashedKey.subarray(0, KEY_LENGTH_BYTES_AES_128)),
|
|
|
|
|
mKey: uint8ArrayToBitArray(hashedKey.subarray(KEY_LENGTH_BYTES_AES_128, KEY_LENGTH_BYTES_AES_128 * 2))
|
2017-09-25 16:22:38 +02:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
return {
|
|
|
|
|
cKey: key,
|
|
|
|
|
mKey: null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-26 17:37:44 +02:00
|
|
|
function getAes256SubKeys(key: Aes256Key, mac: boolean): {mKey: ?Aes256Key, cKey: Aes256Key} {
|
2017-12-12 14:54:35 +01:00
|
|
|
if (mac) {
|
2018-11-08 14:17:30 +01:00
|
|
|
let hashedKey = Sha512.hash(bitArrayToUint8Array(key));
|
|
|
|
|
return {
|
|
|
|
|
cKey: uint8ArrayToBitArray(hashedKey.subarray(0, KEY_LENGTH_BYTES_AES_256)),
|
|
|
|
|
mKey: uint8ArrayToBitArray(hashedKey.subarray(KEY_LENGTH_BYTES_AES_256, KEY_LENGTH_BYTES_AES_256 * 2))
|
|
|
|
|
}
|
2017-12-12 14:54:35 +01:00
|
|
|
} else {
|
|
|
|
|
return {
|
|
|
|
|
cKey: key,
|
|
|
|
|
mKey: null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-15 13:54:22 +02:00
|
|
|
/************************ Webcrypto AES256 ************************/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Encrypts bytes with AES in GCM mode using the webcrypto API.
|
|
|
|
|
* @param key The key to use for the encryption.
|
|
|
|
|
* @param bytes The plain text.
|
|
|
|
|
* @param iv The initialization vector.
|
|
|
|
|
* @param usePadding If true, padding is used, otherwise no padding is used and the encrypted data must have the key size.
|
2017-09-26 10:34:13 +02:00
|
|
|
* @param useMac Not used because gcm always contains a mac. Just exists for interface compatibility with AES 128
|
2017-08-15 13:54:22 +02:00
|
|
|
* @return The encrypted text as words (sjcl internal structure)..
|
|
|
|
|
*/
|
2017-12-12 14:54:35 +01:00
|
|
|
// export function aes256EncryptFile(key: Aes256Key, bytes: Uint8Array, iv: Uint8Array, usePadding: boolean = true, useMac: boolean = true): Promise<Uint8Array> {
|
|
|
|
|
// verifyKeySize(key, KEY_LENGTH_BITS_AES_256)
|
|
|
|
|
// if (usePadding) {
|
|
|
|
|
// bytes = pad(bytes) // TODO (bdeterding) consider implementing padding for bit array.
|
|
|
|
|
// }
|
|
|
|
|
// if (iv.length !== IV_BYTE_LENGTH) {
|
|
|
|
|
// throw new CryptoError(`Illegal IV length: ${iv.length} (expected: ${IV_BYTE_LENGTH}): ${uint8ArrayToBase64(iv)} `)
|
|
|
|
|
// }
|
|
|
|
|
// return importAesKey(key).then(cryptoKey => {
|
|
|
|
|
// return crypto.subtle.encrypt(
|
|
|
|
|
// {
|
|
|
|
|
// name: "AES-GCM",
|
|
|
|
|
// iv: iv,
|
|
|
|
|
// tagLength: TAG_BIT_LENGTH
|
|
|
|
|
// },
|
|
|
|
|
// cryptoKey,
|
|
|
|
|
// bytes
|
|
|
|
|
// ).then(function (encrypted) {
|
|
|
|
|
// return concat(iv, new Uint8Array(encrypted))
|
|
|
|
|
// })
|
|
|
|
|
// }).catch(e => {
|
|
|
|
|
// throw new CryptoError("aes encryption failed (webcrypto)", e)
|
|
|
|
|
// })
|
|
|
|
|
// }
|
2017-08-15 13:54:22 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Decrypts the given words with AES in GCM mode using the webcrypto API.
|
|
|
|
|
* @param key The key to use for the decryption.
|
|
|
|
|
* @param words The ciphertext encoded as words.
|
|
|
|
|
* @param usePadding If true, padding is used, otherwise no padding is used and the encrypted data must have the key size.
|
|
|
|
|
* @return The decrypted bytes.
|
|
|
|
|
*/
|
2017-12-12 14:54:35 +01:00
|
|
|
// export function aes256DecryptFile(key: Aes256Key, encryptedBytes: Uint8Array, usePadding: boolean = true): Promise<Uint8Array> {
|
|
|
|
|
// verifyKeySize(key, KEY_LENGTH_BITS_AES_256)
|
|
|
|
|
// // take the iv from the front of the encrypted data
|
|
|
|
|
// let iv = encryptedBytes.slice(0, IV_BYTE_LENGTH)
|
|
|
|
|
// let ciphertext = encryptedBytes.slice(IV_BYTE_LENGTH)
|
|
|
|
|
//
|
|
|
|
|
// return importAesKey(key).then(cryptoKey => {
|
|
|
|
|
// return crypto.subtle.decrypt(
|
|
|
|
|
// {
|
|
|
|
|
// name: "AES-GCM",
|
|
|
|
|
// iv: iv,
|
|
|
|
|
// tagLength: TAG_BIT_LENGTH
|
|
|
|
|
// },
|
|
|
|
|
// cryptoKey,
|
|
|
|
|
// ciphertext
|
|
|
|
|
// ).then(function (decrypted) {
|
|
|
|
|
// let decryptedBytes = new Uint8Array(decrypted)
|
|
|
|
|
// if (usePadding) {
|
|
|
|
|
// decryptedBytes = unpad(decryptedBytes)
|
|
|
|
|
// }
|
|
|
|
|
// return decryptedBytes
|
|
|
|
|
// })
|
|
|
|
|
// }).catch(e => {
|
|
|
|
|
// throw new CryptoError("aes decryption failed (webcrypto)", e)
|
|
|
|
|
// })
|
|
|
|
|
// }
|
2017-08-15 13:54:22 +02:00
|
|
|
|
2017-12-12 14:54:35 +01:00
|
|
|
// function importAesKey(key: Aes128Key|Aes256Key): Promise<CryptoKey> {
|
|
|
|
|
// // convert native promise into bluebird promise
|
|
|
|
|
// var keyArray = bitArrayToUint8Array(key);
|
|
|
|
|
// return Promise.resolve(crypto.subtle.importKey(
|
|
|
|
|
// "raw",
|
|
|
|
|
// keyArray,
|
|
|
|
|
// keyArray.length === 128 ? "AES-CBC" : "AES-GCM",
|
|
|
|
|
// false,
|
|
|
|
|
// ["encrypt", "decrypt"]
|
|
|
|
|
// ))
|
|
|
|
|
// }
|