ไบต์เริ่มต้นไม่ถูกต้องหลังจากถอดรหัส Java AES / CBC


116

มีอะไรผิดปกติกับตัวอย่างต่อไปนี้?

ปัญหาคือส่วนแรกของสตริงที่ถอดรหัสเป็นเรื่องไร้สาระ อย่างไรก็ตามส่วนที่เหลือไม่เป็นไรฉันเข้าใจ ...

Result: eB6OgeS��i are you? Have a nice day.
@Test
public void testEncrypt() {
  try {
    String s = "Hello there. How are you? Have a nice day.";

    // Generate key
    KeyGenerator kgen = KeyGenerator.getInstance("AES");
    kgen.init(128);
    SecretKey aesKey = kgen.generateKey();

    // Encrypt cipher
    Cipher encryptCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    encryptCipher.init(Cipher.ENCRYPT_MODE, aesKey);

    // Encrypt
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, encryptCipher);
    cipherOutputStream.write(s.getBytes());
    cipherOutputStream.flush();
    cipherOutputStream.close();
    byte[] encryptedBytes = outputStream.toByteArray();

    // Decrypt cipher
    Cipher decryptCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    IvParameterSpec ivParameterSpec = new IvParameterSpec(aesKey.getEncoded());
    decryptCipher.init(Cipher.DECRYPT_MODE, aesKey, ivParameterSpec);

    // Decrypt
    outputStream = new ByteArrayOutputStream();
    ByteArrayInputStream inStream = new ByteArrayInputStream(encryptedBytes);
    CipherInputStream cipherInputStream = new CipherInputStream(inStream, decryptCipher);
    byte[] buf = new byte[1024];
    int bytesRead;
    while ((bytesRead = cipherInputStream.read(buf)) >= 0) {
        outputStream.write(buf, 0, bytesRead);
    }

    System.out.println("Result: " + new String(outputStream.toByteArray()));

  } 
  catch (Exception ex) {
    ex.printStackTrace();
  }
}

48
อย่าใช้คำตอบใด ๆ ของคำถามนี้ในโครงการที่ร้ายแรง! ตัวอย่างทั้งหมดที่ให้ไว้ในคำถามนี้มีความเสี่ยงที่จะเกิด padding oracle และเป็นการใช้งานการเข้ารหัสโดยรวมที่ไม่ดี คุณจะแนะนำช่องโหว่ด้านการเข้ารหัสที่ร้ายแรงในโครงการของคุณโดยใช้ข้อมูลโค้ดด้านล่าง
HoLyVieR

16
@HoLyVieR เกี่ยวกับคำพูดต่อไปนี้: "คุณไม่ควรพัฒนาไลบรารีการเข้ารหัสของคุณเอง"และ"ใช้ API ระดับสูงที่เฟรมเวิร์กของคุณมีให้" ไม่มีใครที่นี่กำลังพัฒนาห้องสมุดการเข้ารหัสของตนเอง เราใช้ API ระดับสูงที่มีอยู่แล้วซึ่ง java framework มีให้ คุณชายไม่ถูกต้องอย่างมาก
k170

10
@MaartenBodewes เพียงเพราะคุณทั้งคู่เห็นด้วยไม่ได้หมายความว่าคุณทั้งคู่ถูกต้อง นักพัฒนาที่ดีทราบถึงความแตกต่างระหว่างการรวม API ระดับสูงและการเขียน API ระดับต่ำใหม่ ผู้อ่านที่ดีจะสังเกตเห็นว่า OP ถามหา "ง่าย Java AES เข้ารหัส / ถอดรหัสเช่น" และนั่นคือสิ่งที่เขาได้รับ ฉันไม่เห็นด้วยกับคำตอบอื่น ๆ ด้วยเหตุนี้ฉันจึงโพสต์คำตอบของฉันเอง บางทีพวกคุณควรลองแบบเดียวกันและสอนพวกเราทุกคนด้วยความเชี่ยวชาญของคุณ
k170

6
@HoLyVieR นั่นเป็นสิ่งที่ไร้สาระที่สุดที่ฉันเคยอ่าน SO! คุณเป็นใครที่จะบอกคนอื่นว่าพวกเขาทำได้และไม่สามารถพัฒนาได้?
TedTrippin

14
ยังไม่เห็นตัวอย่าง @HoLyVieR มาดูกันบ้างหรือตัวชี้ห้องสมุด? ไม่สร้างสรรค์เลย
danieljimenez

คำตอบ:


245

ผู้คนจำนวนมากรวมถึงตัวฉันเองประสบปัญหามากมายในการทำงานนี้เนื่องจากข้อมูลบางอย่างขาดหายไปเช่นลืมแปลงเป็น Base64 เวกเตอร์การเริ่มต้นชุดอักขระ ฯลฯ ดังนั้นฉันจึงคิดที่จะสร้างโค้ดที่ใช้งานได้อย่างสมบูรณ์

หวังว่านี่จะเป็นประโยชน์สำหรับคุณทุกคน: ในการคอมไพล์คุณต้องมีโถ Apache Commons Codec เพิ่มเติมซึ่งมีอยู่ที่นี่: http://commons.apache.org/proper/commons-codec/download_codec.cgi

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;

public class Encryptor {
    public static String encrypt(String key, String initVector, String value) {
        try {
            IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
            SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");

            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);

            byte[] encrypted = cipher.doFinal(value.getBytes());
            System.out.println("encrypted string: "
                    + Base64.encodeBase64String(encrypted));

            return Base64.encodeBase64String(encrypted);
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        return null;
    }

    public static String decrypt(String key, String initVector, String encrypted) {
        try {
            IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
            SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");

            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);

            byte[] original = cipher.doFinal(Base64.decodeBase64(encrypted));

            return new String(original);
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        return null;
    }

    public static void main(String[] args) {
        String key = "Bar12345Bar12345"; // 128 bit key
        String initVector = "RandomInitVector"; // 16 bytes IV

        System.out.println(decrypt(key, initVector,
                encrypt(key, initVector, "Hello World")));
    }
}

47
หากคุณไม่ต้องการพึ่งพาไลบรารี Apache Commons Codec ของบุคคลที่สามคุณสามารถใช้javax.xml.bind.DatatypeConverterของ JDK เพื่อทำการเข้ารหัส / ถอดรหัส Base64: System.out.println("encrypted string:" + DatatypeConverter.printBase64Binary(encrypted)); byte[] original = cipher.doFinal(DatatypeConverter.parseBase64Binary(encrypted));
curd0

8
คุณใช้ IV คงที่หรือไม่!
vianna77

36
Java 8 มีเครื่องมือ Base64 อยู่แล้ว: java.util.Base64.getDecoder () และ java.util.Base64.getEncoder ()
Hristo Stoyanov

11
IV ไม่จำเป็นต้องเป็นความลับ แต่จะต้องไม่สามารถคาดเดาได้สำหรับโหมด CBC (และไม่ซ้ำกันสำหรับ CTR) สามารถส่งไปพร้อมกับ ciphertext วิธีทั่วไปในการทำเช่นนี้คือการนำ IV ไปใส่ในไซเฟอร์เท็กซ์และหั่นบาง ๆ ออกก่อนที่จะถอดรหัส ควรสร้างผ่านSecureRandom
Artjom B.

6
รหัสผ่านไม่ใช่กุญแจ IV ควรเป็นแบบสุ่ม
Maarten Bodewes

40

นี่คือวิธีการแก้ปัญหาโดยไม่ต้องApache Commons Codec's Base64:

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

public class AdvancedEncryptionStandard
{
    private byte[] key;

    private static final String ALGORITHM = "AES";

    public AdvancedEncryptionStandard(byte[] key)
    {
        this.key = key;
    }

    /**
     * Encrypts the given plain text
     *
     * @param plainText The plain text to encrypt
     */
    public byte[] encrypt(byte[] plainText) throws Exception
    {
        SecretKeySpec secretKey = new SecretKeySpec(key, ALGORITHM);
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, secretKey);

        return cipher.doFinal(plainText);
    }

    /**
     * Decrypts the given byte array
     *
     * @param cipherText The data to decrypt
     */
    public byte[] decrypt(byte[] cipherText) throws Exception
    {
        SecretKeySpec secretKey = new SecretKeySpec(key, ALGORITHM);
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, secretKey);

        return cipher.doFinal(cipherText);
    }
}

ตัวอย่างการใช้งาน:

byte[] encryptionKey = "MZygpewJsCpRrfOr".getBytes(StandardCharsets.UTF_8);
byte[] plainText = "Hello world!".getBytes(StandardCharsets.UTF_8);
AdvancedEncryptionStandard advancedEncryptionStandard = new AdvancedEncryptionStandard(
        encryptionKey);
byte[] cipherText = advancedEncryptionStandard.encrypt(plainText);
byte[] decryptedCipherText = advancedEncryptionStandard.decrypt(cipherText);

System.out.println(new String(plainText));
System.out.println(new String(cipherText));
System.out.println(new String(decryptedCipherText));

พิมพ์:

Hello world!
դ;��LA+�ߙb*
Hello world!

5
นี่คือตัวอย่างการทำงานที่สมบูรณ์แบบเช่นเดียวกับของ @ chandpriyankara แต่ทำไมกำหนดลายเซ็นของencrypt(String)และไม่encrypt(byte[] )? การเข้ารหัส (ถอดรหัสด้วย) เป็นกระบวนการที่ใช้ไบต์ (AES อยู่แล้ว) การเข้ารหัสใช้ไบต์เป็นอินพุตและเอาต์พุตเป็นไบต์การถอดรหัสก็เช่นกัน (case in point: Cipherอ็อบเจ็กต์ทำ) ตอนนี้กรณีการใช้งานเฉพาะอย่างหนึ่งอาจจะมีการเข้ารหัสไบต์ที่มาจาก String หรือส่งเป็น String (ไฟล์แนบ base64 MIME สำหรับ Mail ... ) แต่นั่นเป็นปัญหาของการเข้ารหัสไบต์ซึ่งมีอยู่หลายร้อย โซลูชันไม่เกี่ยวข้องกับ AES / การเข้ารหัสโดยสิ้นเชิง
GPI

3
@GPI: ใช่ แต่ฉันคิดว่ามันมีประโยชน์มากกว่าStringsเพราะนั่นคือสิ่งที่ฉันทำงานโดยใช้เวลา 95% และคุณก็จะเปลี่ยนใจกันอยู่ดี
BullyWiiPlaza

9
ไม่นี่ไม่เทียบเท่าโค้ดของ Chandpriyankara! รหัสของคุณใช้ ECB ซึ่งโดยทั่วไปไม่ปลอดภัยและไม่เป็นที่ต้องการ ควรระบุ CBC อย่างชัดเจน เมื่อระบุ CBC รหัสของคุณจะพัง
แดน

ทำงานได้อย่างสมบูรณ์แบบไม่ปลอดภัยอย่างที่สุดและใช้วิธีการเขียนโปรแกรมที่ไม่ดี ชั้นเรียนมีชื่อไม่ดี ไม่มีการตรวจสอบขนาดคีย์ล่วงหน้า แต่ที่สำคัญที่สุดโค้ดใช้โหมด ECB ไม่ปลอดภัยซ่อนตัวปัญหาในคำถามเดิม สุดท้ายไม่ได้ระบุการเข้ารหัสอักขระซึ่งหมายความว่าการถอดรหัสเป็นข้อความอาจล้มเหลวบนแพลตฟอร์มอื่น ๆ
Maarten Bodewes

24

ดูเหมือนฉันจะจัดการกับ Initialization Vector (IV) ไม่ถูกต้อง เป็นเวลานานแล้วที่ฉันอ่านเกี่ยวกับ AES, IVs และ block chaining แต่เป็นแนวของคุณ

IvParameterSpec ivParameterSpec = new IvParameterSpec(aesKey.getEncoded());

ดูเหมือนจะไม่โอเค ในกรณีของ AES คุณสามารถคิดว่าเวกเตอร์เริ่มต้นเป็น "สถานะเริ่มต้น" ของอินสแตนซ์การเข้ารหัสและสถานะนี้เป็นข้อมูลเล็กน้อยที่คุณไม่สามารถหาได้จากคีย์ของคุณ แต่มาจากการคำนวณจริงของการเข้ารหัสการเข้ารหัส (อาจมีคนโต้แย้งว่าถ้า IV สามารถดึงออกมาจากคีย์ได้ก็จะไม่มีประโยชน์เนื่องจากคีย์นั้นถูกมอบให้กับอินสแตนซ์การเข้ารหัสในช่วงเริ่มต้นแล้ว)

ดังนั้นคุณควรได้รับ IV เป็นไบต์ [] จากอินสแตนซ์การเข้ารหัสที่ส่วนท้ายของการเข้ารหัสของคุณ

  cipherOutputStream.close();
  byte[] iv = encryptCipher.getIV();

และคุณควรจะเริ่มต้นของคุณCipherในDECRYPT_MODEกับไบต์นี้ []:

  IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);

จากนั้นการถอดรหัสของคุณควรจะใช้ได้ หวังว่านี่จะช่วยได้


ขอบคุณที่ช่วยมือใหม่ ฉันปูตัวอย่างนี้จากโพสต์อื่น ๆ ฉันไม่คิดว่าคุณรู้วิธีหลีกเลี่ยงความจำเป็นในการ IV? ฉันเคยเห็น แต่ไม่ได้ลองตัวอย่าง AES อื่น ๆ ที่ไม่ได้ใช้
TedTrippin

ไม่สนใจสิ่งนั้นฉันพบคำตอบแล้ว! ฉันจำเป็นต้องใช้ AES / ECB / PKCS5Padding
TedTrippin

20
เวลาส่วนใหญ่คุณไม่ต้องการใช้ ECB แค่ Google ทำไม
João Fernandes

2
@Mushy: ตกลงกันว่าการเลือกและตั้งค่า IV อย่างชัดเจนจากแหล่งที่มาแบบสุ่มที่เชื่อถือได้นั้นดีกว่าการปล่อยให้อินสแตนซ์ Cihper หยิบขึ้นมา ในทางกลับกันคำตอบนี้ตอบปัญหาเดิมที่ทำให้เวกเตอร์การเริ่มต้นของคีย์สับสน ซึ่งเป็นเหตุผลว่าทำไมจึงได้รับการโหวตในตอนแรก ตอนนี้โพสต์นี้กลายเป็นโค้ดตัวอย่างมากขึ้นและผู้คนที่นี่ก็สร้างตัวอย่างที่ยอดเยี่ยมซึ่งอยู่ข้างๆคำถามเดิมเกี่ยวกับอะไร
GPI

3
@GPI โหวตขึ้น. "ตัวอย่างที่ยอดเยี่ยม" อื่น ๆ นั้นไม่ค่อยดีนักและไม่ได้ตอบคำถามเลย แต่ดูเหมือนว่านี่จะเป็นสถานที่สำหรับมือใหม่ในการคัดลอกตัวอย่างการเข้ารหัสแบบสุ่มสี่สุ่มห้าโดยไม่เข้าใจว่าอาจมีปัญหาด้านความปลอดภัยที่เป็นไปได้และเช่นเคยก็มีเช่นกัน
Maarten Bodewes

17

IV ที่คุณใช้สำหรับการถอดรหัสไม่ถูกต้อง แทนที่รหัสนี้

//Decrypt cipher
Cipher decryptCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec ivParameterSpec = new IvParameterSpec(aesKey.getEncoded());
decryptCipher.init(Cipher.DECRYPT_MODE, aesKey, ivParameterSpec);

ด้วยรหัสนี้

//Decrypt cipher
Cipher decryptCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec ivParameterSpec = new IvParameterSpec(encryptCipher.getIV());
decryptCipher.init(Cipher.DECRYPT_MODE, aesKey, ivParameterSpec);

และควรแก้ปัญหาของคุณ


ด้านล่างนี้มีตัวอย่างของคลาส AES อย่างง่ายใน Java ฉันไม่แนะนำให้ใช้คลาสนี้ในสภาพแวดล้อมการใช้งานจริงเนื่องจากอาจไม่ได้คำนึงถึงความต้องการเฉพาะทั้งหมดของแอปพลิเคชันของคุณ

import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class AES 
{
    public static byte[] encrypt(final byte[] keyBytes, final byte[] ivBytes, final byte[] messageBytes) throws InvalidKeyException, InvalidAlgorithmParameterException
    {       
        return AES.transform(Cipher.ENCRYPT_MODE, keyBytes, ivBytes, messageBytes);
    }

    public static byte[] decrypt(final byte[] keyBytes, final byte[] ivBytes, final byte[] messageBytes) throws InvalidKeyException, InvalidAlgorithmParameterException
    {       
        return AES.transform(Cipher.DECRYPT_MODE, keyBytes, ivBytes, messageBytes);
    }

    private static byte[] transform(final int mode, final byte[] keyBytes, final byte[] ivBytes, final byte[] messageBytes) throws InvalidKeyException, InvalidAlgorithmParameterException
    {
        final SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
        final IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
        byte[] transformedBytes = null;

        try
        {
            final Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");

            cipher.init(mode, keySpec, ivSpec);

            transformedBytes = cipher.doFinal(messageBytes);
        }        
        catch (NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException | BadPaddingException e) 
        {
            e.printStackTrace();
        }
        return transformedBytes;
    }

    public static void main(final String[] args) throws InvalidKeyException, InvalidAlgorithmParameterException
    {
        //Retrieved from a protected local file.
        //Do not hard-code and do not version control.
        final String base64Key = "ABEiM0RVZneImaq7zN3u/w==";

        //Retrieved from a protected database.
        //Do not hard-code and do not version control.
        final String shadowEntry = "AAECAwQFBgcICQoLDA0ODw==:ZtrkahwcMzTu7e/WuJ3AZmF09DE=";

        //Extract the iv and the ciphertext from the shadow entry.
        final String[] shadowData = shadowEntry.split(":");        
        final String base64Iv = shadowData[0];
        final String base64Ciphertext = shadowData[1];

        //Convert to raw bytes.
        final byte[] keyBytes = Base64.getDecoder().decode(base64Key);
        final byte[] ivBytes = Base64.getDecoder().decode(base64Iv);
        final byte[] encryptedBytes = Base64.getDecoder().decode(base64Ciphertext);

        //Decrypt data and do something with it.
        final byte[] decryptedBytes = AES.decrypt(keyBytes, ivBytes, encryptedBytes);

        //Use non-blocking SecureRandom implementation for the new IV.
        final SecureRandom secureRandom = new SecureRandom();

        //Generate a new IV.
        secureRandom.nextBytes(ivBytes);

        //At this point instead of printing to the screen, 
        //one should replace the old shadow entry with the new one.
        System.out.println("Old Shadow Entry      = " + shadowEntry);
        System.out.println("Decrytped Shadow Data = " + new String(decryptedBytes, StandardCharsets.UTF_8));
        System.out.println("New Shadow Entry      = " + Base64.getEncoder().encodeToString(ivBytes) + ":" + Base64.getEncoder().encodeToString(AES.encrypt(keyBytes, ivBytes, decryptedBytes)));
    }
}

โปรดทราบว่า AES ไม่มีส่วนเกี่ยวข้องกับการเข้ารหัสดังนั้นฉันจึงเลือกที่จะจัดการแยกต่างหากและไม่จำเป็นต้องใช้ไลบรารีของบุคคลที่สาม


ก่อนอื่นคุณยังไม่ได้ตอบคำถามเดิม ประการที่สองทำไมคุณถึงตอบคำถามที่ตอบแล้วและเป็นที่ยอมรับกันดี? ฉันคิดว่าการป้องกันควรจะหยุดสแปมนี้
TedTrippin

14
เช่นเดียวกับคำตอบที่ยอมรับฉันเลือกที่จะตอบคำถามของคุณผ่านทางตัวอย่าง ฉันให้โค้ดที่ใช้งานได้อย่างสมบูรณ์ซึ่งจะแสดงวิธีจัดการกับเวกเตอร์การเริ่มต้นอย่างถูกต้องเหนือสิ่งอื่นใด สำหรับคำถามที่สองของคุณฉันรู้สึกว่าจำเป็นต้องมีคำตอบที่อัปเดตแล้วเนื่องจากไม่จำเป็นต้องใช้ตัวแปลงสัญญาณ Apache อีกต่อไป ดังนั้นไม่นี่ไม่ใช่สแปม หยุด Trippin
k170

7
IVมีวัตถุประสงค์เฉพาะซึ่งเป็นไปสุ่มไซเฟอร์เท็กซ์และให้การรักษาความปลอดภัยความหมาย หากคุณใช้คีย์ + IV คู่เดียวกันผู้โจมตีสามารถระบุได้ว่าคุณได้ส่งข้อความที่มีคำนำหน้าเหมือนเดิมหรือไม่ IV ไม่จำเป็นต้องเป็นความลับ แต่ต้องไม่สามารถคาดเดาได้ วิธีทั่วไปคือเพียงนำหน้า IV เข้ากับไซเฟอร์เท็กซ์และตัดออกก่อนที่จะถอดรหัส
Artjom B.

4
downvote: hardcoded IV ดูที่ Artjom B. แสดงความคิดเห็นด้านบนว่าทำไมมันถึงไม่ดี
Murmel

