Skip to content

Instantly share code, notes, and snippets.

@antydemant
Created January 17, 2021 20:26
Show Gist options
  • Save antydemant/ad29542c53b7e7047b8ad1a808f3c8d1 to your computer and use it in GitHub Desktop.
Save antydemant/ad29542c53b7e7047b8ad1a808f3c8d1 to your computer and use it in GitHub Desktop.
Encryption demo
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* AES Counter-mode implementation in JavaScript (c) Chris Veness 2005-2014 / MIT Licence */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* jshint node:true *//* global define, escape, unescape, btoa, atob */
'use strict';
if (typeof module != 'undefined' && module.exports) var Aes = require('./aes'); // CommonJS (Node.js)
function textTo16Binary(string) {
return string.split('').map(function (char) {
return char.charCodeAt(0).toString(16);
}).join(' ');
}
function binary26toText(string) {
return string.split(' ').map(function (char) {
return String.fromCharCode(parseInt(char, 16));
}).join('');
}
/**
* Aes.Ctr: Counter-mode (CTR) wrapper for AES.
*
* This encrypts a Unicode string to produces a base64 ciphertext using 128/192/256-bit AES,
* and the converse to decrypt an encrypted ciphertext.
*
* See http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf
*
* @augments Aes
*/
Aes.Ctr = {};
/**
* Encrypt a text using AES encryption in Counter mode of operation.
*
* Unicode multi-byte character safe
*
* @param {string} plaintext - Source text to be encrypted.
* @param {string} password - The password to use to generate a key for encryption.
* @param {number} nBits - Number of bits to be used in the key; 128 / 192 / 256.
* @returns {string} Encrypted text.
*
* @example
* var encr = Aes.Ctr.encrypt('big secret', 'pāşšŵōřđ', 256); // 'lwGl66VVwVObKIr6of8HVqJr'
*/
Aes.Ctr.encrypt = function (plaintext, password, nBits) {
var blockSize = 8; // block size fixed at 16 bytes / 128 bits (Nb=4) for AES
if (!(nBits == 128 || nBits == 192 || nBits == 256)) return ''; // standard allows 128/192/256 bit keys
plaintext = String(plaintext).utf8Encode();
password = String(password).utf8Encode();
// use AES itself to encrypt password to get cipher key (using plain password as source for key
// expansion) - gives us well encrypted key (though hashed key might be preferred for prod'n use)
var nBytes = nBits / 8; // no bytes in key (16/24/32)
var pwBytes = new Array(nBytes);
for (var i = 0; i < nBytes; i++) { // use 1st 16/24/32 chars of password for key
pwBytes[i] = isNaN(password.charCodeAt(i)) ? 0 : password.charCodeAt(i);
}
var key = Aes.cipher(pwBytes, Aes.keyExpansion(pwBytes)); // gives us 16-byte key
key = key.concat(key.slice(0, nBytes - 16)); // expand key to 16/24/32 bytes long
// initialise 1st 8 bytes of counter block with nonce (NIST SP800-38A §B.2): [0-1] = millisec,
// [2-3] = random, [4-7] = seconds, together giving full sub-millisec uniqueness up to Feb 2106
var counterBlock = new Array(blockSize);
var nonce = (new Date()).getTime(); // timestamp: milliseconds since 1-Jan-1970
var nonceMs = nonce % 1000;
var nonceSec = Math.floor(nonce / 1000);
var nonceRnd = Math.floor(Math.random() * 0xffff);
// for debugging: nonce = nonceMs = nonceSec = nonceRnd = 0;
for (var i = 0; i < 2; i++) counterBlock[i] = (nonceMs >>> i * 8) & 0xff;
for (var i = 0; i < 2; i++) counterBlock[i + 2] = (nonceRnd >>> i * 8) & 0xff;
for (var i = 0; i < 4; i++) counterBlock[i + 4] = (nonceSec >>> i * 8) & 0xff;
// and convert it to a string to go on the front of the ciphertext
var ctrTxt = '';
for (var i = 0; i < 8; i++) ctrTxt += String.fromCharCode(counterBlock[i]);
// generate key schedule - an expansion of the key into distinct Key Rounds for each round
var keySchedule = Aes.keyExpansion(key);
var blockCount = Math.ceil(plaintext.length / blockSize);
var ciphertxt = new Array(blockCount); // ciphertext as array of strings
for (var b = 0; b < blockCount; b++) {
// set counter (block #) in last 8 bytes of counter block (leaving nonce in 1st 8 bytes)
// done in two stages for 32-bit ops: using two words allows us to go past 2^32 blocks (68GB)
for (var c = 0; c < 4; c++) counterBlock[15 - c] = (b >>> c * 8) & 0xff;
for (var c = 0; c < 4; c++) counterBlock[15 - c - 4] = (b / 0x100000000 >>> c * 8);
var cipherCntr = Aes.cipher(counterBlock, keySchedule); // -- encrypt counter block --
// block size is reduced on final block
var blockLength = b < blockCount - 1 ? blockSize : (plaintext.length - 1) % blockSize + 1;
var cipherChar = new Array(blockLength);
for (var i = 0; i < blockLength; i++) { // -- xor plaintext with ciphered counter char-by-char --
cipherChar[i] = cipherCntr[i] ^ plaintext.charCodeAt(b * blockSize + i);
cipherChar[i] = String.fromCharCode(cipherChar[i]);
}
ciphertxt[b] = cipherChar.join('');
}
// use Array.join() for better performance than repeated string appends
var ciphertext = ctrTxt + ciphertxt.join('');
return {
ciphertext: textTo16Binary(ciphertext),
blocks: ciphertxt.map((block) => {
return textTo16Binary(block).split(' ').join('');
})
};
};
/**
* Decrypt a text encrypted by AES in counter mode of operation
*
* @param {string} ciphertext - Cipher text to be decrypted.
* @param {string} password - Password to use to generate a key for decryption.
* @param {number} nBits - Number of bits to be used in the key; 128 / 192 / 256.
* @returns {string} Decrypted text
*
* @example
* var decr = Aes.Ctr.decrypt('lwGl66VVwVObKIr6of8HVqJr', 'pāşšŵōřđ', 256); // 'big secret'
*/
Aes.Ctr.decrypt = function (ciphertext, password, nBits) {
var blockSize = 8; // block size fixed at 16 bytes / 128 bits (Nb=4) for AES
if (!(nBits == 128 || nBits == 192 || nBits == 256)) return ''; // standard allows 128/192/256 bit keys
ciphertext = binary26toText(ciphertext);
password = String(password).utf8Encode();
// use AES to encrypt password (mirroring encrypt routine)
var nBytes = nBits / 8; // no bytes in key
var pwBytes = new Array(nBytes);
for (var i = 0; i < nBytes; i++) {
pwBytes[i] = isNaN(password.charCodeAt(i)) ? 0 : password.charCodeAt(i);
}
var key = Aes.cipher(pwBytes, Aes.keyExpansion(pwBytes));
key = key.concat(key.slice(0, nBytes - 16)); // expand key to 16/24/32 bytes long
// recover nonce from 1st 8 bytes of ciphertext
var counterBlock = new Array(8);
var ctrTxt = ciphertext.slice(0, 8);
for (var i = 0; i < 8; i++) counterBlock[i] = ctrTxt.charCodeAt(i);
// generate key schedule
var keySchedule = Aes.keyExpansion(key);
// separate ciphertext into blocks (skipping past initial 8 bytes)
var nBlocks = Math.ceil((ciphertext.length - 8) / blockSize);
var ct = new Array(nBlocks);
for (var b = 0; b < nBlocks; b++) ct[b] = ciphertext.slice(8 + b * blockSize, 8 + b * blockSize + blockSize);
ciphertext = ct; // ciphertext is now array of block-length strings
// plaintext will get generated block-by-block into array of block-length strings
var plaintxt = new Array(ciphertext.length);
for (var b = 0; b < nBlocks; b++) {
// set counter (block #) in last 8 bytes of counter block (leaving nonce in 1st 8 bytes)
for (var c = 0; c < 4; c++) counterBlock[15 - c] = ((b) >>> c * 8) & 0xff;
for (var c = 0; c < 4; c++) counterBlock[15 - c - 4] = (((b + 1) / 0x100000000 - 1) >>> c * 8) & 0xff;
var cipherCntr = Aes.cipher(counterBlock, keySchedule); // encrypt counter block
var plaintxtByte = new Array(ciphertext[b].length);
for (var i = 0; i < ciphertext[b].length; i++) {
// -- xor plaintxt with ciphered counter byte-by-byte --
plaintxtByte[i] = cipherCntr[i] ^ ciphertext[b].charCodeAt(i);
plaintxtByte[i] = String.fromCharCode(plaintxtByte[i]);
}
plaintxt[b] = plaintxtByte.join('');
}
// join array of blocks into single plaintext string
var plaintext = plaintxt.join('');
plaintext = plaintext.utf8Decode(); // decode from UTF8 back to Unicode multi-byte chars
return plaintext;
};
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* Extend String object with method to encode multi-byte string to utf8
* - monsur.hossa.in/2012/07/20/utf-8-in-javascript.html
* - note utf8Encode is an identity function with 7-bit ascii strings, but not with 8-bit strings;
* - utf8Encode('x') = 'x', but utf8Encode('ça') = 'ça', and utf8Encode('ça') = 'ça'*/
if (typeof String.prototype.utf8Encode == 'undefined') {
String.prototype.utf8Encode = function () {
return unescape(encodeURIComponent(this));
};
}
/* Extend String object with method to decode utf8 string to multi-byte */
if (typeof String.prototype.utf8Decode == 'undefined') {
String.prototype.utf8Decode = function () {
try {
return decodeURIComponent(escape(this));
} catch (e) {
return this; // invalid UTF-8? return as-is
}
};
}
/* Extend String object with method to encode base64
* - developer.mozilla.org/en-US/docs/Web/API/window.btoa, nodejs.org/api/buffer.html
* - note: btoa & Buffer/binary work on single-byte Unicode (C0/C1), so ok for utf8 strings, not for general Unicode...
* - note: if btoa()/atob() are not available (eg IE9-), try github.com/davidchambers/Base64.js */
if (typeof String.prototype.base64Encode == 'undefined') {
String.prototype.base64Encode = function () {
if (typeof btoa != 'undefined') return btoa(this); // browser
if (typeof Buffer != 'undefined') return new Buffer(this, 'binary').toString('base64'); // Node.js
throw new Error('No Base64 Encode');
};
}
/* Extend String object with method to decode base64 */
if (typeof String.prototype.base64Decode == 'undefined') {
String.prototype.base64Decode = function () {
if (typeof atob != 'undefined') return atob(this); // browser
if (typeof Buffer != 'undefined') return new Buffer(this, 'base64').toString('binary'); // Node.js
throw new Error('No Base64 Decode');
};
}
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
if (typeof module != 'undefined' && module.exports) module.exports = Aes.Ctr; // ≡ export default Aes.Ctr
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>AES client/server test</title>
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/normalize/3.0.3/normalize.min.css">
<style>
body { font-size: 80%; padding: 1em; }
form { margin-top: 2em; }
label { display: inline-block; width: 6em; }
input { width: 36em; }
form ul { list-style: none; padding: 0; }
form li { margin: 0.5em 1em; }
code { display: inline-block; font-size: 80%; margin-left: 2em; }
</style>
</head>
<body>
<h1>JavaScript AES client/server interoperability test</h1>
<p>The same AES JavaScript files are used both client-side and server-side.</p>
<p>Client-side they are accessed by</p>
<code>&lt;script src="/js/aes.js"&gt;&lt;/script&gt;<br>&lt;script src="/js/aes-ctr.js"&gt;&lt;/script&gt;</code>
<p>Server-side they are accessed by</p>
<code>const Aes = require('./public/js/aes.js');<br>Aes.Ctr = require('./public/js/aes-ctr.js');</code>
<form method="post">
<fieldset><legend>Encrypt</legend>
<ul>
<li>
<label for="plaintext">Plaintext</label>
<input name="plaintext" id="plaintext" value="pssst ... đon’t tell anyøne!">
</li>
<li>
<label for="password">Password</label>
<input name="password" id="password" value="L0ck it up ŝaf3">
</li>
</ul>
<h2>Encrypt on server, decrypt on server</h2>
<ul>
{{#each blocks}}
<li>Block #{{@index}} - {{ this }}</li>
{{/each}}
</ul>
<p>Cipher text (encrypted on server): <span id="ciphertext-server">{{ciphertext}}</span></p>
<p>Plain text (decrypted on server): <output>{{plaintext}}</output></p>
<button type="submit">Submit</button>
</fieldset>
</form>
</body>
</html>
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* JavaScript AES client/server interoperability (c) Chris Veness 2016 / MIT Licence */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
'use strict';
const fs = require('fs'); // nodejs.org/api/fs.html
const connect = require('connect'); // simple middleware framework
const serveStatic = require('serve-static'); // serve static files
const bodyParser = require('body-parser'); // http request body parsing
const handlebars = require('handlebars'); // handlebars templating
const AesCtr = require('./public/js/aes-ctr.js');
const app = connect();
app.use(serveStatic('public')); // for .js files
app.use(bodyParser.urlencoded({ 'extended': false })); // parse request bodies into req.body
app.use(function processRequest(req, res, next) {
let context = null;
switch (req.method) {
case 'GET':
context = {};
break;
case 'POST':
const { ciphertext, blocks } = AesCtr.encrypt(req.body.plaintext, req.body.password, 256);
const plaintext = AesCtr.decrypt(ciphertext, req.body.password, 256);
context = { 'ciphertext': ciphertext.split(' ').join(''), 'plaintext': plaintext, 'blocks': blocks };
break;
}
const template = fs.readFileSync('index.html', 'utf-8');
const templateFn = handlebars.compile(template);
const html = templateFn(context);
res.setHeader('Content-Type', 'text/html');
res.end(html);
});
app.listen(process.env.PORT || 8080);
console.log('Listening on port ' + (process.env.PORT || 8080));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment