/** | |
* Encrypts plaintext using AES-GCM with supplied password, for decryption with aesGcmDecrypt(). | |
* (c) Chris Veness MIT Licence | |
* | |
* @param {String} plaintext - Plaintext to be encrypted. | |
* @param {String} password - Password to use to encrypt plaintext. | |
* @returns {String} Encrypted ciphertext. | |
* | |
* @example | |
* const ciphertext = await aesGcmEncrypt('my secret text', 'pw'); | |
* aesGcmEncrypt('my secret text', 'pw').then(function(ciphertext) { console.log(ciphertext); }); | |
*/ | |
async function aesGcmEncrypt(plaintext, password) { | |
const pwUtf8 = new TextEncoder().encode(password); // encode password as UTF-8 | |
const pwHash = await crypto.subtle.digest('SHA-256', pwUtf8); // hash the password | |
const iv = crypto.getRandomValues(new Uint8Array(12)); // get 96-bit random iv | |
const alg = { name: 'AES-GCM', iv: iv }; // specify algorithm to use | |
const key = await crypto.subtle.importKey('raw', pwHash, alg, false, ['encrypt']); // generate key from pw | |
const ptUint8 = new TextEncoder().encode(plaintext); // encode plaintext as UTF-8 | |
const ctBuffer = await crypto.subtle.encrypt(alg, key, ptUint8); // encrypt plaintext using key | |
const ctArray = Array.from(new Uint8Array(ctBuffer)); // ciphertext as byte array | |
const ctStr = ctArray.map(byte => String.fromCharCode(byte)).join(''); // ciphertext as string | |
const ctBase64 = btoa(ctStr); // encode ciphertext as base64 | |
const ivHex = Array.from(iv).map(b => ('00' + b.toString(16)).slice(-2)).join(''); // iv as hex string | |
return ivHex+ctBase64; // return iv+ciphertext | |
} | |
/** | |
* Decrypts ciphertext encrypted with aesGcmEncrypt() using supplied password. | |
* (c) Chris Veness MIT Licence | |
* | |
* @param {String} ciphertext - Ciphertext to be decrypted. | |
* @param {String} password - Password to use to decrypt ciphertext. | |
* @returns {String} Decrypted plaintext. | |
* | |
* @example | |
* const plaintext = await aesGcmDecrypt(ciphertext, 'pw'); | |
* aesGcmDecrypt(ciphertext, 'pw').then(function(plaintext) { console.log(plaintext); }); | |
*/ | |
async function aesGcmDecrypt(ciphertext, password) { | |
const pwUtf8 = new TextEncoder().encode(password); // encode password as UTF-8 | |
const pwHash = await crypto.subtle.digest('SHA-256', pwUtf8); // hash the password | |
const iv = ciphertext.slice(0,24).match(/.{2}/g).map(byte => parseInt(byte, 16)); // get iv from ciphertext | |
const alg = { name: 'AES-GCM', iv: new Uint8Array(iv) }; // specify algorithm to use | |
const key = await crypto.subtle.importKey('raw', pwHash, alg, false, ['decrypt']); // use pw to generate key | |
const ctStr = atob(ciphertext.slice(24)); // decode base64 ciphertext | |
const ctUint8 = new Uint8Array(ctStr.match(/[\s\S]/g).map(ch => ch.charCodeAt(0))); // ciphertext as Uint8Array | |
// note: why doesn't ctUint8 = new TextEncoder().encode(ctStr) work? | |
const plainBuffer = await crypto.subtle.decrypt(alg, key, ctUint8); // decrypt ciphertext using key | |
const plaintext = new TextDecoder().decode(plainBuffer); // decode password from UTF-8 | |
return plaintext; // return the plaintext | |
} |
This comment has been minimized.
This comment has been minimized.
With the benefit of node-webcrypto-ossl and text-encoding polyfills, these routines will run on Node.js, and will inter-operate between the browser and the server. Simply add the following at the top of the file:
Then to use it,
|
This comment has been minimized.
This comment has been minimized.
match(/./g) is leaving out whitespace chars, making the Decrypt fail when ctStr contains a return or linefeed for instance. For now I am using a for-loop with charCodeAt() but maybe there's a more efficient way. |
This comment has been minimized.
This comment has been minimized.
Hi @chrisveness , Also by the way, I'm assuming that there is no known symmetric-key encryption system significantly more secure than AES-256-GCM. Thanks, |
This comment has been minimized.
This comment has been minimized.
Hi @majodi, Thanks, |
This comment has been minimized.
This comment has been minimized.
i want to say a big thanks to you. this is the first time I found simple, useable and readable crypto libs made for casual users. |
This comment has been minimized.
This comment has been minimized.
hm, not so easy then expected. even your code use await internally it returns a promise, not a string. So i can't simply do an
looks like I have to convert over everything to async :-(, because it seems not easy to getb the value a promise from for not async code ... |
This comment has been minimized.
This comment has been minimized.
You can replace: const ctUint8 = new Uint8Array(ctStr.match(/./g).map(ch => ch.charCodeAt(0))); // ciphertext as Uint8Array ...with: const ctUint8 = new Uint8Array(ctStr.match(/[\s\S]/g).map(ch => ch.charCodeAt(0))); // ciphertext as Uint8Array See also https://stackoverflow.com/questions/1068280/javascript-regex-multiline-flag-doesnt-work |
This comment has been minimized.
This comment has been minimized.
@majodi, thanks for picking up the case where the ciphertext includes line terminators, and @sohrabsaran for providing a nice, clean fix. @sohrabsaran, I've added a (MIT) licence notice. As far as I'm aware, AES-256-GCM is pretty much tops. I'm afraid I can't help with asymmetric key encryption. @gnadelwartz I've added an alternative syntax to the examples using |
This comment has been minimized.
This comment has been minimized.
Amazing gist, thanks a ton! I'd like to add a note, that Safari 10 (and iOS <11) does not support AES-GCM, but AES-CBC. To use AES-CBC the initializationVector (iv) needs to be a |
This comment has been minimized.
This comment has been minimized.
Thank you for sharing. I am trying to convert this to AES-CTR but having some problems getting the decoding correct: /**
* Encrypts plaintext using AES-CTR with supplied password, for decryption with aesCtrDecrypt().
* (c) Chris Veness MIT Licence
*
* @param {String} plaintext - Plaintext to be encrypted.
* @param {String} password - Password to use to encrypt plaintext.
* @returns {String} Encrypted ciphertext.
*/
async function aesCtrEncrypt(plaintext, password) {
const data = new TextEncoder().encode(plaintext);
const iv = crypto.getRandomValues(new Uint8Array(16)); // get 120-bit random iv
const alg = {name: "AES-CTR", counter: iv, length: 128};
console.log('iv', iv);
// Generate key
// const key = await crypto.subtle.generateKey(
// {
// name: "AES-CTR",
// length: 256, //can be 128, 192, or 256
// },
// false, //whether the key is extractable (i.e. can be used in exportKey)
// ["encrypt", "decrypt"] //can "encrypt", "decrypt", "wrapKey", or "unwrapKey"
// );
// Use provided key
const pwUtf8 = new TextEncoder().encode(password); // encode password as UTF-8
const pwHash = await crypto.subtle.digest('SHA-256', pwUtf8); // hash the password
const key = await crypto.subtle.importKey('raw', pwHash, alg, false, ['encrypt']); // generate key from pw
const ctBuffer = await crypto.subtle.encrypt(alg, key, data)
const ctArray = Array.from(new Uint8Array(ctBuffer)); // ciphertext as byte array
const ctStr = ctArray.map(byte => String.fromCharCode(byte)).join(''); // ciphertext as string
const ctBase64 = btoa(ctStr); // encode ciphertext as base64
const ivHex = Array.from(iv).map(b => ('00' + b.toString(16)).slice(-2)).join(''); // iv as hex string
return ivHex+ctBase64; // return iv+ciphertext
}
/**
* Decrypts ciphertext encrypted with aesGcmEncrypt() using supplied password.
* (c) Chris Veness MIT Licence
*
* @param {String} ciphertext - Ciphertext to be decrypted.
* @param {String} password - Password to use to decrypt ciphertext.
* @returns {String} Decrypted plaintext.
*
* @example
* const plaintext = await aesGcmDecrypt(ciphertext, 'pw');
* aesGcmDecrypt(ciphertext, 'pw').then(function(plaintext) { console.log(plaintext); });
*/
async function aesCtrDecrypt(ciphertext, password) {
const pwUtf8 = new TextEncoder().encode(password); // encode password as UTF-8
const pwHash = await crypto.subtle.digest('SHA-256', pwUtf8); // hash the password
const iv = ciphertext.slice(0,32).match(/.{2}/g).map(byte => parseInt(byte, 16)); // get iv from ciphertext
console.log('iv', iv);
const alg = { name: 'AES-CTR', counter: new Uint8Array(iv), length: 128 }; // specify algorithm to use
const key = await crypto.subtle.importKey('raw', pwHash, alg, false, ['decrypt']); // use pw to generate key
console.log('ciphertext.slice(32)', ciphertext.slice(32));
const ctStr = atob(ciphertext.slice(32)); // decode base64 ciphertext
const ctUint8 = new Uint8Array(ctStr.match(/\s\S/g).map(ch => ch.charCodeAt(0))); // ciphertext as Uint8Array
// note: why doesn't ctUint8 = new TextEncoder().encode(ctStr) work?
const plainBuffer = await crypto.subtle.decrypt(alg, key, ctUint8); // decrypt ciphertext using key
const plaintext = new TextDecoder().decode(plainBuffer); // decode password from UTF-8
return plaintext; // return the plaintext
}
async function run() {
const plaintext = "This my secret message";
console.log(plaintext);
const password = crypto.getRandomValues(new Uint8Array(32));
console.log('password', password);
let ciphertext = await aesCtrEncrypt(plaintext, password);
console.log("ciphertext", ciphertext);
// Decrypt
let decryptedText = await aesCtrDecrypt(ciphertext, password);
console.log('decryptedText', decryptedText);
}
run(); After I fix this I'll need to add a SHA2 (512bit) HMAC to authenticate the ciphertext. |
This comment has been minimized.
This comment has been minimized.
I think there is a problem with the regex here: |
This comment has been minimized.
This comment has been minimized.
The new regex pattern fixed the problem I had with the gist. |
This comment has been minimized.
This comment has been minimized.
The code uses SHA-256 to create a hash from the password. If we look at how 7-zip etc. works, it takes the user-given password and hashes it several thousand times to increase the time it will take an attacker to use brute force to guess the user-given password. On further reading, it looks like you cannot just do the hashing just like that- you need to preserve the 'entropy' of the original user-given password. There are algorithms such as PBKDF2 and Argon. See https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveKey |
This comment has been minimized.
This comment has been minimized.
Regexp updated. Oops! |
This comment has been minimized.
This comment has been minimized.
@sohrabsaran I don't know anything about how 7-zip etc work. I believe repeatedly hashing a password does not necessarily increase entropy; for storing passwords for e.g. interactive logins, key derivation functions such as bcrypt or scrypt are a better solution. Correct me if I am wrong, but I think using a password to obtain a key for AES encryption is an entirely different beast. But in any case, you are welcome to improve the security, I just wanted to illustrate the use of the SubtleCrypto interface of the Web Cryptography API, as it is not necessarily immediately evident how to string the different elements together, from documents I found. |
This comment has been minimized.
This comment has been minimized.
It seems nodejs has replaced the crypto package with a dud=blank and their's included is not compatible, which ultimately led me here.
Well I guess I'm asking is there a simple update to modernize this great helpful example to show compatible node server side methods with the nodejs crypto https://nodejs.org/api/crypto.html ? Note: it seems to find TextEncoder() without any linked library, (why it requires 'new' seems wrong to me.) Maybe the comment about using PBKDF2 is better if one is moving forward? And on node 11.6 crypto importKey also comes up undefined not found. |
This comment has been minimized.
This comment has been minimized.
Okay well my apologies for being worked up about this. After much deliberation and frustration I have documented my current suggestions and thoughts to hopefully help bring clarity to others. In the link below I merely suggest to NodeJS Documentation that they consider putting an example of what matches up on the browser side with their new improved crypto in NodeJS 11.7 (which stomped the npm package I had before with a dumby blank). It's really the browser SubtleCrypto that could be encrypting a string if passed and not need ArrayBuffers etc. but it's also less node dependent as node is browser related (for it to be more up to them to document it). |
This comment has been minimized.
This comment has been minimized.
Lines 7 and 42 should be |
This comment has been minimized.
This comment has been minimized.
Not sure if it's relevant, but received the typescript warning that Changed it to
Same a bit further up
|
This comment has been minimized.
This comment has been minimized.
I heartily recommend throwing an error instead of providing a default. Please refer to my implementation (inspired by this gist): https://github.com/dipasqualew/keylocal/blob/master/src/strategy/web/aes-gcp.ts
|
This comment has been minimized.
This comment has been minimized.
@chrisveness thanks so much for this. What is the purpose of slicing at const arr = [212, 242, 255, 184, 247, 93, 182, 42, 148, 68, 242, 103]
arr.map(b => ('00' + b.toString(16)).slice(-2))
// returns ["d4", "f2", "ff", "b8", "f7", "5d", "b6", "2a", "94", "44", "f2", "67"]
arr.map(b => b.toString(16))
// returns ["d4", "f2", "ff", "b8", "f7", "5d", "b6", "2a", "94", "44", "f2", "67"] Why is each byte string padded with '00' only for these two characters to be removed? edit now I'm starting to think this a safety precaution of some kind.. edit 2 after testing more, it looks like the '00' is absolutely needed, but not sure exactly why that is. |
This comment has been minimized.
This comment has been minimized.
TextEncoder is available in Node v14 (and possibly earlier): https://nodejs.org/docs/latest-v14.x/api/util.html#util_class_util_textencoder And also the Web Crypto API in Node v15: https://nodejs.org/api/webcrypto.html |
This comment has been minimized.
Cryptography is indeed subtle. If I have made any errors, let me know and I will attempt to correct.
Thx to Tim Taubert @ Mozilla.