Java - Missing final characters when encrypting using blowfish
Asked Answered
A

4

5

I am using some java code that encrypts the contents of a text file using Blowfish. When I convert the encrypted file back (i.e. decrypt it) the string is missing a character from the end. Any ideas why? I am very new to Java and have been fiddling with this for hours with no luck.

The file war_and_peace.txt just contains the string "This is some text". decrypted.txt contains "This is some tex" (with no t on the end). Here is the java code:

public static void encrypt(String key, InputStream is, OutputStream os) throws Throwable {
    encryptOrDecrypt(key, Cipher.ENCRYPT_MODE, is, os);
}

public static void decrypt(String key, InputStream is, OutputStream os) throws Throwable {
    encryptOrDecrypt(key, Cipher.DECRYPT_MODE, is, os);
}

private static byte[] getBytes(String toGet)
{
    try
    {
        byte[] retVal = new byte[toGet.length()];
        for (int i = 0; i < toGet.length(); i++)
        {
            char anychar = toGet.charAt(i);
            retVal[i] = (byte)anychar;
        }
        return retVal;
    }catch(Exception e)
    {
        String errorMsg = "ERROR: getBytes :" + e;
        return null;
    }
}

public static void encryptOrDecrypt(String key, int mode, InputStream is, OutputStream os) throws Throwable {


   String iv = "12345678";
   byte[] IVBytes = getBytes(iv);
   IvParameterSpec IV = new IvParameterSpec(IVBytes);


    byte[] KeyData = key.getBytes(); 
    SecretKeySpec blowKey = new SecretKeySpec(KeyData, "Blowfish"); 
    //Cipher cipher = Cipher.getInstance("Blowfish/CBC/PKCS5Padding");
    Cipher cipher = Cipher.getInstance("Blowfish/CBC/NoPadding");

    if (mode == Cipher.ENCRYPT_MODE) {
        cipher.init(Cipher.ENCRYPT_MODE, blowKey, IV);
        CipherInputStream cis = new CipherInputStream(is, cipher);
        doCopy(cis, os);
    } else if (mode == Cipher.DECRYPT_MODE) {
        cipher.init(Cipher.DECRYPT_MODE, blowKey, IV);
        CipherOutputStream cos = new CipherOutputStream(os, cipher);
        doCopy(is, cos);
    }
}

public static void doCopy(InputStream is, OutputStream os) throws IOException {
    byte[] bytes = new byte[4096];
    //byte[] bytes = new byte[64];
    int numBytes;
    while ((numBytes = is.read(bytes)) != -1) {
        os.write(bytes, 0, numBytes);
    }
    os.flush();
    os.close();
    is.close();
}   

public static void main(String[] args) {


    //Encrypt the reports
    try {
        String key = "squirrel123";

        FileInputStream fis = new FileInputStream("war_and_peace.txt");
        FileOutputStream fos = new FileOutputStream("encrypted.txt");
        encrypt(key, fis, fos);

        FileInputStream fis2 = new FileInputStream("encrypted.txt");
        FileOutputStream fos2 = new FileOutputStream("decrypted.txt");
        decrypt(key, fis2, fos2);
    } catch (Throwable e) {
        e.printStackTrace();
    }
}

`

Ackerley answered 27/5, 2012 at 9:35 Comment(2)
I doubled the length of the string and ran the same code and it worked! Don't know why it did not work in the first place. Thanks.Ackerley
Related stackoverflow.com/questions/18146985/get-decrypted-inputstreamDaphnedaphnis
F
7

There is a couple of things not optimal here.

But let's first solve your problem. The reason why the last portion of your input is somehow missing is the padding you specify: none! Without specifying a padding, the Cipher can just operate on full-length blocks (8 bytes for Blowfish). Excess input that is less than a block long will be silently discarded, and there's your missing text. In detail: "This is some text" is 17 bytes long, so two full blocks will be decrypted, and the final 17th byte, "t", will be discarded.

Always use a padding in combination with symmetric block ciphers, PKCS5Padding is fine.

Next, when operating with Cipher, you don't need to implement your own getBytes() - there's String#getBytes already doing the job for you. Just be sure to operate on the same character encoding when getting the bytes and when reconstructing a String from bytes later on, it's a common source of errors.

You should have a look at the JCE docs, they will help you avoiding some of the common mistakes.

For example, using String keys directly is a no-go for symmetric cryptography, they do not contain enough entropy, which would make it easier to brute-force such a key. The JCE gives you theKeyGenerator class and you should always use it unless you know exactly what you are doing. It generates a securely random key of the appropriate size for you, but in addition, and that is something people tend to forget, it will also ensure that it doesn't create a weak key. For example, there are known weak keys for Blowfish that should be avoided in practical use.

Finally, you shouldn't use a deterministic IV when doing CBC encryption. There are some recent attacks that make it possible to exploit this, resulting in total recovery of the message, and that's obviously not cool. The IV should always be chosen at random (using a SecureRandom) in order to make it unpredictable. Cipher does this for you by default, you can simply obtain the used IV after encryption with Cipher#getIV.

On another note, less security-relevant: you should close streams in a finally block to ensure they're closed at all cost - otherwise you will be left with an open file handle in case of an exception.

Here's an updated version of your code that takes all these aspects into account (had to use Strings instead of files in main, but you can simply replace it with what you had there):

private static final String ALGORITHM = "Blowfish/CBC/PKCS5Padding";

/* now returns the IV that was used */
private static byte[] encrypt(SecretKey key, 
                              InputStream is, 
                              OutputStream os) {
    try {
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, key);
        CipherInputStream cis = new CipherInputStream(is, cipher);
        doCopy(cis, os);
        return cipher.getIV();
    } catch (Exception ex) {
        throw new RuntimeException(ex);
    }
}

private static void decrypt(SecretKey key, 
                            byte[] iv, 
                            InputStream is, 
                            OutputStream os) 
{
    try {
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        IvParameterSpec ivSpec = new IvParameterSpec(iv);
        cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
        CipherInputStream cis = new CipherInputStream(is, cipher);
        doCopy(cis, os);
    } catch (Exception ex) {
        throw new RuntimeException(ex);
    }
}

private static void doCopy(InputStream is, OutputStream os) 
throws IOException {
    try {
        byte[] bytes = new byte[4096];
        int numBytes;
        while ((numBytes = is.read(bytes)) != -1) {
            os.write(bytes, 0, numBytes);
        }
    } finally {
        is.close();
        os.close();
    }
}

public static void main(String[] args) {
    try {
        String plain = "I am very secret. Help!";

        KeyGenerator keyGen = KeyGenerator.getInstance("Blowfish");
        SecretKey key = keyGen.generateKey();
        byte[] iv;

        InputStream in = new ByteArrayInputStream(plain.getBytes("UTF-8"));
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        iv = encrypt(key, in, out);

        in = new ByteArrayInputStream(out.toByteArray());
        out = new ByteArrayOutputStream();
        decrypt(key, iv, in, out);

        String result = new String(out.toByteArray(), "UTF-8");
        System.out.println(result);
        System.out.println(plain.equals(result)); // => true
    } catch (Exception e) {
        e.printStackTrace();
    }
}
Fushih answered 27/5, 2012 at 16:39 Comment(0)
C
2

You have your CipherInputStream and CipherOutputStream mixed up. To encrypt, you read from a plain inputstream and write to a CipherOutputStream. To decrypt ... you get the idea.

EDIT:

What is happening is that you have specified NOPADDING and you are attempting to encrypt using a CipherInputStream. The first 16 bytes form two valid complete blocks and so are encrypted correctly. Then there is only 1 byte left over, and when the CipherInputStream class receives the end-of-file indication it performs a Cipher.doFinal() on the cipher object and receives an IllegalBlockSizeException. This exception is swallowed, and read returns -1 indicating end-of-file. If however you use PKCS5PADDING everything should work.

EDIT 2:

emboss is correct in that the real issue is simply that it is tricky and error-prone to use the CipherStream classes with the NOPADDING option. In fact, these classes explicitly state that they silently swallow every Security exception thrown by the underlying Cipher instance, so they are perhaps not a good choice for beginners.

Cosmonautics answered 27/5, 2012 at 16:53 Comment(4)
That's probably the most intuitive way to do it, but it's not necessary to do it that way. The OP's way works - CipherInputStream and CipherOutputStream are FilterInputStreams and FilterOutputStreams respectively, so the "direction" where you apply them shouldn't matter.Fushih
I'd have to look at the source code for CipherInputStream, but it apparently ignores any partial blocks if you specify NOPADDING. That is why it is only reading the first 16 bytes and seemingly not the 17th.Cosmonautics
Ah, OK, I see what you mean. I was wondering why it doesn't throw an Exception, I'm gonna check if CipherOutputStream behaves differently. If so, it would be a bug, don't you think?Fushih
CipherOutputStream swallows the exception in the same way. So at least it's consistent in its weirdness :) +1 for looking it up in the sources btw.Fushih
P
1

Keys are binary, and String is not a container for binary data. Use a byte[].

Petua answered 27/5, 2012 at 9:39 Comment(2)
I have a method called getBytes() that converts the key string into a byte[] - is this where the problem lies?Ackerley
It can be a problem if you don't specify exactly the encoding you want.Ceja
K
1

When I had this problem I had to call doFinal on the cipher:

http://docs.oracle.com/javase/1.4.2/docs/api/javax/crypto/Cipher.html#doFinal()

Khamsin answered 27/5, 2012 at 9:45 Comment(1)
Calling doFinal is important, but closing the CipherInputStream/CipherOutputStream does this for you already. It's not the problem here, the reason for the missing bytes is using no padding.Fushih

© 2022 - 2024 — McMap. All rights reserved.