Changeset - 12f10d9d5948
[Not reviewed]
default
0 4 0
Laman - 5 years ago 2019-07-05 15:42:03

refactored Chacha20.encrypt, .decrypt
4 files changed with 22 insertions and 22 deletions:
0 comments (0 inline, 0 general)
spec/test/chachaSpec.js
Show inline comments
 
/* 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));
 
			expect(encrypt(str2utf8(plaintext),key,nonce2)).toEqual([nonce2,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);
 
			let [_,ciphertext]=encrypt(text,key,nonce);
 
			expect(decrypt(ciphertext,key,nonce)).toEqual(text);
 
		});
 
	});
 
	
 
});
src/chacha.js
Show inline comments
 
// https://tools.ietf.org/html/rfc7539
 
import {MASK,int32s2bytes,bytes2int32s,zeroPad} from "./util.js";
 
import {MASK,int32s2bytes,bytes2int32s,zeroPad,createRandomNonce} from "./util.js";
 

	
 
function lrot(x,shift){
 
	return (x<<shift|x>>>(32-shift))&MASK;
 
}
 

	
 
export function createNonce(){
 
	let nonce=new Uint8Array(12);
 
	window.crypto.getRandomValues(nonce);
 
	return Array.from(nonce);
 
}
 

	
 
/**
 
 * 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){
 
	const NONCE_LEN=12;
 
	if(nonce===undefined){
 
		nonce=createNonce();
 
		nonce=createRandomNonce(NONCE_LEN);
 
	}
 
	this._nonce=zeroPad(nonce,12);
 
	this._nonce=zeroPad(nonce,NONCE_LEN);
 
	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],
 
		1,nonce[0],nonce[1],nonce[2]
 
	];
 
	this._buffer=[];
 
}
 

	
 
Chacha20.prototype.setPos=function(pos){
 
	this._state[12]=pos;
 
	this._buffer=[];
 
}
 

	
 
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);
 
	c=(c+d)&MASK; b=lrot(b^c,12);
 
	a=(a+b)&MASK; d=lrot(d^a,8);
 
	c=(c+d)&MASK; b=lrot(b^c,7);
 
	arr[ia]=a; arr[ib]=b; arr[ic]=c; arr[id]=d;
 
};
 

	
 
Chacha20.prototype._computeBlock=function(){
 
	let state=this._state.slice();
 

	
 
	for(let i=0;i<10;i++){ // 10 double rounds
 
		// column round
 
		this._quarterRound(state,0,4,8,12);
 
		this._quarterRound(state,1,5,9,13);
 
		this._quarterRound(state,2,6,10,14);
 
		this._quarterRound(state,3,7,11,15);
 
		// diagonal round
 
		this._quarterRound(state,0,5,10,15);
 
		this._quarterRound(state,1,6,11,12);
 
		this._quarterRound(state,2,7,8,13);
 
		this._quarterRound(state,3,4,9,14);
 
	}
 

	
 
	return state.map((xi,i)=>(xi+this._state[i])&MASK);
 
};
 

	
 
Chacha20.prototype._incrementPos=function(){
 
	this._state[12]=(this._state[12]+1)&MASK;
 
};
 

	
 
export function encrypt(data,key,nonce){
 
	let cipher=new Chacha20(key,nonce);
 
	nonce=cipher.getNonce();
 
	return nonce.concat(data.map(b=>b^cipher.getByte()));
 
	return [nonce,data.map(b=>b^cipher.getByte())];
 
}
 

	
 
export function decrypt(data,key){
 
	let nonce=data.slice(0,12);
 
	let ciphertext=data.slice(12);
 
export function decrypt(ciphertext,key,nonce){
 
	let cipher=new Chacha20(key,nonce);
 
	return ciphertext.map(b=>b^cipher.getByte());
 
}
src/main.js
Show inline comments
 
import * as util from "./util.js";
 
import {blake2s} from "./blake.js";
 
import {pbkdf2} from "./pbkdf2.js";
 
import {createNonce,Chacha20,encrypt as _encrypt,decrypt as _decrypt} from "./chacha.js";
 
import {Chacha20,encrypt as _encrypt,decrypt as _decrypt} from "./chacha.js";
 

	
 
const VERSION=1;
 

	
 
function encrypt(s,password){
 
	let bs=util.str2utf8(s);
 
	let pass=util.str2utf8(password);
 
	let salt=createNonce();
 
	let salt=util.createRandomNonce(12);
 
	let [iters,key]=stretchKey(pass,salt);
 
	let noncedCiphertext=_encrypt(bs,key,salt);
 
	let payload=[iters].concat(noncedCiphertext);
 
	let [_,ciphertext]=_encrypt(bs,key,salt);
 
	let payload=[iters].concat(salt,ciphertext);
 
	let signature=blake2s([VERSION].concat(payload),16,pass);
 
	let arr=[VERSION].concat(signature,payload);
 
	return util.bytes2base64(arr);
 
}
 

	
 
function decrypt(s,password){
 
	let pass=util.str2utf8(password);
 
	let arr=util.base642bytes(s);
 
	let version=arr[0];
 
	let signature=arr.slice(1,17);
 
	let iters=arr[17];
 
	let salt=arr.slice(18,30);
 
	let noncedCiphertext=arr.slice(18);
 
	let check=blake2s([version].concat([iters],noncedCiphertext),16,pass);
 
	let ciphertext=arr.slice(30);
 
	let check=blake2s([version,iters].concat(salt,ciphertext),16,pass);
 
	if(!signature.every((b,i)=>b===check[i])){return false;}
 
	if(version>VERSION){return false;}
 
	let key=pbkdf2(pass,salt,1<<iters,32);
 
	let plainbytes=_decrypt(noncedCiphertext,key);
 
	let plainbytes=_decrypt(ciphertext,key,salt);
 
	return util.utf82str(plainbytes);
 
}
 

	
 
function stretchKey(password,salt){
 
	let start=Date.now(); // ms
 
	let i,key;
 
	for(i=0;i<256;i++){
 
		key=pbkdf2(password,salt,1<<i,32);
 
		if(Date.now()-start>=500){break;}
 
	}
 
	return [i,key];
 
}
 

	
 
export default {util,blake2s,pbkdf2,Chacha20,encrypt,decrypt};
 

	
 
// export for tests running on Node
 
if(typeof module!=='undefined'&&module.hasOwnProperty('exports')){
 
	module.exports.util=util;
 
	module.exports.blake2s=blake2s;
 
	module.exports.pbkdf2=pbkdf2;
 
	module.exports.Chacha20=Chacha20;
 
	module.exports.encrypt=_encrypt;
 
	module.exports.decrypt=_decrypt;
 
}
src/util.js
Show inline comments
 
@@ -45,96 +45,102 @@ export function bytes2hex(arr){
 
	return arr.map(x=>x.toString(16).padStart(2,"0")).join("");
 
}
 

	
 
export function str2utf8(s){
 
	let res=[];
 
	let c=s.codePointAt(0);
 
	for(let i=0; c!==undefined; i++,c=s.codePointAt(i)){
 
		if(c<0x80){res.push(c);}
 
		else if(c<0x800){
 
			res.push(0b11000000|(c>>>6));
 
			res.push(0b10000000|(c&0b111111));
 
		}
 
		else if(c<0x10000){
 
			res.push(0b11100000|(c>>>12));
 
			res.push(0b10000000|((c>>>6)&0b111111));
 
			res.push(0b10000000|(c&0b111111));
 
		}
 
		else{
 
			res.push(0b11110000|(c>>>18));
 
			res.push(0b10000000|((c>>>12)&0b111111));
 
			res.push(0b10000000|((c>>>6)&0b111111));
 
			res.push(0b10000000|(c&0b111111));
 
		}
 
		if(c>0xffff){i++;} // skip surrogate
 
	}
 
	return res;
 
}
 

	
 
export function utf82str(arr){
 
	let res=[];
 
	for(let i=0;i<arr.length;i++){
 
		let x=arr[i];
 
		if(x<=0b1111111){res.push(x);}
 
		else if(x<=0b11011111){
 
			let a=x&0b11111;
 
			let b=arr[++i]&0b111111;
 
			res.push(a<<6|b);
 
		}
 
		else if(x<=0b11101111){
 
			let a=x&0b1111;
 
			let b=arr[++i]&0b111111;
 
			let c=arr[++i]&0b111111;
 
			res.push(a<<12|b<<6|c);
 
		}
 
		else{
 
			let a=x&0b111;
 
			let b=arr[++i]&0b111111;
 
			let c=arr[++i]&0b111111;
 
			let d=arr[++i]&0b111111;
 
			res.push(a<<18|b<<12|c<<6|d);
 
		}
 
	}
 
	return res.map(x=>String.fromCodePoint(x)).join("");
 
}
 

	
 
const mapping="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".split("");
 
const remapping=new Array(128);
 
mapping.forEach((c,i)=>{remapping[c.charCodeAt(0)]=i;});
 

	
 
export function bytes2base64(byteArr){
 
	let arr=byteArr.concat();
 
	let out=[];
 
	let rem=(3-arr.length%3)%3;
 
	for(let i=0;i<rem;i++){arr.push(0);} // pad array to a multiple of 3
 
	
 
	for(let i=0;i<arr.length;i+=3){ // encode 3 bytes into 4 characters
 
		out.push(mapping[arr[i]>>>2&63]);
 
		out.push(mapping[((arr[i]&3)<<4)+(arr[i+1]>>>4&15)]);
 
		out.push(mapping[((arr[i+1]&15)<<2)+(arr[i+2]>>>6&3)]);
 
		out.push(mapping[arr[i+2]&63]);
 
	}
 
	
 
	for(let i=0;i<rem;i++){out.pop();}
 
	for(let i=0;i<rem;i++){out.push("=");}
 

	
 
	return out.join("");
 
}
 

	
 
export function base642bytes(str){
 
	let out=[];
 
	
 
	for(let i=0;i<str.length;i+=4){
 
		let b1=remapping[str.charCodeAt(i)];
 
		let b2=remapping[str.charCodeAt(i+1)];
 
		let b3=remapping[str.charCodeAt(i+2)];
 
		let b4=remapping[str.charCodeAt(i+3)];
 
		
 
		out.push((b1<<2)+(b2>>4&3));
 
		out.push(((b2&15)<<4)+(b3>>2&15));
 
		out.push(((b3&3)<<6)+b4);
 
	}
 
	
 
	for(let i=1; i<3&&str[str.length-i]=="="; i++){out.pop();}
 

	
 
	return out;
 
}
 

	
 
export function createRandomNonce(n){
 
	let nonce=new Uint8Array(n);
 
	window.crypto.getRandomValues(nonce);
 
	return Array.from(nonce);
 
}
0 comments (0 inline, 0 general)