cycle-crypt
Version:
Variable size symmetric key encryption algorithm
371 lines (344 loc) • 11.1 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.cycleCrypt = factory());
})(this, (function () { 'use strict';
/**
* Convert different types of JavaScript String to/from Uint8Array.
*
* @author Dumitru Uzun (DUzun.Me)
* @version 0.2.2
*/
/*requires Uint8Array*/
/*globals escape, unescape, encodeURI, decodeURIComponent, btoa*/
var chr = String.fromCharCode;
function buffer2bin(buf) {
buf = view8(buf);
return chr.apply(String, buf);
}
/**
* Get the hex representation of a buffer (TypedArray)
*
* @requires String.prototype.padStart()
*
* @param {TypedArray} buf Uint8Array is desirable, cause it is consistent regardless of the endianness
*
* @return {String} The hex representation of the buf
*/
function buffer2hex(buf) {
var bpe = buf.BYTES_PER_ELEMENT << 1;
return buf.reduce(function (r, c) {
return r += (c >>> 0).toString(16).padStart(bpe, '0');
}, '');
}
function buffer2str(buf, asUtf8) {
if (typeof buf == 'string') return buf;
buf = buffer2bin(buf);
if (asUtf8 !== false && !isASCII(buf)) {
if (asUtf8) {
buf = utf8Decode(buf);
} else if (asUtf8 == undefined) {
try {
buf = utf8Decode(buf);
} catch (err) {}
}
}
return buf;
}
function str2buffer(str, asUtf8) {
str = String(str);
if (asUtf8 == undefined) {
// Some guessing
asUtf8 = hasMultibyte(str); // || !isASCII(str)
}
if (asUtf8) {
str = utf8Encode(str);
}
// Smaller x2
// return new Uint8Array(String(str).split('').map(ord));
// Faster x3-4
var len = str.length;
var buf = new Uint8Array(len);
while (len--) {
buf[len] = str.charCodeAt(len);
}
return buf;
}
/**
* This method is a replacement of Buffer.toString(enc)
* for Browser, where Buffer is not available.
*
* @requires btoa
*
* @this {Uint8Array}
*
* @param {String} enc 'binary' | 'hex' | 'base64' | 'utf8' | undefined
*
* @return {String}
*/
function toString(enc) {
// The Node.js equivalent would be something like:
// if(typeof Buffer == 'function') {
// if(enc === false) enc = 'binary';
// if(enc === true) enc = 'utf8';
// return Buffer.from(this.buffer, this.byteOffset, this.byteLength).toString(enc);
// }
switch (enc) {
case false:
case 'binary':
return buffer2bin(this);
case 'hex':
return buffer2hex(this);
case 'base64':
return btoa(buffer2bin(this));
case 'utf8':
enc = true;
break;
}
return buffer2str(this, enc);
}
function view8(buf, start, len) {
// If buf is a Buffer, we still want to make it an Uint8Array
if (!start && !len && buf instanceof Uint8Array && !buf.copy) return buf;
start = start >>> 0;
if (len == undefined) len = buf.byteLength - start;
return new Uint8Array(buf.buffer, buf.byteOffset + start, len);
}
var hasMultibyteRE = /([^\x00-\xFF])/;
var isASCIIRE = /^[\x00-\x7F]*$/;
function hasMultibyte(str) {
var m = hasMultibyteRE.exec(str);
return m ? m[1] : false;
}
function isASCII(str) {
return isASCIIRE.test(str);
}
function utf8Encode(str) {
return unescape(encodeURI(str));
}
function utf8Decode(str) {
return decodeURIComponent(escape(str));
}
/*requires Uint8Array, Uint32Array*/
function randomBytes(size) {
var bits = -1 >>> 0;
var len = size & 3;
len = len ? size + 4 - len : size;
var ret = new Uint8Array(len);
var words = new Uint32Array(ret.buffer);
var ent = Date.now();
len >>= 2;
while (len--) {
words[len] = ent ^= Math.random() * bits;
}
if (ret.length > size) {
ret = ret.slice(0, size);
}
ret.toString = toString;
return ret;
}
/*requires Uint8Array, Uint32Array*/
/**
* Variable size symmetric key encryption algorithm.
* The cipher-key is generated by cycling the input key with a variation of XorShift+ PRNG.
* The bigger the key-size, the longer the period.
*
* @param {String|ArrayBuffer} key The encryption key
* @param {String|ArrayBuffer|Boolean} salt
* If a string, use it as salt.
* If TRUE, generate salt.
* @param {Number} saltRounds - Number of rounds of initial state generated from salt ⊕ key
*/
function CycleCrypt(key, salt, saltRounds) {
var self = this;
key = str2buf(key);
if (salt === true || salt === undefined) {
salt = randomBytes(Math.min(256, key.byteLength << 1));
}
self.salt = salt;
self._key = key = saltKey(key, str2buf(salt), saltRounds);
}
Object.defineProperties(CycleCrypt.prototype, {
constructor: {
value: CycleCrypt
},
/**
* Encrypt/decrypt the data and advance the internal state of the cipher-key.
*
* @param {String|ArrayBuffer} data Data to encrypt
*
* @return Uint8Array The encrypted/decrypted data
*/
_: {
value: function _(data) {
data = str2buf(data);
var _data = data,
dataLength = _data.dataLength;
if (!dataLength) return data;
var key = this._key;
var klen = key.length;
var len = data.length;
for (var i = 0, k = 0; i < len; ++i, ++k === klen && (k = 0)) {
if (!k) mixKey(key);
data[i] ^= key[k];
}
data = view8(data, 0, dataLength);
data.toString = toString;
return data;
}
},
/**
* Get the number of bytes in the key
*
* @return int
*/
keyByteSize: {
get: function get() {
return this._key.byteLength;
}
}
});
// Static methods:
CycleCrypt.randomBytes = randomBytes; // Uint8Array
CycleCrypt.toString = toString;
CycleCrypt.str2buffer = str2buffer; // Uint8Array
CycleCrypt.str2buf = str2buf; // Uint32Array
// // CycleCrypt.buf2str = buf2str;
/**
* Use a variation of Xorshift+ to mix the key
*
* @param Uint32Array $key List of int32 values representing the key
* @param int $rounds Number of rounds to process the key
*
* @return array A mixed key
*/
function mixKey(key, rounds) {
var len = key.length;
var k = len > 1 ? key[len - 1] : 0;
if (rounds == undefined) rounds = 1;
while (rounds-- > 0) {
for (var $i = len; $i--;) {
var ki = $i % len;
k = key[ki] + k;
k ^= k << 13; // 19
k ^= k >>> 17; // 15
k ^= k << 5; // 27
// k >>>= 0;
key[ki] = k;
}
}
return key;
}
/**
* Use a variation of Xorshift+ to salt the key
*
* @param Uint32Array $key
* @param Uint32Array $salt
* @param int $rounds Number of rounds to mix the key
*
* @return array A mixed key
*/
function saltKey(key, salt, rounds) {
var klen = key.length;
var slen = salt.length;
if (!slen) return key;
// make a copy to avoid altering the input salt
salt = salt.slice();
var k = klen > 1 ? key[klen - 1] : 0;
var s = slen > 1 ? salt[slen - 1] : 0;
if (rounds == undefined) rounds = 1;
while (rounds-- > 0) {
for (var i = Math.max(klen, slen); i--;) {
var ki = i % klen;
var si = i % slen;
k = key[ki] + k;
s = salt[si] + s;
s ^= s << 13; // 19
s ^= s >>> 7; // 25
k ^= k << 11; // 21
k ^= k >>> 8; // 24
// s >>>= 0;
k += s;
// k >>>= 0;
key[ki] = k;
salt[si] = s;
}
}
return key;
}
function str2buf(str) {
if (!str || typeof str.byteLength != 'number') {
str = str2buffer(str);
} else {
str = view8(str);
}
// Make sure the new buffer has a multiple of 4 byteLength
var _str = str,
byteLength = _str.byteLength;
var b = byteLength & 3;
var i;
// if(b) {
b = byteLength + (b && 4 - b);
i = new Uint8Array(b);
i.set(str);
// }
// else {
// b = byteLength;
// i = str.slice();
// }
str = new Uint32Array(i.buffer, i.byteOffset, b >> 2);
str.dataLength = byteLength;
return str;
}
// Unused
// function buf2str(buf) {
// return buffer2str(view8(buf));
// }
// ---------------------------------------------------------------
cycleCrypt.CycleCrypt = CycleCrypt;
cycleCrypt.randomBytes = CycleCrypt.randomBytes;
cycleCrypt.str2buffer = CycleCrypt.str2buffer;
// ---------------------------------------------------------------
/**
* Simple encryption using xor, a key and salt.
*
* @param {String|Uint8Array} $key The encryption key
* @param {String|Uint8Array} $data Data to encrypt
* @param {String|Uint8Array|bool} $salt
* If a string, use it as salt.
* If TRUE, generate salt and prepend it to the encrypted data.
* If FALSE, get the salt from the data.
* @param {Number} saltRounds - Number of rounds of initial state generated from salt ⊕ key
*
*
* @return Uint8Array The encrypted data. If $salt is TRUE, the generated salt is prepended to the result.
*/
function cycleCrypt(key, data) {
var salt = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
var saltRounds = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 1;
// Read salt from input
if (salt === false) {
data = view8(typeof data == 'string' ? str2buffer(data) : data);
var i = data[0];
salt = data.slice(1, ++i);
data = data.slice(i);
} else if (salt === undefined) {
salt = true;
}
var cc = new CycleCrypt(key, salt, saltRounds);
data = cc._(data);
// Add the generated salt to the output
if (salt === true) {
salt = cc.salt;
var ret = new Uint8Array(1 + salt.byteLength + data.byteLength);
ret[0] = salt.byteLength;
ret.set(salt, 1);
ret.set(data, 1 + ret[0]);
ret.toString = data.toString;
data = ret;
}
return data;
}
return cycleCrypt;
}));
//# sourceMappingURL=cycle-crypt.js.map