tutanota/src/api/worker/crypto/Aes.js

291 lines
11 KiB
JavaScript
Raw Normal View History

2017-08-15 13:54:22 +02:00
// @flow
// $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"
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"
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) {
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"]
// ))
// }