diff --git a/spec/test/chachaSpec.js b/spec/test/chachaSpec.js new file mode 100644 --- /dev/null +++ b/spec/test/chachaSpec.js @@ -0,0 +1,52 @@ +/* global expect */ + +describe("Chacha",function(){ + let cryptoJS=require("../../dist/main.js"); + let util=cryptoJS.util; + let Chacha20=cryptoJS.Chacha20; + let encrypt=cryptoJS.encrypt; + let decrypt=cryptoJS.decrypt; + let str2utf8=util.str2utf8; + + let key=[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31]; + let nonce1=[0,0,0,9,0,0,0,74,0,0,0,0]; + let nonce2=[0,0,0,0,0,0,0,74,0,0,0,0]; + let plaintext="Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it."; + + describe("Chacha20",function(){ + it("should return a correct test vector",function(){ + let cipher=new Chacha20(key,nonce1); + let output=cipher._computeBlock().map(x=>x>>>0); + let expected=[0xe4e7f110,0x15593bd1,0x1fdd0f50,0xc47120a3,0xc7f4d1c7,0x0368c033,0x9aaa2204,0x4e6cd4c3,0x466482d2,0x09aa9f07,0x05d7c214,0xa2028bd9,0xd19c12b5,0xb94e16de,0xe883d0cb,0x4e3c50a2]; + expect(output).toEqual(expected); + }); + + it("should output a correct key stream",function(){ + let cipher=new Chacha20(key,nonce2); + let expected=[34,79,81,243,64,27,217,225,47,222,39,111,184,99,29,237,140,19,31,130,61,44,6, 226,126,79,202,236,158,243,207,120,138,59,10,163,114,96,10,146,181,121,116,205,237,43,147,52,121, 76,186,64,198,62,52,205,234,33,44,76,240,125,65,183,105,166,116,159,63,99,15,65,34,202,254,40,236, 77,196,126,38,212,52,109,112,185,140,115,243,233,197,58,196,12,89,69,57,139,110,218,26,131,44,137, 193,103,234,205,144,29,126,43,243,99]; + expected.forEach(b=>{ + expect(cipher.getByte()).toEqual(b); + }); + }); + + }); + + describe("encrypt",function(){ + it("should correctly encrypt an example text",function(){ + let ciphertext=[110,46,53,154,37,104,249,128,65,186,7,40,221,13,105,129,233,126,122,236,29, 67,96,194,10,39,175,204,253,159,174,11,249,27,101,197,82,71,51,171,143,89,61,171,205,98,179,87,22, 57,214,36,230,81,82,171,143,83,12,53,159,8,97,216,7,202,13,191,80,13,106,97,86,163,142,8,138,34, 182,94,82,188,81,77,22,204,248,6,129,140,233,26,183,121,55,54,90,249,11,191,116,163,91,230,180,11, 142,237,242,120,94,66,135,77]; + expect(encrypt(str2utf8(plaintext),key,nonce2)).toEqual(nonce2.concat(ciphertext)); + }); + }); + + describe("decrypt",function(){ + it("should be able to decrypt a Chacha20 encrypted text",function(){ + let text=str2utf8(plaintext); + let key=[]; + for(let i=0;i<16;i++){key.push(Math.floor(Math.random()*256));} + let nonce=[]; + for(let i=0;i<8;i++){nonce.push(Math.floor(Math.random()*256));} + expect(decrypt(encrypt(text,key,nonce),key)).toEqual(text); + }); + }); + +}); diff --git a/spec/test/utilSpec.js b/spec/test/utilSpec.js --- a/spec/test/utilSpec.js +++ b/spec/test/utilSpec.js @@ -14,6 +14,15 @@ describe("Util",function(){ ["\ud84c\udfb4",[0xf0,0xa3,0x8e,0xb4]] ]; + describe("bytes2int32s",function(){ + it("should pack bytes into 32b integers",function(){ + expect(util.bytes2int32s([])).toEqual([]); + expect(util.bytes2int32s([0])).toEqual([0]); + expect(util.bytes2int32s([1])).toEqual([1]); + expect(util.bytes2int32s([0x12,0x34,0x56,0x78,0x9a])).toEqual([0x78563412,0x9a]); + }); + }); + describe("str2utf8",function(){ it("should encode a String into bytes in UTF-8",function(){ utf.forEach(couple=>expect(util.str2utf8(couple[0])).toEqual(couple[1])); diff --git a/src/chacha.js b/src/chacha.js --- a/src/chacha.js +++ b/src/chacha.js @@ -1,4 +1,5 @@ // https://tools.ietf.org/html/rfc7539 +import {MASK,int32s2bytes,bytes2int32s} from "./util.js"; function lrot(x,shift){ return (x<>>(32-shift))&MASK; @@ -10,18 +11,28 @@ function createNonce(){ return nonce; } +function zeroPad(arr,length){ + return arr.concat((new Array(length)).fill(0)).slice(0,length); +} + +/** + * A Chacha20 cipher class. + * @param {Array} key Array of bytes (integers: 0<=x<256). Short keys are padded to 32B, long keys are silently truncated. + * @param {Array} nonce optional. If present, it must be an Array of bytes (integers: 0<=x<256). Short nonces are padded to 12B, long nonces are silently truncated. + */ export function Chacha20(key,nonce){ if(nonce===undefined){ - let nonce=createNonce(); + nonce=createNonce(); } - nonce=bytes2int32s(nonce); - key=bytes2int32s(key); + this._nonce=zeroPad(nonce,12); + nonce=bytes2int32s(this._nonce); + key=bytes2int32s(zeroPad(key,32)); this._state=[ 0x61707865,0x3320646e,0x79622d32,0x6b206574, key[0],key[1],key[2],key[3], key[4],key[5],key[6],key[7], - 0,nonce[0],nonce[1],nonce[2] + 1,nonce[0],nonce[1],nonce[2] ]; this._buffer=[]; } @@ -34,10 +45,15 @@ Chacha20.prototype.setPos=function(pos){ Chacha20.prototype.getByte=function(){ if(this._buffer.length==0){ this._buffer=int32s2bytes(this._computeBlock()); + this._incrementPos(); } return this._buffer.shift(); }; +Chacha20.prototype.getNonce=function(){ + return this._nonce.concat(); +}; + Chacha20.prototype._quarterRound=function(arr,ia,ib,ic,id){ let a=arr[ia], b=arr[ib], c=arr[ic], d=arr[id]; a=(a+b)&MASK; d=lrot(d^a,16); @@ -70,12 +86,15 @@ Chacha20.prototype._incrementPos=functio this._state[12]=(this._state[12]+1)&MASK; }; -function testChacha(){ - let key=[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31]; - let nonce=[0,0,0,9,0,0,0,74,0,0,0,0]; +export function encrypt(data,key,nonce){ let cipher=new Chacha20(key,nonce); - cipher.setPos(1); - let output=cipher._computeBlock().map(x=>(x>>>0).toString(16).padStart(8,"0")); - let example=["e4e7f110","15593bd1","1fdd0f50","c47120a3","c7f4d1c7","0368c033","9aaa2204","4e6cd4c3","466482d2","09aa9f07","05d7c214","a2028bd9","d19c12b5","b94e16de","e883d0cb","4e3c50a2"]; - console.log(output.every((x,i)=>x==example[i])); + nonce=cipher.getNonce(); + return nonce.concat(data.map(b=>b^cipher.getByte())); } + +export function decrypt(data,key){ + let nonce=data.slice(0,12); + let ciphertext=data.slice(12); + let cipher=new Chacha20(key,nonce); + return ciphertext.map(b=>b^cipher.getByte()); +} diff --git a/src/main.js b/src/main.js --- a/src/main.js +++ b/src/main.js @@ -1,6 +1,6 @@ import * as util from "./util.js"; import {blake2s} from "./blake.js"; -import {Chacha20} from "./chacha.js"; +import {Chacha20,encrypt,decrypt} from "./chacha.js"; export default {util,blake2s,Chacha20}; @@ -9,4 +9,6 @@ if(typeof module!=='undefined'&&module.h module.exports.util=util; module.exports.blake2s=blake2s; module.exports.Chacha20=Chacha20; + module.exports.encrypt=encrypt; + module.exports.decrypt=decrypt; }