I've tested Twofish (256 bit) as follows. A lengthy pseudorandom sequence was generated and used as a key. Plaintext phrases 000000000..00
, 00000..01
, 0000...FFFFF
, were generated and encoded using Twofish with the key. Ciphertexts were split in 32 bit numbers (40 million numbers total).
In theory, the cipher should pass the Dieharder tests. In practice, it appears to fail a number of them.
Pseudorandom number generator passed all tests, so this isn't a bug with Dieharder.
Does this mean that Twofish is not as secure as claimed?
Edited:
TwoFish.java comes from http://www.sethi.org/tmp/ssh/src/com/mindbright/security/cipher/Twofish.java
FishRunner.java
is my wrapper that generates "sequential plain text blocks as ASCII characters". Each block is ciphered using Twofish with byte[] defaultKey = {-24,29,83,38,-77,-92,-46,25,-117,-71,42,-44,-52,97,58,-114,50,-27,49,47,12,52,-76,-88,-26,17,18,84,30,-95,80,13,32};
. From each plaintext block I get 256 bits of ciphered text. These 256 bits are split in chunks of 32 bit each and saved to "/tmp/FromFish/fish.txt" with an appropriate header.
first several lines of fish.txt
#==================================================================
# generator mt19937 seed = 316179543
#==================================================================
type: d
count: 40000000
numbit: 32
4205098517
1950644630
217236044
4021751533
1670623305
4216006007
1982760947
2956150679
2523872870
1802016715
546984380
3195151793
478820823
3432394711
Once all 40000000 records were created (440Mb of data) Dieharder was launched with dieharder -a -g 202 -f fish.txt >>res.txt
dieharder version 3.31.1 Copyright 2003 Robert G. Brown #
##
rng_name | filename |rands/second|
file_input| fish.txt| 7.26e+06 |
#
test_name |ntup| tsamples |psamples| p-value |Assessment
# b #
diehard_birthdays| 0| 100| 100|0.65344498| PASSED
diehard_operm5| 0| 1000000| 100|0.11726095| PASSED
diehard_rank_32x32| 0| 40000| 100|0.84203088| PASSED
diehard_rank_6x8| 0| 100000| 100|0.73796762| PASSED
diehard_bitstream| 0| 2097152| 100|0.38051493| PASSED
diehard_opso| 0| 2097152| 100|0.04995365| PASSED
diehard_oqso| 0| 2097152| 100|0.59990831| PASSED
diehard_dna| 0| 2097152| 100|0.62220146| PASSED
diehard_count_1s_str| 0| 256000| 100|0.86262588| PASSED
diehard_count_1s_byt| 0| 256000| 100|0.12560712| PASSED
diehard_parking_lot| 0| 12000| 100|0.28041223| PASSED
diehard_2dsphere| 2| 8000| 100|0.76183470| PASSED
diehard_3dsphere| 3| 4000| 100|0.17846568| PASSED
diehard_squeeze| 0| 100000| 100|0.00384865| WEAK
diehard_sums| 0| 100| 100|0.43470008| PASSED
diehard_runs| 0| 100000| 100|0.70560801| PASSED
diehard_runs| 0| 100000| 100|0.98961342| PASSED
diehard_craps| 0| 200000| 100|0.18043071| PASSED
diehard_craps| 0| 200000| 100|0.89290364| PASSED
marsaglia_tsang_gcd| 0| 10000000| 100|0.00000000| FAILED
marsaglia_tsang_gcd| 0| 10000000| 100|0.00000000| FAILED
sts_monobit| 1| 100000| 100|0.72613955| PASSED
sts_runs| 2| 100000| 100|0.26712378| PASSED
sts_serial| 1| 100000| 100|0.94935591| PASSED
sts_serial| 2| 100000| 100|0.93198856| PASSED
sts_serial| 3| 100000| 100|0.99554380| WEAK
sts_serial| 3| 100000| 100|0.72444970| PASSED
sts_serial| 4| 100000| 100|0.19981717| PASSED
sts_serial| 4| 100000| 100|0.06124493| PASSED
sts_serial| 5| 100000| 100|0.87811046| PASSED
sts_serial| 5| 100000| 100|0.09618787| PASSED
sts_serial| 6| 100000| 100|0.87317579| PASSED
sts_serial| 6| 100000| 100|0.99688012| WEAK
sts_serial| 7| 100000| 100|0.50043383| PASSED
sts_serial| 7| 100000| 100|0.61746527| PASSED
sts_serial| 8| 100000| 100|0.97376805| PASSED
sts_serial| 8| 100000| 100|0.94872352| PASSED
sts_serial| 9| 100000| 100|0.35958803| PASSED
sts_serial| 9| 100000| 100|0.39252366| PASSED
sts_serial| 10| 100000| 100|0.40682407| PASSED
sts_serial| 10| 100000| 100|0.69846273| PASSED
sts_serial| 11| 100000| 100|0.14757958| PASSED
sts_serial| 11| 100000| 100|0.85142983| PASSED
sts_serial| 12| 100000| 100|0.33628714| PASSED
sts_serial| 12| 100000| 100|0.80400201| PASSED
sts_serial| 13| 100000| 100|0.50635238| PASSED
sts_serial| 13| 100000| 100|0.04113439| PASSED
sts_serial| 14| 100000| 100|0.48030593| PASSED
sts_serial| 14| 100000| 100|0.83615004| PASSED
sts_serial| 15| 100000| 100|0.85634237| PASSED
sts_serial| 15| 100000| 100|0.86413582| PASSED
sts_serial| 16| 100000| 100|0.81247787| PASSED
sts_serial| 16| 100000| 100|0.62279344| PASSED
rgb_bitdist| 1| 100000| 100|0.10521730| PASSED
rgb_bitdist| 2| 100000| 100|0.36419006| PASSED
rgb_bitdist| 3| 100000| 100|0.50848488| PASSED
rgb_bitdist| 4| 100000| 100|0.91911028| PASSED
rgb_bitdist| 5| 100000| 100|0.68355135| PASSED
rgb_bitdist| 6| 100000| 100|0.07298074| PASSED
rgb_bitdist| 7| 100000| 100|0.99703911| WEAK
rgb_bitdist| 8| 100000| 100|0.50843903| PASSED
rgb_bitdist| 9| 100000| 100|0.51151893| PASSED
rgb_bitdist| 10| 100000| 100|0.40558627| PASSED
rgb_bitdist| 11| 100000| 100|0.27640943| PASSED
rgb_bitdist| 12| 100000| 100|0.64415540| PASSED
rgb_minimum_distance| 2| 10000| 1000|0.77930849| PASSED
rgb_minimum_distance| 3| 10000| 1000|0.89974521| PASSED
rgb_minimum_distance| 4| 10000| 1000|0.52655953| PASSED
rgb_minimum_distance| 5| 10000| 1000|0.01029230| PASSED
rgb_permutations| 2| 100000| 100|0.49947588| PASSED
rgb_permutations| 3| 100000| 100|0.48976917| PASSED
rgb_permutations| 4| 100000| 100|0.99435735| PASSED
rgb_permutations| 5| 100000| 100|0.18313717| PASSED
rgb_lagged_sum| 0| 1000000| 100|0.11846988| PASSED
rgb_lagged_sum| 1| 1000000| 100|0.20793259| PASSED
rgb_lagged_sum| 2| 1000000| 100|0.08406645| PASSED
rgb_lagged_sum| 3| 1000000| 100|0.00002903| WEAK
rgb_lagged_sum| 4| 1000000| 100|0.00000000| FAILED
rgb_lagged_sum| 5| 1000000| 100|0.00156593| WEAK
rgb_lagged_sum| 6| 1000000| 100|0.13398055| PASSED
rgb_lagged_sum| 7| 1000000| 100|0.00000071| FAILED
rgb_lagged_sum| 8| 1000000| 100|0.07282751| PASSED
rgb_lagged_sum| 9| 1000000| 100|0.00000653| WEAK
rgb_lagged_sum| 10| 1000000| 100|0.24179580| PASSED
rgb_lagged_sum| 11| 1000000| 100|0.00620851| PASSED
rgb_lagged_sum| 12| 1000000| 100|0.24310357| PASSED
rgb_lagged_sum| 13| 1000000| 100|0.15323532| PASSED
rgb_lagged_sum| 14| 1000000| 100|0.00000174| WEAK
rgb_lagged_sum| 15| 1000000| 100|0.00000000| FAILED
rgb_lagged_sum| 16| 1000000| 100|0.39385345| PASSED
rgb_lagged_sum| 17| 1000000| 100|0.11381934| PASSED
rgb_lagged_sum| 18| 1000000| 100|0.78715021| PASSED
rgb_lagged_sum| 19| 1000000| 100|0.00000000| FAILED
rgb_lagged_sum| 20| 1000000| 100|0.52371093| PASSED
rgb_lagged_sum| 21| 1000000| 100|0.00531931| PASSED
rgb_lagged_sum| 22| 1000000| 100|0.19857791| PASSED
rgb_lagged_sum| 23| 1000000| 100|0.00000000| FAILED
rgb_lagged_sum| 24| 1000000| 100|0.00000431| WEAK
rgb_lagged_sum| 25| 1000000| 100|0.02471464| PASSED
rgb_lagged_sum| 26| 1000000| 100|0.07546643| PASSED
rgb_lagged_sum| 27| 1000000| 100|0.01786362| PASSED
rgb_lagged_sum| 28| 1000000| 100|0.28778610| PASSED
rgb_lagged_sum| 23| 1000000| 100|0.00000000| FAILED
rgb_lagged_sum| 30| 1000000| 100|0.13632978| PASSED
rgb_lagged_sum| 31| 1000000| 100|0.00000000| FAILED
rgb_lagged_sum| 32| 1000000| 100|0.11176539| PASSED
rgb_kstest_test| 0| 10000| 1000|0.29113864| PASSED
dab_bytedistrib| 0| 51200000| 1|0.00000000| FAILED
dab_dct| 256| 50000| 1|0.93719430| PASSED
Preparing to run test 207. ntuple = 0
dab_filltree| 32| 15000000| 1|0.60930301| PASSED
dab_filltree| 32| 15000000| 1|0.29252232| PASSED
Preparing to run test 208. ntuple = 0
dab_filltree2| 0| 5000000| 1|0.64246455| PASSED
dab_filltree2| 1| 5000000| 1|0.20829118| PASSED
Preparing to run test 209. ntuple = 0
dab_monobit2| 12| 65000000| 1|1.00000000| FAILED
Here's a summary of the tests that failed:
test_name |ntup| tsamples |psamples| p-value |Assessment
marsaglia_tsang_gcd| 0| 10000000| 100|0.00000000| FAILED
marsaglia_tsang_gcd| 0| 10000000| 100|0.00000000| FAILED
rgb_lagged_sum| 7| 1000000| 100|0.00000071| FAILED
rgb_lagged_sum| 15| 1000000| 100|0.00000000| FAILED
rgb_lagged_sum| 19| 1000000| 100|0.00000000| FAILED
rgb_lagged_sum| 23| 1000000| 100|0.00000000| FAILED
rgb_lagged_sum| 31| 1000000| 100|0.00000000| FAILED
dab_bytedistrib| 0| 51200000| 1|0.00000000| FAILED
dab_monobit2| 12| 65000000| 1|1.00000000| FAILED
Twofish algo is saved here: https://pastebin.com/THGegxKY
Class below is my wrapper to generate ciphers from consequential plain text messages
package models;
import java.io.File;
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Random;
public class FishRunner {
private String filePath = "~/Dev/CleanFish/res/";
private String valuesFolder = "values/in_";
private String keysFolder = "keys/k_";
private String resultsFolder = "results/dh_";
private int keySize = 32;
private byte[] defaultKey = {-24,29,83,38,-77,-92,-46,25,-117,-71,42,-44,-52,97,58,-114,50,-27,49,47,12,52,-76,-88,-26,17,18,84,30,-95,80,13};
public static void main(String[] args) {
System.out.println("l: " + fS(toUnsignedLong(-1)) +", 2xMaxInt "+ (2* ((long) Integer.MAX_VALUE)) +"\n\n\n");
FishRunner fr = new FishRunner();
}
private FishRunner() {
int[] numberOfReps = {6180339, 9227465, 10000000, 14930352};
char[] codesN = {'a', 'b', 'c','d'};
byte[] defPlainTextByteValue = {0,1,2,8,15};
char[] codesP = {'0','1','2','8','F'};
byte[] defaultOffsetStartingPosition = {0,3,7,12};
String[] codesOff = {"00","03","07","12"};
for(int i=0; i< numberOfReps.length; i++) {
for(int j=0; j< defPlainTextByteValue.length; j++) {
for(int k=0; k< defaultOffsetStartingPosition.length; k++) {
String fName = codesN[i]+ "_"+codesP[j]+ "_"+codesOff[k]+".txt";
try {
testAlgo(fName, numberOfReps[i], 0, defPlainTextByteValue[j], defaultOffsetStartingPosition[k]) ;
System.out.println("Done: "+fName);
} catch (Exception e) { e.printStackTrace(); }
}
}
}
System.out.println("Done.");
}
private void testAlgo(String filename, int numOfRepetitions, int initPlainTextValue, byte backgroundValue, byte defaultOffsetStartingPosition) throws Exception {
writeDHCommand(filename);
byte[] keyChars = defaultKey;
Object key = TwoFish.makeKey(keyChars);
int lineCount = numOfRepetitions*4;
writeKeyToFile(filename,keyChars);
PrintWriter writer = new PrintWriter(filePath + valuesFolder + filename, "UTF-8");
writer.println("#==================================================================");
writer.println("# generator mt19937 seed = 316179543");
writer.println("#==================================================================");
writer.println("type: d");
writer.println("count: "+lineCount+"");
writer.println("numbit: 32");
for(int i = 0; i < numOfRepetitions; i++) {
int intOffset = i + initPlainTextValue;
byte[] text = generateText(intOffset,backgroundValue,defaultOffsetStartingPosition);
byte[] ct = TwoFish.blockEncrypt(text, 0, key);
for (int x = 0; x < ct.length ; x=x+4) {
writer.println(fS(Long.toString(toUnsignedLong(toInt(ct, x))),10));
}
}
writer.close();
}
private byte[] generateText(int stepNumber, byte defValue, byte defaultOffsetStartingPosition) {
byte[] text = new byte[32];
Arrays.fill(text, defValue);
text[(defaultOffsetStartingPosition)% 32] = (byte) (stepNumber );
text[(defaultOffsetStartingPosition+1)% 32] = (byte) (stepNumber >>> 8);
text[(defaultOffsetStartingPosition+2)% 32] = (byte) (stepNumber >>> 16);
text[(defaultOffsetStartingPosition+3)% 32] = (byte) (stepNumber >>> 24);
return text;
}
private byte[] initKey() {
Random r = new Random(0);
byte[] key = new byte[keySize];
for (int i = 0; i < keySize; i++) {
key[i] = (byte)(-128+r.nextInt(256)) ;
}
System.out.println("Key: "+Arrays.toString(key));
return key;
}
public static int toInt(byte[] bytes, int offset) {
int ret = 0;
for (int i=0; i<4 && i+offset<bytes.length; i++) {
ret <<= 8;
ret |= (int)bytes[i+offset] & 0xFF;
}
return ret;
}
public static long toUnsignedLong(int x) {
return x & 0x00000000ffffffffL;
}
private static final char[] HEX_DIGITS = { '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F' };
private static String toString (byte[] ba, int offset, int length) {
char[] buf = new char[length * 2];
for (int i = offset, j = 0, k; i < offset+length; ) {
k = ba[i++];
buf[j++] = HEX_DIGITS[(k >>> 4) & 0x0F];
buf[j++] = HEX_DIGITS[ k & 0x0F];
}
return new String(buf);
}
private void writeDHCommand(String fileName) throws Exception {
String formattedFileName = valuesFolder + fileName;
String dhFileName = resultsFolder + fileName;
PrintWriter writerDHCommand = new PrintWriter(new FileOutputStream(new File(filePath+"dh_commands.txt"), true));
String dhCmdLine = "dieharder -a -g 202 -f "+ formattedFileName+ " >>"+ dhFileName;
writerDHCommand.println(dhCmdLine);
writerDHCommand.close();
}
private void writeKeyToFile(String filename, byte[] keyChars) throws Exception {
PrintWriter keyWritter = new PrintWriter(filePath + keysFolder + filename, "UTF-8");
keyWritter.println(Arrays.toString(keyChars));
keyWritter.close();
}
// INNER METHODS
private static final int longSpaces = 35;
private static final int intSpaces = 15;
public static String fS(long text) {
return fS(Long.toString(text), longSpaces);
}
public static String fS(int text) {
return fS(Integer.toString(text), intSpaces);
}
public static String fS(String text, int charTotal) {
return String.format("%1$" + charTotal + "s",text + "" );
}
private static String toString (byte[] ba) {
return toString(ba, 0, ba.length);
}
}
Edited:
Rewinds are not spotted by in 10M x 32bit datasets from Pi Hex and SecureRandom in at least some data sets. Increasing the size of PT for TwoFish allows it to pass DH tests. Possible explainations:
- False negative. The larger the number of sequencial contributes to
Randomness of plaintext blocks increases with their number.
As the number of PT blocks increases
- False positive. Looping over a small data set results in FAILED tests(Unlikely, since Hex Pi passes tests at the same size)
- False negative. Large number of PT blocks increases the entropy of PT itself, allowing the cipher to PASS tests
Passing extensive randomness testing suggests (but doesn't guarantee) that the cipher might be strong. Failing randomness tests indicate a problem with one of the following:
- Weak key (ruled out by testing a set of keys, generated using SecureRandom)
- Wrong conversion from CT to DH input format (if so - what went wrong?)
- Wrong implementation of the cipher (Does Google return buggy TF.java ?)
- Wrong cipher design (unlikely: algo validation improved since 1960s RANDU)
byte[] ct = MyFish.blockEncrypt(text, 0, key);
with bytes fromSecureRandom
as a troubleshooting step precisely because it should pass all tests. If it didn't then it allows one to conclude that there is a formatting error for fish.txt. $\endgroup$key
with bytes fromSecureRandom
is like adding a one-time pad after a Cesar cipher when testing a Cesar cipher. The result will be unbreakable but this security comes from one-time-pad, not the Cesar cipher. Likewise, if each block00...0
is ciphered with a new securely random key the output will pass Dieharder tests. But how helpful is that? The whole point of block ciphers is that you send a key once and use it for multiple blocks of text. If I can securely stream keys at high rate I would just use one-time-pad approach. $\endgroup$