1
ควรจับคู่โหมด CTR กับ NoPadding โหมด CTR แน่นอนไม่จำเป็นต้องแทน CBC (ยกเว้นกรณีที่ออราเคิล padding ใช้) แต่ถ้า CTR จะถูก"/NoPadding"นำมาใช้แล้วใช้งาน CTR คือโหมดที่เปลี่ยน AES ในการเข้ารหัสสตรีมและการเข้ารหัสสตรีมจะทำงานบนไบต์แทนที่จะเป็นบล็อก
Maarten Bodewes

16

ในคำตอบนี้ฉันเลือกที่จะเข้าใกล้ธีมหลัก "Simple Java AES encrypt / decrypt example" ไม่ใช่คำถามเฉพาะสำหรับการดีบักเพราะฉันคิดว่าสิ่งนี้จะเป็นประโยชน์ต่อผู้อ่านส่วนใหญ่

นี่คือสรุปง่ายๆในบล็อกโพสต์ของฉันเกี่ยวกับการเข้ารหัส AES ใน Javaดังนั้นฉันขอแนะนำให้อ่านก่อนที่จะนำไปใช้ อย่างไรก็ตามฉันจะยังคงให้ตัวอย่างง่ายๆในการใช้งานและให้คำแนะนำในสิ่งที่ต้องระวัง

ในตัวอย่างนี้ฉันจะเลือกใช้การเข้ารหัสที่ผ่านการรับรองความถูกต้องกับGalois / Counter Mode หรือโหมดGCM เหตุผลก็คือในกรณีส่วนใหญ่คุณต้องการ ความสมบูรณ์และความถูกต้องร่วมกับการรักษาความลับ (อ่านเพิ่มเติมในบล็อก )

การสอนการเข้ารหัส / ถอดรหัส AES-GCM

นี่เป็นขั้นตอนที่จำเป็นในการเข้ารหัส / ถอดรหัสกับAES-GCMกับJava Cryptography Architecture (JCA) อย่าผสมกับตัวอย่างอื่น ๆเนื่องจากความแตกต่างเล็กน้อยอาจทำให้โค้ดของคุณไม่ปลอดภัยอย่างที่สุด

1. สร้างคีย์

เนื่องจากขึ้นอยู่กับกรณีการใช้งานของคุณฉันจะถือว่ากรณีที่ง่ายที่สุดนั่นคือคีย์ลับแบบสุ่ม

SecureRandom secureRandom = new SecureRandom();
byte[] key = new byte[16];
secureRandom.nextBytes(key);
SecretKey secretKey = SecretKeySpec(key, "AES");

สำคัญ:

2. สร้างเวกเตอร์เริ่มต้น

เวกเตอร์เริ่มต้น (IV)ถูกนำมาใช้เพื่อให้คีย์ลับเดียวกันจะสร้างที่แตกต่างกันตำราตัวเลข

byte[] iv = new byte[12]; //NEVER REUSE THIS IV WITH SAME KEY
secureRandom.nextBytes(iv);

สำคัญ:

3. เข้ารหัสด้วย IV และ Key

final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv); //128 bit auth tag length
cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
byte[] cipherText = cipher.doFinal(plainText);

สำคัญ:

  • ใช้แท็กการตรวจสอบความถูกต้อง16 ไบต์ / 128 บิต(ใช้เพื่อตรวจสอบความสมบูรณ์ / ความถูกต้อง)
  • แท็กการตรวจสอบความถูกต้องจะต่อท้ายข้อความเข้ารหัสโดยอัตโนมัติ (ในการใช้งาน JCA)
  • เนื่องจาก GCM ทำงานเหมือนรหัสของสตรีมจึงไม่จำเป็นต้องมีช่องว่างภายใน
  • ใช้CipherInputStreamเมื่อเข้ารหัสข้อมูลจำนวนมาก
  • ต้องการตรวจสอบข้อมูลเพิ่มเติม (ไม่เป็นความลับ) ว่ามีการเปลี่ยนแปลงหรือไม่ คุณอาจต้องการใช้ข้อมูลที่เกี่ยวข้องกับcipher.updateAAD(associatedData); More here

3. จัดลำดับเป็นข้อความเดียว

เพียงต่อท้าย IV และ ciphertext ตามที่ระบุไว้ข้างต้น IV ไม่จำเป็นต้องเป็นความลับ

ByteBuffer byteBuffer = ByteBuffer.allocate(iv.length + cipherText.length);
byteBuffer.put(iv);
byteBuffer.put(cipherText);
byte[] cipherMessage = byteBuffer.array();

เลือกที่จะเข้ารหัสด้วยBase64หากคุณต้องการการแสดงสตริง ใช้การติดตั้งในตัวของ AndroidหรือJava 8 (อย่าใช้ Apache Commons Codec - เป็นการใช้งานที่แย่มาก) การเข้ารหัสใช้เพื่อ "แปลง" ไบต์อาร์เรย์เป็นการแสดงสตริงเพื่อให้ ASCII ปลอดภัยเช่น:

String base64CipherMessage = Base64.getEncoder().encodeToString(cipherMessage);

4. เตรียมถอดรหัส: Deserialize

หากคุณเข้ารหัสข้อความก่อนอื่นให้ถอดรหัสเป็นไบต์อาร์เรย์:

byte[] cipherMessage = Base64.getDecoder().decode(base64CipherMessage)

สำคัญ:

5. ถอดรหัส

เริ่มต้นการเข้ารหัสและตั้งค่าพารามิเตอร์เดียวกันกับการเข้ารหัส:

final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
//use first 12 bytes for iv
AlgorithmParameterSpec gcmIv = new GCMParameterSpec(128, cipherMessage, 0, 12);
cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmIv);
//use everything from 12 bytes on as ciphertext
byte[] plainText = cipher.doFinal(cipherMessage, 12, cipherMessage.length - 12);

สำคัญ:

คุณสามารถดูข้อมูลโค้ดที่ใช้งานได้ในส่วนสำคัญนี้


โปรดทราบว่าการใช้งาน Android (SDK 21+) และ Java (7+) ล่าสุดควรมี AES-GCM รุ่นเก่าอาจขาดมัน ฉันยังคงเลือกโหมดนี้เนื่องจากใช้งานได้ง่ายกว่านอกจากจะมีประสิทธิภาพมากกว่าเมื่อเทียบกับโหมดเข้ารหัสที่คล้ายกันของMac (เช่นAES-CBC + HMAC ) ดูบทความนี้เกี่ยวกับวิธีใช้ AES-CBC กับ HMAC


ปัญหาคือการขอตัวอย่างนั้นไม่ชัดเจนในหัวข้อ SO และปัญหาที่ใหญ่กว่าคือโค้ดเหล่านี้เป็นโค้ดที่ยังไม่ได้ตรวจสอบซึ่งยากที่จะตรวจสอบความถูกต้อง ฉันขอขอบคุณในความพยายามนี้ แต่ฉันไม่คิดว่า SO ควรจะเป็นสถานที่สำหรับสิ่งนี้
Maarten Bodewes

1
ฉันชื่นชมในความพยายามดังนั้นฉันจะชี้ให้เห็นข้อผิดพลาดเพียงข้อเดียว: "iv ต้องไม่สามารถคาดเดาได้ร่วมกับความเป็นเอกลักษณ์ (เช่นใช้ iv สุ่ม)" - นี่เป็นจริงสำหรับโหมด CBC แต่ไม่ใช่สำหรับ GCM
Maarten Bodewes

this is true for CBC mode but not for GCMคุณหมายถึงส่วนทั้งหมดหรือเพียง แต่ไม่จำเป็นต้องคาดเดาไม่ได้?
Patrick Favre

1
"ถ้าคุณไม่ได้รับหัวข้อคุณก็ไม่ควรใช้พื้นฐานระดับต่ำตั้งแต่แรก" แน่นอนว่าน่าจะเป็นเช่นนั้นนักพัฒนาหลายคนยังคงทำเช่นนั้น ฉันไม่แน่ใจว่าการละเว้นจากการวางเนื้อหาที่มีคุณภาพสูงเกี่ยวกับความปลอดภัย / การเข้ารหัสในสถานที่ซึ่งมักจะไม่มีทางออกที่เหมาะสมสำหรับสิ่งนี้มากนัก - ขอบคุณสำหรับการชี้ไปที่ความผิดพลาดของฉัน
Patrick Favre

1
ตกลงเพียงเพราะฉันชอบเนื้อหาคำตอบ wrt (มากกว่าวัตถุประสงค์): การจัดการ IV สามารถทำให้ง่ายขึ้นโดยเฉพาะในระหว่างการถอดรหัส: Java ทำให้ง่ายต่อการสร้าง IV โดยตรงจากอาร์เรย์ไบต์ที่มีอยู่หลังจากทั้งหมด เช่นเดียวกันกับการถอดรหัสซึ่งไม่จำเป็นต้องเริ่มต้นที่ออฟเซ็ต 0 การคัดลอกทั้งหมดนี้ไม่จำเป็น นอกจากนี้ถ้าคุณต้องส่งความยาวสำหรับ IV (คุณหรือไม่) ทำไมไม่ใช้ไบต์เดียว (ไม่ได้ลงชื่อ) - คุณจะไม่เกิน 255 ไบต์สำหรับ IV ใช่ไหม?
Maarten Bodewes

2

เวอร์ชันออนไลน์ Editor Runnable: -

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
//import org.apache.commons.codec.binary.Base64;
import java.util.Base64;

public class Encryptor {
    public static String encrypt(String key, String initVector, String value) {
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));

            SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
            cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);

            byte[] encrypted = cipher.doFinal(value.getBytes());

            //System.out.println("encrypted string: "
              //      + Base64.encodeBase64String(encrypted));

            //return Base64.encodeBase64String(encrypted);
            String s = new String(Base64.getEncoder().encode(encrypted));
            return s;
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

    public static String decrypt(String key, String initVector, String encrypted) {
        try {
            IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
            SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");

            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);

            byte[] original = cipher.doFinal(Base64.getDecoder().decode(encrypted));

            return new String(original);
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        return null;
    }

    public static void main(String[] args) {
        String key = "Bar12345Bar12345"; // 128 bit key
        String initVector = "RandomInitVector"; // 16 bytes IV

        System.out.println(encrypt(key, initVector, "Hello World"));
        System.out.println(decrypt(key, initVector, encrypt(key, initVector, "Hello World")));
    }
}

เจ๋งมีความสุข!
Bhupesh Pant

รหัสผ่านไม่ใช่กุญแจ IV ไม่ควรเป็นแบบคงที่ ยังคงพิมพ์รหัสอย่างเข้มงวดซึ่งทำให้ไม่สามารถทำลายคีย์ได้ ไม่มีข้อบ่งชี้ว่าจะทำอย่างไรกับ IV หรือความคิดใด ๆ ที่ไม่ควรคาดเดา
Maarten Bodewes

1

มักเป็นความคิดที่ดีที่จะพึ่งพาโซลูชันมาตรฐานที่ให้มา:

