Skip to content

Instantly share code, notes, and snippets.

@jlinoff
Created April 5, 2017 02:01
Show Gist options
  • Save jlinoff/7d6cb5b5d7f92f28fc54270884b964d7 to your computer and use it in GitHub Desktop.
Save jlinoff/7d6cb5b5d7f92f28fc54270884b964d7 to your computer and use it in GitHub Desktop.
Javascript ES6 class that does aes-256-cbc encryption using the Google closure library that matches openssl.
// ================================================================
// This must be included before index.js to make sure that the
// Google closure libraries are loaded correctly.
// <script type="text/javascript" src="js/closure-library/closure/goog/base.js"></script>
// <script type="text/javascript" src="js/inc.js"></script>
// <script type="text/javascript" src="js/index.js"></script>
// ================================================================
goog.require('goog.array')
goog.require('goog.asserts')
goog.require('goog.color')
goog.require('goog.crypt')
goog.require('goog.cssom')
goog.require('goog.date')
goog.require('goog.db')
goog.require('goog.debug')
goog.require('goog.dom')
goog.require('goog.events')
goog.require('goog.format')
goog.require('goog.fs')
goog.require('goog.functions')
goog.require('goog.fx')
goog.require('goog.graphics')
goog.require('goog.iter')
goog.require('goog.json')
goog.require('goog.locale')
goog.require('goog.log')
goog.require('goog.math')
goog.require('goog.memoize')
goog.require('goog.messaging')
goog.require('goog.module')
goog.require('goog.object')
goog.require('goog.positioning')
goog.require('goog.proto')
goog.require('goog.reflect')
goog.require('goog.result')
goog.require('goog.soy')
goog.require('goog.string')
goog.require('goog.structs')
goog.require('goog.style')
goog.require('goog.testing')
goog.require('goog.tweak')
goog.require('goog.vec')
goog.require('goog.webgl')
goog.require('goog.window')
// Stuff used locally.
goog.require('goog.crypt.Md5')
goog.require('goog.crypt.Aes')
goog.require('goog.crypt.Cbc')
goog.require('goog.crypt.base64');
<!DOCTYPE html>
<html>
<head>
<title>Text</title>
<meta charset="UTF-8">
<!-- JS -->
<script type="text/javascript" src="js/closure-library/closure/goog/base.js"></script>
<script type="text/javascript" src="js/inc.js"></script>
<script type="text/javascript" src="js/index.js"></script>
</head>
<body>
<p>See the console for the output.</p>
</body>
</html>
// ================================================================
// Author: Joe Linoff
// Release: v0.1.0
// Copyright: copyright (c) 2017 by Joe Linoff
// License: MIT Open Source
// ================================================================
var PassMan = {}
PassMan.Aes256Cbc = class {
constructor() {
this.m_keySize = 32;
this.m_ivSize = 16;
this.m_blockSize = this.m_ivSize;
}
/**
* Encrypt plaintext using aes-256-cbc.
*
* The encrypted text can be decrypted by openssl using this command.
*
* echo -n "$encrypted" openssl aes-256-cbc -e -a -salt -pass pass:<password>
*
* The encrypted string can be broken into MIME encoded strings like this:
* var obj = new Passman();
* var password = 'secret';
* var e = obj.encrypt(plaintext, password);
* var s = e.match(/.{1,64}/g).join('\n');
* console.log('encrypt data:\n' + s);
*
* @param plaintext The plaintext string to encrypt.
* @param password The password string.
* @returns The encrypted string.
*/
encrypt(plaintext, password) {
var plaintextBytes = goog.crypt.stringToByteArray(plaintext);
var plaintextBytesPadded = this.pkcs7Encode(plaintextBytes, this.m_ivSize);
var passwordBytes = goog.crypt.stringToByteArray(password);
var saltPrefixBytes = goog.crypt.stringToByteArray('Salted__'); // openssl compatibility
var saltBytes = this.getRandomBytes(8);
var ki = this.getKeyAndIV(passwordBytes, saltBytes);
var keyBytes = ki[0];
var initialVectorBytes = ki[1];
var aes = new goog.crypt.Aes(keyBytes);
var cbc = new goog.crypt.Cbc(aes);
var ciphertextBytes = cbc.encrypt(plaintextBytesPadded, initialVectorBytes);
var ciphertext = goog.crypt.byteArrayToHex(ciphertextBytes);
// Openssl compatibility.
var encryptedBytes = [...saltPrefixBytes, ...saltBytes, ...ciphertextBytes];
var encrypted = goog.crypt.base64.encodeByteArray(encryptedBytes);
return encrypted;
}
/**
* Decrypt a string that was encrypted using aes-256-cbc.
*
* It is the same as running:
* echo -n "$encryted" | openssl aes-256-cbc -d -a -salt -pass pass:<password>
*
* @param encrypted The encryted string.
* @param password The password string.
* @returns The plaintext string.
*/
decrypt(encrypted, password) {
var passwordBytes = goog.crypt.stringToByteArray(password);
var encryptedBytes = goog.crypt.base64.decodeStringToByteArray(encrypted);
var saltBytes = encryptedBytes.slice(8, this.m_ivSize);
var ciphertextBytesPadded = encryptedBytes.slice(this.m_ivSize);
var ki = this.getKeyAndIV(passwordBytes, saltBytes);
var keyBytes = ki[0];
var initialVectorBytes = ki[1];
var aes = new goog.crypt.Aes(keyBytes);
var cbc = new goog.crypt.Cbc(aes);
var plaintextBytesPadded = cbc.decrypt(ciphertextBytesPadded, initialVectorBytes);
var plaintextBytes = this.pkcs7Decode(plaintextBytesPadded);
var plaintext = goog.crypt.byteArrayToString(plaintextBytes);
return plaintext;
}
/**
* Simple wrapper to get the MD5 digest value for an array of bytes.
*
* @param bytes The array of bytes.
* @returns The 16 byte MD5 hash digest.
*/
getMd5Digest(bytes) {
var md5 = new goog.crypt.Md5();
md5.update(bytes);
return md5.digest();
}
/**
* Get the openssl value compatible key and IV.
*
* Note that for openssl compatibility the salt must
* be 8 bytes because a 'Salted__' prefix is pre-pended.
*
* @param passwordBytes The password as an array of bytes.
* @param saltBytes The salt as an array of bytes.
* @returns The key and iv in a two element array.
*/
getKeyAndIV(passwordBytes, saltBytes) {
var ps = [...passwordBytes, ...saltBytes];
var digest = this.getMd5Digest(ps);
var keyiv = digest;
var last = digest;
var m = this.m_keySize + this.m_ivSize;
while (keyiv.length < m) {
digest = this.getMd5Digest([...last, ...ps]);
keyiv = [...keyiv, ...digest];
last = digest
}
var key = keyiv.slice(0, this.m_keySize);
var iv = keyiv.slice(this.m_keySize);
return [key, iv];
}
/**
* Get an array of random bytes in the range [0..255].
*
* @param size The size of the array.
* @returns The array.
*/
getRandomBytes(size) {
var result = [];
for (var i=0; i<size; i++) {
result.push(this.getRandomByte());
}
return result;
}
/**
* Get a single random byte in the range [0..255].
*
* @returns A random value in the range [0..255].
*/
getRandomByte() {
return Math.floor(Math.random() * 256);
}
/**
* Encode a message according to the PKCS#7 standard (RFC-2315).
* Simply put, pad the message with the number of bytes needed to
* align the message on the size boundary. The value of each
* appended byte is the value of the number of pad bytes. This
* means that size must never exceed 255.
*
* @param unpadded The byte array to pad.
* @param size The size of align on. Maximum value cannot exceed 255.
* @returns The padded array.
*/
pkcs7Encode(unpadded, size) {
var padded = unpadded.slice();
var padval = size - (unpadded.length % size);
for (var i=0; i<padval; i++) {
padded.push(padval);
}
return padded;
}
/**
* Decode a message according to the PKCS#7 standard (RFC-2315).
* All we do is get the entry in the padded array and use that
* valute to truncate the array.
*
* @param padded The byte array to unpad.
* @returns The unpadded byte array.
*/
pkcs7Decode(padded) {
var unpadded = padded.slice();
var padval = padded[padded.length - 1];
for (var i=0; i<padval; i++) {
unpadded.pop();
}
return unpadded;
}
}
let aes = new PassMan.Aes256Cbc();
var plaintext = `
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
culpa qui officia deserunt mollit anim id est laborum.
`;
var password = 'secret';
console.log('\n------------------------\npassword: (' + password.length + ')\n' + password);
console.log('\n------------------------\nplaintext: (' + plaintext.length + ')\n' + plaintext);
var ct = aes.encrypt(plaintext, password);
console.log('\n------------------------\nct: (' + ct.length + ')\n' + ct);
var s = ct.match(/.{1,64}/g).join('\n');
console.log('\n------------------------\nencrypted MIME data:\n' + s);
var pt = aes.decrypt(ct, password);
console.log('\n------------------------\npt: (' + pt.length + ')\n' + pt);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment