Created
January 17, 2021 20:26
-
-
Save antydemant/ad29542c53b7e7047b8ad1a808f3c8d1 to your computer and use it in GitHub Desktop.
Encryption demo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ | |
/* 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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><script src="/js/aes.js"></script><br><script src="/js/aes-ctr.js"></script></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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ | |
/* 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