private static void stackOverflow15554296()
    throws
        NoSuchAlgorithmException, NoSuchPaddingException,        
        InvalidKeyException, IllegalBlockSizeException,
        BadPaddingException
{

    // prepare key
    KeyGenerator keygen = KeyGenerator.getInstance("AES");
    SecretKey aesKey = keygen.generateKey();
    String aesKeyForFutureUse = Base64.getEncoder().encodeToString(
            aesKey.getEncoded()
    );

    // cipher engine
    Cipher aesCipher = Cipher.getInstance("AES/ECB/PKCS5Padding");

    // cipher input
    aesCipher.init(Cipher.ENCRYPT_MODE, aesKey);
    byte[] clearTextBuff = "Text to encode".getBytes();
    byte[] cipherTextBuff = aesCipher.doFinal(clearTextBuff);

    // recreate key
    byte[] aesKeyBuff = Base64.getDecoder().decode(aesKeyForFutureUse);
    SecretKey aesDecryptKey = new SecretKeySpec(aesKeyBuff, "AES");

    // decipher input
    aesCipher.init(Cipher.DECRYPT_MODE, aesDecryptKey);
    byte[] decipheredBuff = aesCipher.doFinal(cipherTextBuff);
    System.out.println(new String(decipheredBuff));
}

สิ่งนี้จะพิมพ์ "ข้อความที่จะเข้ารหัส"

โซลูชันเป็นไปตามคู่มืออ้างอิงสถาปัตยกรรมการเข้ารหัส Javaและคำตอบhttps://stackoverflow.com/a/20591539/146745


5
ห้ามใช้โหมด ECB ระยะเวลา
Konstantino Sparakis

1
ไม่ควรใช้ ECB หากเข้ารหัสข้อมูลมากกว่าหนึ่งบล็อกด้วยคีย์เดียวกันดังนั้นสำหรับ "ข้อความที่จะเข้ารหัส" จึงดีพอ stackoverflow.com/a/1220869/146745
andrej

คีย์ @AndroidDev ถูกสร้างขึ้นในส่วนเตรียมคีย์: aesKey = keygen.generateKey ()
andrej

1

นี่เป็นการปรับปรุงคำตอบที่ได้รับการยอมรับ

การเปลี่ยนแปลง:

(1) ใช้ IV แบบสุ่มและนำหน้าไปไว้ข้างหน้าข้อความที่เข้ารหัส

(2) การใช้ SHA-256 เพื่อสร้างคีย์จากข้อความรหัสผ่าน

(3) ไม่มีการพึ่งพา Apache Commons

public static void main(String[] args) throws GeneralSecurityException {
    String plaintext = "Hello world";
    String passphrase = "My passphrase";
    String encrypted = encrypt(passphrase, plaintext);
    String decrypted = decrypt(passphrase, encrypted);
    System.out.println(encrypted);
    System.out.println(decrypted);
}

private static SecretKeySpec getKeySpec(String passphrase) throws NoSuchAlgorithmException {
    MessageDigest digest = MessageDigest.getInstance("SHA-256");
    return new SecretKeySpec(digest.digest(passphrase.getBytes(UTF_8)), "AES");
}

private static Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException {
    return Cipher.getInstance("AES/CBC/PKCS5PADDING");
}

public static String encrypt(String passphrase, String value) throws GeneralSecurityException {
    byte[] initVector = new byte[16];
    SecureRandom.getInstanceStrong().nextBytes(initVector);
    Cipher cipher = getCipher();
    cipher.init(Cipher.ENCRYPT_MODE, getKeySpec(passphrase), new IvParameterSpec(initVector));
    byte[] encrypted = cipher.doFinal(value.getBytes());
    return DatatypeConverter.printBase64Binary(initVector) +
            DatatypeConverter.printBase64Binary(encrypted);
}

public static String decrypt(String passphrase, String encrypted) throws GeneralSecurityException {
    byte[] initVector = DatatypeConverter.parseBase64Binary(encrypted.substring(0, 24));
    Cipher cipher = getCipher();
    cipher.init(Cipher.DECRYPT_MODE, getKeySpec(passphrase), new IvParameterSpec(initVector));
    byte[] original = cipher.doFinal(DatatypeConverter.parseBase64Binary(encrypted.substring(24)));
    return new String(original);
}

แฮชยังคงไม่ใช่ฟังก์ชันการสร้างคีย์ตามรหัสผ่าน / PBKDF ไม่ว่าคุณจะใช้คีย์แบบสุ่มหรือคุณใช้ PBKDF เช่น PBKDF2 / การเข้ารหัสตามรหัสผ่าน
Maarten Bodewes

@MaartenBodewes คุณสามารถแนะนำการปรับปรุงได้หรือไม่?
wvdz

PBKDF2 มีอยู่ใน Java ดังนั้นฉันคิดว่าฉันเพิ่งแนะนำ ตกลงฉันไม่ได้เขียนโค้ดแต่นั่นคือความคิดของฉันมากเกินไป มีตัวอย่างมากมายของการเข้ารหัสด้วยรหัสผ่าน
Maarten Bodewes

@MaartenBodewes ฉันคิดว่ามันอาจจะแก้ไขได้ง่ายๆ ด้วยความอยากรู้อยากเห็นอะไรคือช่องโหว่ที่เฉพาะเจาะจงเมื่อใช้รหัสนี้ตามที่เป็นอยู่
wvdz

0

อีกวิธีหนึ่งโดยใช้ java.util.Base64 พร้อม Spring Boot

คลาสเข้ารหัส

package com.jmendoza.springboot.crypto.cipher;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

@Component
public class Encryptor {

    @Value("${security.encryptor.key}")
    private byte[] key;
    @Value("${security.encryptor.algorithm}")
    private String algorithm;

    public String encrypt(String plainText) throws Exception {
        SecretKeySpec secretKey = new SecretKeySpec(key, algorithm);
        Cipher cipher = Cipher.getInstance(algorithm);
        cipher.init(Cipher.ENCRYPT_MODE, secretKey);
        return new String(Base64.getEncoder().encode(cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8))));
    }

    public String decrypt(String cipherText) throws Exception {
        SecretKeySpec secretKey = new SecretKeySpec(key, algorithm);
        Cipher cipher = Cipher.getInstance(algorithm);
        cipher.init(Cipher.DECRYPT_MODE, secretKey);
        return new String(cipher.doFinal(Base64.getDecoder().decode(cipherText)));
    }
}

คลาส EncryptorController

package com.jmendoza.springboot.crypto.controller;

import com.jmendoza.springboot.crypto.cipher.Encryptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/cipher")
public class EncryptorController {

    @Autowired
    Encryptor encryptor;

    @GetMapping(value = "encrypt/{value}")
    public String encrypt(@PathVariable("value") final String value) throws Exception {
        return encryptor.encrypt(value);
    }

    @GetMapping(value = "decrypt/{value}")
    public String decrypt(@PathVariable("value") final String value) throws Exception {
        return encryptor.decrypt(value);
    }
}

application.properties

server.port=8082
security.encryptor.algorithm=AES
security.encryptor.key=M8jFt46dfJMaiJA0

ตัวอย่าง

http: // localhost: 8082 / ตัวเลข / เข้ารหัส / jmendoza

2h41HH8Shzc4BRU3hVDOXA ==

http: // localhost: 8082 / การเข้ารหัส / ถอดรหัส / 2h41HH8Shzc4BRU3hVDOXA ==

jmendoza


-1

คำตอบที่ยอมรับในเวอร์ชันที่ปรับให้เหมาะสม

  • ไม่มี libs ของบุคคลที่สาม

  • รวม IV ลงในข้อความที่เข้ารหัส (สามารถเป็นสาธารณะ)

  • รหัสผ่านสามารถมีความยาวเท่าใดก็ได้

รหัส:

import java.io.UnsupportedEncodingException;
import java.security.SecureRandom;
import java.util.Base64;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class Encryptor {
    public static byte[] getRandomInitialVector() {
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            SecureRandom randomSecureRandom = SecureRandom.getInstance("SHA1PRNG");
            byte[] initVector = new byte[cipher.getBlockSize()];
            randomSecureRandom.nextBytes(initVector);
            return initVector;
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

    public static byte[] passwordTo16BitKey(String password) {
        try {
            byte[] srcBytes = password.getBytes("UTF-8");
            byte[] dstBytes = new byte[16];

            if (srcBytes.length == 16) {
                return srcBytes;
            }

            if (srcBytes.length < 16) {
                for (int i = 0; i < dstBytes.length; i++) {
                    dstBytes[i] = (byte) ((srcBytes[i % srcBytes.length]) * (srcBytes[(i + 1) % srcBytes.length]));
                }
            } else if (srcBytes.length > 16) {
                for (int i = 0; i < srcBytes.length; i++) {
                    dstBytes[i % dstBytes.length] += srcBytes[i];
                }
            }

            return dstBytes;
        } catch (UnsupportedEncodingException ex) {
            ex.printStackTrace();
        }

        return null;
    }

    public static String encrypt(String key, String value) {
        return encrypt(passwordTo16BitKey(key), value);
    }

    public static String encrypt(byte[] key, String value) {
        try {
            byte[] initVector = Encryptor.getRandomInitialVector();
            IvParameterSpec iv = new IvParameterSpec(initVector);
            SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");

            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);

            byte[] encrypted = cipher.doFinal(value.getBytes());
            return Base64.getEncoder().encodeToString(encrypted) + " " + Base64.getEncoder().encodeToString(initVector);
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        return null;
    }

    public static String decrypt(String key, String encrypted) {
        return decrypt(passwordTo16BitKey(key), encrypted);
    }

    public static String decrypt(byte[] key, String encrypted) {
        try {
            String[] encryptedParts = encrypted.split(" ");
            byte[] initVector = Base64.getDecoder().decode(encryptedParts[1]);
            if (initVector.length != 16) {
                return null;
            }

            IvParameterSpec iv = new IvParameterSpec(initVector);
            SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");

            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);

            byte[] original = cipher.doFinal(Base64.getDecoder().decode(encryptedParts[0]));

            return new String(original);
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        return null;
    }
}

การใช้งาน:

String key = "Password of any length.";
String encrypted = Encryptor.encrypt(key, "Hello World");
String decrypted = Encryptor.decrypt(key, encrypted);
System.out.println(encrypted);
System.out.println(decrypted);

ตัวอย่างผลลัพธ์:

QngBg+Qc5+F8HQsksgfyXg== yDfYiIHTqOOjc0HRNdr1Ng==
Hello World

ฟังก์ชันการสร้างรหัสผ่านของคุณไม่ปลอดภัย ฉันไม่คาดหวังe.printStackTrace()ในสิ่งที่เรียกว่าโค้ดที่ปรับให้เหมาะสม
Maarten Bodewes
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.