2012-05-27 16 views
5

Tôi đang sử dụng một số mã java mã hóa nội dung của tệp văn bản bằng Blowfish. Khi tôi chuyển đổi các tập tin được mã hóa trở lại (tức là giải mã nó) chuỗi bị thiếu một ký tự từ cuối. Bất kỳ ý tưởng tại sao? Tôi rất mới với Java và đã không hài lòng với điều này trong nhiều giờ mà không có may mắn.Java - Thiếu ký tự cuối cùng khi mã hóa bằng blowfish

Tệp war_and_peace.txt chỉ chứa chuỗi "Đây là một số văn bản". decrypted.txt chứa "Đây là một số tex" (không có t vào cuối). Đây là đoạn mã java:

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(); 
    } 
} 

'

+0

chèn ký tự giả – SjB

+0

Tôi đã tăng gấp đôi độ dài của chuỗi và chạy cùng một mã và nó hoạt động! Không biết tại sao nó không hoạt động ngay từ đầu. Cảm ơn. –

+0

http://stackoverflow.com/questions/18146985/get-decrypted-inputstream – Qwerky

Trả lời

1

phím là nhị phân, và String không phải là một container cho dữ liệu nhị phân. Sử dụng một byte [].

+0

Tôi có một phương thức gọi là getBytes() để chuyển đổi chuỗi khóa thành một byte [] - đây có phải là vấn đề nằm ở đâu không? –

+0

Nó có thể là một vấn đề nếu bạn không chỉ định chính xác mã hóa bạn muốn. – rossum

7

Có một vài điều không tối ưu ở đây.

Nhưng trước hết hãy giải quyết vấn đề của bạn. Lý do tại sao phần cuối của đầu vào của bạn bằng cách nào đó bị thiếu là phần đệm bạn chỉ định: không có gì! Nếu không chỉ định một padding, Cipher chỉ có thể hoạt động trên các khối có độ dài đầy đủ (8 byte cho Blowfish). Đầu vào dư thừa nhỏ hơn một khối dài sẽ bị loại bỏ âm thầm và có văn bản bị thiếu của bạn. Cụ thể: "Đây là một số văn bản" dài 17 byte, do đó, hai khối đầy đủ sẽ được giải mã và byte thứ 17 cuối cùng, "t", sẽ bị hủy.

Luôn sử dụng phần đệm kết hợp với mật mã khối đối xứng, PKCS5Padding là tốt.

Tiếp theo, khi hoạt động với Cipher, bạn không cần triển khai getBytes() riêng của mình - có String#getBytes đã thực hiện công việc cho bạn. Chỉ cần chắc chắn để hoạt động trên cùng một mã hóa ký tự khi nhận được các byte và khi xây dựng lại một String từ các byte sau này, đó là một nguồn phổ biến của các lỗi.

Bạn nên xem qua số JCE docs, chúng sẽ giúp bạn tránh một số lỗi thường gặp. Ví dụ, bằng cách sử dụng các khóa String trực tiếp là không đi cho mật mã đối xứng, chúng không chứa đủ entropy, điều này sẽ làm cho nó dễ dàng hơn để tạo ra một khóa như vậy. JCE cung cấp cho bạn lớp KeyGenerator và bạn nên luôn sử dụng nó trừ khi bạn biết chính xác mình đang làm gì. Nó tạo ra một khóa ngẫu nhiên an toàn với kích thước thích hợp cho bạn, nhưng ngoài ra, và đó là điều mà mọi người có xu hướng quên, nó cũng sẽ đảm bảo rằng nó không tạo ra khóa yếu. Ví dụ, có các phím yếu được biết cho Blowfish nên tránh trong thực tế sử dụng.

Cuối cùng, bạn không nên sử dụng IV xác định khi thực hiện mã hóa CBC. Có một số cuộc tấn công gần đây làm cho nó có thể khai thác điều này, dẫn đến tổng hồi phục của thông điệp, và điều đó rõ ràng là không mát mẻ. IV luôn luôn được chọn ngẫu nhiên (sử dụng SecureRandom) để làm cho nó không thể đoán trước. Cipher thực hiện điều này cho bạn theo mặc định, bạn có thể chỉ cần lấy IV đã sử dụng sau khi mã hóa với Cipher#getIV.

Lưu ý khác, ít liên quan đến bảo mật: bạn nên đóng luồng trong khối finally để đảm bảo chúng được đóng bằng mọi giá - nếu không bạn sẽ bị xử lý tệp mở trong trường hợp ngoại lệ.

Dưới đây là một phiên bản cập nhật của mã của bạn mà sẽ đưa tất cả những khía cạnh này vào tài khoản (phải dùng Strings thay vì tập tin trong main, nhưng bạn chỉ có thể thay thế nó với những gì bạn đã có):

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(); 
    } 
} 
2

Bạn có CipherInputStreamCipherOutputStream được trộn lẫn. Để mã hóa, bạn đọc từ một luồng đầu vào đơn giản và ghi vào CipherOutputStream. Để giải mã ... bạn có ý tưởng.

EDIT:

gì đang xảy ra là bạn đã xác định NOPADDING bạn đang cố gắng để mã hóa bằng cách sử dụng CipherInputStream. 16 byte đầu tiên tạo thành hai khối hoàn chỉnh hợp lệ và do đó được mã hóa chính xác. Sau đó, chỉ còn lại 1 byte và khi lớp CipherInputStream nhận được chỉ báo kết thúc tệp, nó thực hiện một đối tượng mã hóa và nhận một ngoại lệ IllegalBlockSizeException Cipher.doFinal(). Ngoại lệ này bị nuốt và đọc trả về -1 cho biết kết thúc tập tin. Tuy nhiên, nếu bạn sử dụng PKCS5PADDING thì mọi thứ sẽ hoạt động.

EDIT 2:

chạm khắc là đúng ở chỗ vấn đề thực sự chỉ đơn giản là nó phức tạp và dễ bị lỗi sử dụng các lớp CipherStream với các tùy chọn NOPADDING. Trong thực tế, các lớp này tuyên bố rõ ràng rằng chúng âm thầm nuốt mọi ngoại lệ bảo mật được ném bởi cá thể mã hóa cơ bản, vì vậy chúng có lẽ không phải là một lựa chọn tốt cho người mới bắt đầu.

+0

Đó có lẽ là cách trực quan nhất để làm điều đó, nhưng không cần thiết phải làm theo cách đó. Cách hoạt động của OP - CipherInputStream và CipherOutputStream là FilterInputStreams và FilterOutputStreams tương ứng, do đó, "hướng" mà bạn áp dụng chúng không quan trọng. – emboss

+1

Tôi phải xem mã nguồn của CipherInputStream, nhưng nó dường như bỏ qua bất kỳ khối nào nếu bạn chỉ định NOPADDING. Đó là lý do tại sao nó chỉ đọc 16 byte đầu tiên và dường như không phải là 17. –

+0

Ah, OK, tôi hiểu ý của bạn là gì. Tôi đã tự hỏi tại sao nó không ném một ngoại lệ, tôi sẽ kiểm tra xem CipherOutputStream có hoạt động khác không. Nếu vậy, nó sẽ là một lỗi, bạn không nghĩ sao? – emboss