1/** 2 * Encaspsulates AES-CBC encryption using SubtleCrypto 3 * 4 * @link https://gist.github.com/chrisveness/43bcda93af9f646d083fad678071b90a 5 */ 6class SubtleAES { 7 8 iterations = 10000; // OpenSSL default 9 10 /** 11 * Encrypts plaintext using AES-CBC with Pkdf2 derived key 12 * 13 * @param {String} plaintext Plaintext to be encrypted 14 * @param {String} password Password to use to encrypt plaintext 15 * @returns {Promise<String>} Encrypted ciphertext base64 encoded 16 */ 17 async encrypt(plaintext, password) { 18 19 const salt = this.randomSalt(); 20 const {hash, iv} = await this.derivePkdf2(password, salt, 'SHA-256', this.iterations); 21 22 const alg = {name: 'AES-CBC', iv: iv}; 23 const key = await crypto.subtle.importKey('raw', hash, alg, false, ['encrypt']); 24 25 const cipher = await crypto.subtle.encrypt(alg, key, new TextEncoder().encode(plaintext)); 26 27 return this.createOpenSSLCryptString(salt, new Uint8Array(cipher)); 28 } 29 30 /** 31 * Decrypts OpenSSL ciphertext 32 * 33 * @param {String} ciphertext Base64 encoded ciphertext to be decrypted. 34 * @param {String} password Password to use to decrypt ciphertext 35 * @param {boolean} legacy 36 * @returns {Promise<String>} Decrypted plaintext. 37 */ 38 async decrypt(ciphertext, password, legacy = false) { 39 const {salt, cipher} = this.parseOpenSSLCryptString(ciphertext); 40 41 let hash, iv; 42 if (legacy) { 43 ({hash, iv} = this.deriveMd5(password, salt)); 44 } else { 45 ({hash, iv} = await this.derivePkdf2(password, salt, 'SHA-256', this.iterations)); 46 } 47 48 const alg = {name: 'AES-CBC', iv: iv}; 49 const key = await crypto.subtle.importKey('raw', hash, alg, false, ['decrypt']); 50 51 try { 52 const plainBuffer = await crypto.subtle.decrypt(alg, key, cipher); 53 return new TextDecoder().decode(plainBuffer); 54 } catch (e) { 55 throw new Error('Decrypt failed'); 56 } 57 } 58 59 /** 60 * Decrypt trying modern and legacy variants 61 * 62 * @param {String} ciphertext Base64 encoded ciphertext to be decrypted. 63 * @param {String} password Password to use to decrypt ciphertext 64 * @returns {Promise<String>} Decrypted plaintext. 65 */ 66 async autodecrypt(ciphertext, password) { 67 try { 68 return await this.decrypt(ciphertext, password); 69 } catch (e) { 70 //ignore 71 } 72 return await this.decrypt(ciphertext, password, true); 73 } 74 75 /** 76 * Generate a random salt 77 * 78 * @return {Uint8Array} 79 */ 80 randomSalt() { 81 return crypto.getRandomValues(new Uint8Array(8)); 82 } 83 84 /** 85 * Parse a base64 string created by openssl enc 86 * 87 * @param str 88 * @return {{cipher: Uint8Array, salt: Uint8Array}} 89 */ 90 parseOpenSSLCryptString(str) { 91 const ostring = atob(str); 92 93 if (ostring.slice(0, 8) !== 'Salted__') { 94 throw new Error('Input seems not to be created by OpenSSL compatible enc mechanism'); 95 } 96 97 return { 98 salt: new Uint8Array(Array.from(ostring.slice(8, 16)).map(ch => ch.charCodeAt(0))), 99 cipher: new Uint8Array(Array.from(ostring.slice(16)).map(ch => ch.charCodeAt(0))), 100 } 101 } 102 103 /** 104 * Create the openssl enc compatible string 105 * 106 * @param {Uint8Array} salt 107 * @param {Uint8Array} cipher 108 * @return {string} 109 */ 110 createOpenSSLCryptString(salt, cipher) { 111 const concat = new Uint8Array([ 112 0x53, 0x61, 0x6c, 0x74, 0x65, 0x64, 0x5f, 0x5f, // Salted__ 113 ...salt, 114 ...cipher, 115 ]); 116 117 // base64 string 118 return btoa(String.fromCharCode.apply(null, concat)); 119 } 120 121 /** 122 * Use PKDF2 to derive the IV and key 123 * 124 * @param {string} strPassword The clear text password 125 * @param {Uint8Array} salt The salt 126 * @param {string} hash The Hash model, e.g. ["SHA-256" | "SHA-512"] 127 * @param {int} iterations Number of iterations 128 * @return {Promise<{hash: Uint8Array, iv: Uint8Array}>} 129 * @link https://stackoverflow.com/q/67993979 130 */ 131 async derivePkdf2(strPassword, salt, hash, iterations) { 132 const password = new TextEncoder().encode(strPassword); 133 134 const ik = await window.crypto.subtle.importKey("raw", password, {name: "PBKDF2"}, false, ["deriveBits"]); 135 const dk = await window.crypto.subtle.deriveBits( 136 { 137 name: "PBKDF2", 138 hash: hash, 139 salt: salt, 140 iterations: iterations 141 }, 142 ik, 143 48 * 8 144 ); // Bytes to bits 145 const buffer = new Uint8Array(dk); 146 147 return { 148 hash: buffer.slice(0, 32), 149 iv: buffer.slice(32, 48), 150 } 151 } 152 153 /** 154 * The old, legacy method to derive key and IV from the password 155 * 156 * @param {string} strPassword The clear text password 157 * @param {Uint8Array} salt The salt 158 * @return {{iv: Uint8Array, hash: Uint8Array}} 159 * @link https://security.stackexchange.com/a/242567 160 */ 161 deriveMd5(strPassword, salt) { 162 const password = new TextEncoder().encode(strPassword); 163 164 const D1 = md5.array(new Uint8Array([...password, ...salt])); 165 const D2 = md5.array(new Uint8Array([...D1, ...password, ...salt])); 166 const D3 = md5.array(new Uint8Array([...D2, ...password, ...salt])); 167 168 return { 169 hash: new Uint8Array([...D1, ...D2]), 170 iv: new Uint8Array(D3), 171 } 172 } 173} 174