UNPKG

spritzjs

Version:

A Spritz stream-cipher implementation in JavaScript

624 lines (545 loc) 13.3 kB
/****************************************************************************** spritzjs - A Spritz stream-cipher implementation in JavaScript Created with reference to "Spritz - a spongy RC4-like stream cipher and hash function" by Ronald L. Rivest and Jacob C. N. Schuldt on 2014.10.27 Intent: Provide a literal JavaScript reference implementation from the excellent pseudo-code created by Rivest and Schuldt; allowing the reader to follow the Paper with this code serving as an accompaniment. To this end, variable/parameter names and their casing have been preserved, however the function names have been camelCased for JS convention. It is hoped this may serve as a starting point for those who wish to explore this cipher in a JS context. This file is deliberately unoptimized with the aim of readability, yet is *relatively* portable. (For example, "isArray" support is assumed rather than feature-detected, and may need replacing if you wish to run this on IE8-, or very old Firefox versions. See: http://kangax.github.io/compat-table/es5/#Array.isArray Tested in: Node.js (0.10.26+), Chrome (38+), Firefox (33+) References: "Spritz - a spongy RC4-like stream cipher and hash function" by Rivest and Schuldt - http://people.csail.mit.edu/rivest/pubs/RS14.pdf "Security Now" podcast, episode 479 (~21 mins) - http://twit.tv/show/security-now/479 "Security Now" podcast, episode 480 (~57 mins) - http://twit.tv/show/security-now/480 "Schneier on Security" - https://www.schneier.com/blog/archives/2014/10/spritz_a_new_rc.html For more information you can find the latest README.md, tests and other versions at: https://github.com/therealjampers/spritzjs Test-vectors appreciated! Released under the MIT license. - therealjampers 2014 ******************************************************************************/ (function () { "use strict"; /* N is apparently arbitrary, but we shall elect to use byte-size N-values throughout */ var N = 256 , N_MINUS_1 = N - 1 , N_OVER_TWO_FLOOR = Math.floor(N / 2) , TWO_N = 2 * N /* TODO: this is unused, merely a reminder to move to high-level facade/guards */ , MAX_ABSORB_LEN = Math.floor(N / 4) ; /* The state consists of byte registers i, j, k, z, w, and a, and an array S containing a permutation of {0, 1,... , N − 1}. */ var i , j , k , z , w , a , S ; /****************** core functions *******************/ /* InitializeState(N) 1 i = j = k = z = a = 0 2 w = 1 3 for v = 0 to N - 1 4 S[v] = v */ function initializeState() { // NB. N is set to 256 in the above code, so we are not receiving it as per literal spec var v; i = j = k = z = a = 0; w = 1; S = []; for (v = 0; v < N; v++) { S[v] = v; } } /* Absorb(I) 1 for v = 0 to I.length - 1 2 AbsorbByte(I[v]) */ function absorb(I) { var v; /* basic validation of I consider bounds-checking and deep-checking of each N-value such that they are in the valid range (0 <= x < N) */ if (!(Array.isArray(I) && I.length > 0)) return false; for (v = 0; v < I.length; v++) { absorbByte(I[v]); } return true; } /* AbsorbByte(b) 1 AbsorbNibble(low(b)) 2 AbsorbNibble(high(b)) */ function absorbByte(b) { absorbNibble(low(b)); absorbNibble(high(b)); } /* AbsorbNibble(x) 1 if a = N/2 2 Shuffle() 3 Swap(S[a],S[N/2+x]) 4 a=a+1 */ function absorbNibble(x) { if (a === N_OVER_TWO_FLOOR){ shuffle(); } swap(a, madd(N_OVER_TWO_FLOOR,x)); a = madd(a, 1); } /* AbsorbStop() 1 if a=N/2 2 Shuffle() 3 a=a+1 */ function absorbStop() { if (a === N_OVER_TWO_FLOOR){ shuffle(); } a = madd(a, 1); } /* Shuffle() 1 Whip(2N) 2 Crush() 3 Whip(2N) 4 Crush() 5 Whip(2N) 6 a=0 */ function shuffle() { whip(TWO_N); crush(); whip(TWO_N); crush(); whip(TWO_N); a = 0; } /* Whip(r) 1 for v=0 to r−1 2 Update() 3 do w=w+1 4 until gcd(w,N)=1 */ function whip(r) { var v; for (v = 0; v < r; v++) { update(); } do { w = madd(w, 1); } while (gcd(w, N) !== 1); // NB. a simple-case assumption is that if N is a power of 2 then one could instead use: // w = madd(w, 2); } /* Crush() 1 for v=0 to N/2−1 2 if S[v]>S[N−1−v] 3 Swap(S[v], S[N − 1 − v]) */ function crush() { var v , index ; for (v = 0; v < N_OVER_TWO_FLOOR; v++) { index = N_MINUS_1 - v; if (S[v] > S[index]) { swap(v, index); } } } /* Squeeze(r) 1 if a>0 2 Shuffle() 3 P = Array.New(r) 4 for v=0 to r−1 5 P[v] = Drip() 6 return P */ function squeeze(r) { var v , P ; if (a > 0) { shuffle(); } P = []; for (v = 0; v < r; v++) { P[v] = drip(); } return P; } /* Drip() 1 if a>0 2 Shuffle() 3 Update() 4 return Output() */ function drip() { if (a > 0) { shuffle(); } update(); return output(); } /* Update() 1 i=i+w 2 j=k+S[j+S[i]] 3 k=i+k+S[j] 4 Swap(S[i], S[j]) */ function update() { i = madd(i, w); j = madd(k, S[madd(j, S[i])]); k = madd(i + k, S[j]); swap(i, j); } /* Output() 1 z = S[j+S[i+S[z+k]]] 2 return z */ function output() { z = S[ madd(j, S[ madd(i, S[ madd(z, k) ]) ]) ]; return z; } /****************** high-level functions (target API) *******************/ /* Hash(M,r) 1 InitializeState() 2 Absorb(M ); AbsorbStop() 3 Absorb(r) 4 return Squeeze(r) */ function hash(M, r) { initializeState(); absorb(M); absorbStop(); absorb([r & 0xff]); // NB. restricted(!) to 255-byte hashes return squeeze(r); } /* Encrypt(K, M) 1 KeySetup(K) 2 C = M + Squeeze(M.length) 3 return C */ function encrypt(K, M) { var C = [] , stream , i ; keySetup(K); stream = squeeze(M.length); for (i = 0; i < M.length; i++) { C[i] = madd(M[i], stream[i]); // NB. this could be xor instead of modulo addition } return C; } /* Decrypt(K , C) 1 KeySetup(K) 2 M = C − Squeeze(M.length) <--- this should be C.length, IMHO 3 return M */ function decrypt(K, C) { var M = [] , stream , i ; keySetup(K); stream = squeeze(C.length); for (i = 0; i < C.length; i++) { M[i] = msub(C[i], stream[i]); // NB. this could be xor instead of modulo subtraction } return M; } /* EncryptWithIV(K, IV, M) 1 KeySetup(K); AbsorbStop() 2 Absorb(IV) 3 C = M + Squeeze(M.length) 4 return C */ function encryptWithIV(K, IV, M) { var C = [] , stream , i ; keySetup(K); absorbStop(); absorb(IV); stream = squeeze(M.length); for (i = 0; i < M.length; i++) { C[i] = madd(M[i], stream[i]); // NB. this could be xor instead of modulo addition } return C; } /* NB. This is not in the Paper, it is implied DecryptWithIV(K, IV, C) 1 KeySetup(K); AbsorbStop() 2 Absorb(IV) 3 M = C - Squeeze(C.length) 4 return M */ function decryptWithIV(K, IV, C) { var M = [] , stream , i ; keySetup(K); absorbStop(); absorb(IV); stream = squeeze(C.length); for (i = 0; i < C.length; i++) { M[i] = msub(C[i], stream[i]); // NB. this could be xor instead of modulo subtraction } return M; } /* KeySetup(K) 1 InitializeState() 2 Absorb(K) */ function keySetup(K) { /* TODO refactor into some common guard functions NB. we are striving for readibility and Paper->Code mapping but we can/should proxy the result of absorb when optimizing */ if (!(Array.prototype.slice.call(arguments).length === 1 && Array.isArray(K) && K.length > 0)) return false; initializeState(); absorb(K); return true; } /* DomHash(J,M,r) 1 InitializeState() 2 Absorb(J ); AbsorbStop() 3 Absorb(M ); AbsorbStop() 4 Absorb(r) 5 return Squeeze(r ) */ function domHash(J, M, r) { initializeState(); absorb(J); absorbStop(); absorb(M); absorbStop(); absorb(r); return squeeze(r); } /* MAC(K,M,r) 1 InitializeState() 2 Absorb(K ); AbsorbStop() 3 Absorb(M ); AbsorbStop() 4 Absorb(r) 5 return Squeeze(r ) */ function mac(K, M, r) { initializeState(); absorb(K); absorbStop(); absorb(M); absorbStop(); absorb(r); return squeeze(r); } /* TODO AEAD(K,Z,H,M,r) 1 InitializeState() 2 Absorb(K ); AbsorbStop() 3 Absorb(Z ); AbsorbStop() 4 Absorb(H ); AbsorbStop() 5 DivideMintoblocksM1,M2,...,Mt, each N/4 bytes long except possibly the last. 6 fori=1tot 7 Output Ci = Mi + Squeeze(Mi . length ) 8 Absorb(Ci ) 9 AbsorbStop() 6 10 11 Absorb(r) Output Squeeze(r ) */ /****************** utility functions *******************/ /* addition modulo N */ function madd(a, b) { return (a + b) % N; // return (a + b) & 0xff; // when N = 256, 0xff = N - 1 } /* subtraction modulo N. assumes a and b are in N-value range */ function msub(a, b) { return madd(N, a - b); } /* get the low nibble of b */ function low(b) { return b & 0xf; } /* get the high nibble of b */ function high(b) { /* zero-fill right shift chosen, however if there were bits back there we're in trouble anyway! */ return b >>> 4 & 0xf; } /* convenience function to swap S values at pointers p1 and p2 */ function swap(p1, p2) { var tmp = S[p1]; S[p1] = S[p2]; S[p2] = tmp; } /* Greatest Common Divisor - Euclid's algorithm gcd(a, 0) = a gcd(a, b) = gcd(b,a mod b) */ function gcd(a, b) { var t; while (b != 0) { t = b; b = a % b; a = t; } return a; } /* allow retrieval of the (initialized) internal cipher-state as a plain object NB. only for testing purposes, should not be exposed in "production" */ function getState() { /* basic guard against unitialized state */ if (!Array.isArray(S)) return false; return { i: i , j: j , k: k , z: z , w: w , a: a /* return the permutation by value not reference to avoid potential fubars TODO: check in Nodeland, though we are using primitive N-values in Arrays, and not Buffers */ , S: S.slice(0) }; } /* API definition NB. the optional facade construction now allows for "opt-in" exposure, guarding, and freezing of these :) */ var API = { /* high-level functions */ hash: hash , encrypt: encrypt , decrypt: decrypt , encryptWithIV: encryptWithIV , decryptWithIV: decryptWithIV , domHash: domHash , mac: mac /* core functions */ , keySetup: keySetup , initializeState: initializeState , absorb: absorb , absorbByte: absorbByte , absorbNibble: absorbNibble , absorbStop: absorbStop , shuffle: shuffle , whip: whip , crush: crush , squeeze: squeeze , drip: drip , update: update , output: output /* utility functions */ , getState: getState }; /* NB. breaking-change from 0.3.12, spritzjs is now a constructor with optional facade */ function ctor(facadeOptional) { // if there is a facade provided and the keys match API methods // then call the guard functions first and return the core results afterwards // reassign API to the modified facade // TODO: refactor at your own discretion, establish your support baseline and use bind etc. if (typeof facadeOptional === 'object') { for (var key in facadeOptional) { if (API.hasOwnProperty(key)) { facadeOptional[key] = (function (functionName, guardFunction, spritzjsFunction) { return function () { if (!guardFunction.apply({}, arguments)) return false; return spritzjsFunction.apply({}, arguments); }; }(key, facadeOptional[key], API[key])); } } API = facadeOptional; } /* Object.freeze (may) prevent trivial XSS modification of spritzjs: eg. spritzjs.hash=function(){return 'pwned'} // this shouldn't modify spritzjs.hash spritzjs.hash([65,66,67], 32) // therefore, still returns -> [2, 143, 162,...] */ return Object.freeze(API); } if(typeof module !== 'undefined' && module.exports){ /* export in CommonJS style */ module.exports = ctor; } else if (typeof window !== 'undefined' && !window.spritzjs) { /* augment window object */ window.spritzjs = ctor; } else { /* ...requires consumer-thought */ if (typeof console !== 'undefined') console.error("unable to export spritzjs ctor!"); } }());