/*
* Copyright (C) Sergey P. Derevyago, 2005.
*
* Permission to copy, use, modify, sell and distribute this software is granted
* provided this copyright notice appears in all copies.
* This software is provided "as is" without express or implied warranty, and
* with no claim as to its suitability for any purpose.
*
*/
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.LinkedList;
/**
* This is the reference implementation of DersCrypt symmetric-key block cipher.
*
* @see Crypto
* algorithm DersCrypt -- DersCrypt home page.
* @version 1.1 2005-12-24
* @author Sergey P. Derevyago
*/
public final class DersCrypt
{
/**
* Encrypts in
and writes the result to out
.
*
* @param key key to use
* @param in stream to read plain text from
* @param out stream to write encrypted text to
* @return number of bytes read from in
* @throws Exception in case of errors
*/
public static long streamEncrypt(byte[] key, InputStream in, OutputStream out)
throws Exception
{
long readCnt=0;
int minLen=minBlockLength(key.length);
byte[] buf1=readMax(in, minLen);
readCnt+=buf1.length;
for (byte[] prevEncr=null; buf1.length>0; ) {
byte[] plain, buf2=readMax(in, minLen);
readCnt+=buf2.length;
if (buf2.length==minLen) { // [1]
plain=buf1;
buf1=buf2;
}
else {
plain=new byte[buf1.length+buf2.length];
System.arraycopy(buf1, 0, plain, 0, buf1.length);
System.arraycopy(buf2, 0, plain, buf1.length, buf2.length);
buf1=new byte[0];
}
byte[] encr=blockEncrypt(key, plain, prevEncr); // [2]
prevEncr=encr;
writeInt16(out, encr.length); // [3]
out.write(encr);
}
return readCnt;
}
/**
* Decrypts in
and writes the result to out
.
*
* @param key key to use
* @param in stream to read encrypted text from
* @param out stream to write decrypted text to
* @return number of bytes read from in
* @throws Exception in case of errors
*/
public static long streamDecrypt(byte[] key, InputStream in, OutputStream out)
throws Exception
{
long readCnt=0;
int minLen=minBlockLength(key.length);
String errMsg="Invalid data: Invalid block format";
byte[] prevEncr=null;
for (int len; (len=readInt16(in))!=-1; ) {
readCnt+=2;
if (len<=minLen || len>=minLen*3) throw new Exception(errMsg); // [4]
byte[] encr=readMax(in, len);
readCnt+=encr.length;
if (encr.length!=len) throw new Exception(errMsg); // [5]
byte[] decr=blockDecrypt(key, encr, prevEncr);
prevEncr=encr;
out.write(decr);
}
return readCnt;
}
/**
* Encrypts a block of plain text.
*
* @param key key to use
* @param plain block of plain text to encrypt
* @param prevEncr previously encrypted block or null
* @return encrypted block
* @throws Exception in case of errors
*/
public static byte[] blockEncrypt(byte[] key, byte[] plain, byte[] prevEncr)
throws Exception
{
BigInteger b2=new BigInteger(1, key); // [6]
byte[] hash=computeHash(key.length, b2, plain, prevEncr);
BigInteger aPr1=new BigInteger(1, appendHash(plain, hash)); // [7]
boolean[] substOne=new boolean[1];
BigInteger aPr2=basicEncrypt(b2, aPr1, substOne);
byte[] encr=stripLeadingZeros(aPr2.toByteArray()); // [8]
if (substOne[0] && blockDecryptImpl(key, encr, prevEncr, false)!=null) {
throw new Exception("Cannot encrypt this data using this key: "+
"Hash collision"); // [9]
}
return encr;
}
/**
* Decrypts a block of encrypted text.
*
* @param key key to use
* @param encr block of encrypted text to decrypt
* @param prevEncr previously encrypted block or null
* @return decrypted block
* @throws Exception in case of errors
*/
public static byte[] blockDecrypt(byte[] key, byte[] encr, byte[] prevEncr)
throws Exception
{
byte[] decr=blockDecryptImpl(key, encr, prevEncr, false); // [10]
if (decr!=null) return decr;
decr=blockDecryptImpl(key, encr, prevEncr, true); // [11]
if (decr==null)
throw new Exception("Incorrect key and/or data: Hash mismatch"); // [12] [13]
return decr;
}
/**
* Decrypts a block of encrypted text performing the substitution if requested.
*
* @param key key to use
* @param encr block of encrypted text to decrypt
* @param prevEncr previously encrypted block or null
* @param substOne true
if the most significant remainder of value
* 1 must be substituted with 0
* @return decrypted block or null
*/
public static byte[] blockDecryptImpl(byte[] key, byte[] encr, byte[] prevEncr,
boolean substOne)
{
BigInteger aPr2=new BigInteger(1, encr), b2=new BigInteger(1, key);
BigInteger aPr1=basicDecrypt(b2, aPr2, substOne);
if (aPr1==null) return null; // [14]
byte[] decr=stripLeadingZeros(aPr1.toByteArray()); // [15]
byte[][] ph=extractHash(decr, key.length);
byte[] plain=ph[0], hash=ph[1];
byte[] hash2=computeHash(key.length, b2, plain, prevEncr);
if (!Arrays.equals(hash, hash2)) return null;
return plain;
}
/**
* Basic encryption functionality.
*
* @param b2 key value to use
* @param a1 number to encrypt
* @param substOne output parameter, true
if the most significant
* remainder of value 0 was substituted with 1
* @return encrypted number
*/
public static BigInteger basicEncrypt(BigInteger b2, BigInteger a1,
boolean[] substOne)
{
BigInteger[] rems=computeRemainders(a1, b2);
BigInteger[] rems2=permute(rems, b2, false);
if (rems2[0].equals(BigInteger.ZERO)) { // [16]
rems2[0]=BigInteger.ONE;
substOne[0]=true;
}
else substOne[0]=false;
BigInteger a2=computeNumber(rems2, b2);
return a2;
}
/**
* Basic decryption functionality.
*
* @param b2 key value to use
* @param a2 number to decrypt
* @param substOne true
if the most significant remainder of value
* 1 must be substituted with 0
* @return decrypted number
*/
public static BigInteger basicDecrypt(BigInteger b2, BigInteger a2,
boolean substOne)
{
BigInteger[] rems=computeRemainders(a2, b2);
if (substOne) { // [17]
if (rems[0].equals(BigInteger.ONE)) rems[0]=BigInteger.ZERO;
else return null;
}
BigInteger[] rems2=permute(rems, b2, true);
BigInteger a1=computeNumber(rems2, b2);
return a1;
}
/**
* Returns minimal data block length required for passed key length. [18]
*
* @param keyLength key length
* @return minimal data block length
*/
public static int minBlockLength(int keyLength)
{
if (keyLength<16) keyLength=16; // 128->40
else if (keyLength>64) keyLength=64; // 512->100
int n=40+((keyLength-16)*5+2)/4;
return keyLength*n;
}
/**
* Checks passed parameters.
*
* @param key key to check
* @param length data length to check
* @throws Exception in case of failed conditions
*/
public static void check(byte[] key, long length) throws Exception
{
if (key[0]==0)
throw new Exception("Suspectable key: Leading zeros are useless"); // [19]
if ((key[key.length-1]&1)==0)
throw new Exception("Suspectable key: Even values are not recommended"); // [20]
int min=minBlockLength(key.length);
if (lengtha into a sequence of remainders of division by
* b
.
*
* @param a number to split
* @param b number to divide by
* @return array of remainders
*/
public static BigInteger[] computeRemainders(BigInteger a, BigInteger b)
{
LinkedList lst=new LinkedList();
while (!a.equals(BigInteger.ZERO)) { // [22]
BigInteger[] qr=a.divideAndRemainder(b);
a=qr[0];
lst.addFirst(qr[1]);
}
return (BigInteger[])lst.toArray(new BigInteger[lst.size()]);
}
/**
* Converts a sequence of remainders of division by b
into a
* number.
*
* @param rems array of remainders
* @param b number to multiply by
* @return computed number
*/
public static BigInteger computeNumber(BigInteger[] rems, BigInteger b)
{
BigInteger a=rems[0];
for (int i=1; inull
* @return computed hash
*/
public static byte[] computeHash(int len, BigInteger b2, byte[] data,
byte[] prevData)
{
byte[] bits;
if (prevData!=null) bits=extractBits(prevData, len);
else {
bits=new byte[len];
for (int i=0; ihash bytes to data
. hash
* array is split into two parts and data
is inserted between
* them.
*
* @param data data to append hash to
* @param hash hash to append
* @return computed array
*/
public static byte[] appendHash(byte[] data, byte[] hash)
{
byte[] ret=new byte[data.length+hash.length];
int len=hash.length, mid=len/2;
System.arraycopy(hash, 0, ret, 0, len-mid); // [26]
System.arraycopy(data, 0, ret, len-mid, data.length);
System.arraycopy(hash, len-mid, ret, ret.length-mid, mid);
return ret;
}
/**
* Splits passed array into the data and hash parts.
*
* @param bytes array to split
* @param hashLen hash length
* @return two-elements array with data and hash
*/
public static byte[][] extractHash(byte[] bytes, int hashLen)
{
byte[] data=new byte[bytes.length-hashLen];
byte[] hash=new byte[hashLen];
int mid=hashLen/2;
System.arraycopy(bytes, 0, hash, 0, hashLen-mid);
System.arraycopy(bytes, hashLen-mid, data, 0, data.length);
System.arraycopy(bytes, bytes.length-mid, hash, hashLen-mid, mid);
return new byte[][]{data, hash};
}
/**
* Strips leading zeros out of bytes
array.
*
* @param bytes array to process
* @return processed array
*/
public static byte[] stripLeadingZeros(byte[] bytes)
{
if (bytes[0]!=0) return bytes;
int i;
for (i=1; idata.
*
* @param data data to extract bits from
* @param bitsLen length of array to return
* @return computed array
*/
public static byte[] extractBits(byte[] data, int bitsLen)
{
byte[] ret=new byte[bitsLen];
for (int i=0, j=1; i>=1) {
int n=(j++)*(data.length*8-1)/(bitsLen*8+1); // [27]
if ((data[n/8]&BIT_MASKS[n%8])!=0)
ret[i]|=mask;
}
}
return ret;
}
/**
* Creates permutation of passed length. [28]
*
* @param len length of permutation
* @param b number to create permutation from
* @return array of positions
*/
public static int[] createPermutation(int len, BigInteger b)
{
BigInteger b2=b;
int[] ret=new int[len], val=new int[len];
int med=len-len/2;
for (int i=0; i0 && v==ret[pbeg+j-1]+1) && ++k==rem) {
ret[pbeg+j]=v;
sub= (j+1true if inverse permutation must be applied
* @return permutted array
*/
public static BigInteger[] permute(BigInteger[] rems, BigInteger b,
boolean inverse)
{
int len=rems.length;
BigInteger[] ret=new BigInteger[len];
int perm[]=createPermutation(len, b);
if (inverse) for (int i=0; i>=1) {
int n=perm[j++];
if ((bits[i]&mask)!=0) ret[n/8]|=BIT_MASKS[n%8];
}
}
return ret;
}
/**
* Creates a subarray from passed array.
*
* @param array array to copy from
* @param pos position to start from
* @param len length of subarray
* @return created subarray
*/
public static byte[] subArray(byte[] array, int pos, int len)
{
byte[] ret=new byte[len];
System.arraycopy(array, pos, ret, 0, len);
return ret;
}
/**
* Reads limited number of bytes.
*
* @param in stream to read from
* @param maxLen maximum number of bytes to read
* @return read bytes
* @throws Exception in case of errors
*/
public static byte[] readMax(InputStream in, int maxLen) throws Exception
{
byte[] buf=new byte[maxLen];
int cnt=in.read(buf);
if (cnt==buf.length) return buf;
if (cnt==-1) return new byte[0];
return subArray(buf, 0, cnt);
}
/**
* Writes 16 bit integer to stream using big-endian byte ordering.
*
* @param out stream to write to
* @param val value to write
* @throws Exception in case of errors
*/
public static void writeInt16(OutputStream out, int val) throws Exception
{
out.write((val>>>8)&0xFF);
out.write(val&0xFF);
}
/**
* Reads 16 bit integer from stream using big-endian byte ordering.
*
* @param in stream to read from
* @return read value or -1
in case of EOF
* @throws Exception in case of errors
*/
public static int readInt16(InputStream in) throws Exception
{
int ch1=in.read();
if (ch1==-1) return -1;
int ch2=in.read();
if (ch2==-1) throw new EOFException();
return (ch1<<8)+ch2;
}
/** array of masks to extract successive bits of byte from left to right */
public static final int[] BIT_MASKS={
0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01
};
/**
* The main function.
* Checks passed parameters and performs requested operation.
*/
public static void main(String[] args) throws Exception
{
if (args.length!=4) invalidArgs();
String sCmd=args[0];
String sKey=args[1];
String sIn=args[2];
String sOut=args[3];
if ( !("e".equals(sCmd) || "d".equals(sCmd)) ) invalidArgs();
byte[] key=readFile(sKey);
check(key, (new File(sIn)).length()); // [36]
FileInputStream fin=new FileInputStream(sIn);
FileOutputStream fout=new FileOutputStream(sOut);
long start=System.currentTimeMillis();
long cnt= ("e".equals(sCmd)) ?
streamEncrypt(key, fin, fout) : streamDecrypt(key, fin, fout);
long dsec=(System.currentTimeMillis()-start+50)/100;
if (dsec!=0) System.out.println((dsec/10)+" sec, "+(cnt*10/dsec)+" b/s");
fout.close();
fin.close();
}
/**
* Prints the error message and exits.
*/
public static void invalidArgs()
{
System.err.println("Invalid args. Must be:");
System.err.println("e|d key_file input_file output_file");
System.exit(1);
}
/**
* Reads file with passed name
.
*
* @param name file name
* @return file content
* @throws Exception in case of errors
*/
public static byte[] readFile(String name) throws Exception
{
RandomAccessFile raf=new RandomAccessFile(name, "r");
byte[] ret=new byte[(int)raf.length()];
raf.readFully(ret);
raf.close();
return ret;
}
}