Java加密算法之单密钥对称加密【2】

位置:首页>文章>详情   分类: Java教程 > 编程技术   阅读(88)   2024-05-24 06:20:14

抽象的

这是涵盖 Java 加密算法的三部分博客系列的第 2 部分。该系列涵盖了如何实现以下内容:

  1. 使用 SHA–512 进行哈希处理
  2. AES–256
  3. RSA–4096

第 2 篇文章详细介绍了如何实现单密钥、对称、AES-256 加密。让我们开始吧。

免责声明

这篇文章仅供参考。在使用所提供的任何信息之前,请认真思考。从中吸取教训,但最终做出自己的决定需要您自担风险。

要求

我使用以下主要技术完成了这篇文章的所有工作。您也许可以使用不同的技术或版本做同样的事情,但不能保证。

  • Java 1.8.0_152_x64
  • Java 加密扩展 (JCE) 无限强度
  • NetBeans 8.2(内部版本 201609300101)
  • Maven 3.0.5(与 NetBeans 捆绑)

下载

访问我的 GitHub 页面,查看我所有的开源项目。这篇文章的代码位于项目中:thoth-cryptography

对称加密

关于

对称加密算法基于单个密钥。这一个密钥用于加密和解密。因此,对称算法只应在实施严格控制以保护密钥的情况下使用。

对称算法通常用于安全环境中的数据加密和解密。一个很好的例子是保护微服务通信。如果 OAuth–2/JWT 架构超出范围,API 网关可以使用对称算法的单一密钥来加密令牌。然后将此令牌传递给其他微服务。其他微服务使用相同的密钥来解密令牌。另一个很好的例子是嵌入在电子邮件中的超链接。电子邮件中的超链接包含一个编码令牌,允许在单击超链接时自动处理登录请求。此令牌是由对称算法生成的高度加密值,因此只能在应用服务器上对其进行解码。当然,无论何时需要保护任何类型的密码或凭据,都会使用对称算法对其进行加密,之后可以使用相同的密钥对字节进行解密。

截至今天所做的研究似乎表明最好和最安全的单密钥、对称加密算法如下(Sheth,2017 年,“选择正确的算法”,第 2 段):

  1. 算法:AES
  2. 模式:GCM
  3. 填充:PKCS5Padding
  4. 密钥大小:256 位
  5. IV 大小:96 位

AES–256 使用 256 位密钥,需要安装 Java Cryptography Extension (JCE) Unlimited Strength 包。让我们看一个例子。

注意:Java 加密扩展 (JCE) 无限强度 包是 256 位密钥所必需的。如果未安装,则最大为 128 位密钥。

例子

如果您还没有,请下载并安装 Java Cryptography Extension (JCE) Unlimited Strength 包。要求使用 256 位密钥。否则,必须更新以下示例以使用 128 位密钥。

清单 1 是 AesTest.java 单元测试。这是对以下内容的完整演示:

  1. 生成并存储 AES 256 位密钥
  2. AES 加密
  3. AES 解密

清单 2 显示了 AesSecretKeyProducer.java。这是一个帮助程序类,负责生成新密钥或从 byte[] 复制现有密钥。

清单 3 显示了 ByteArrayWriter.java,清单 4 显示了 ByteArrayReader.java。这些是负责将 byte[] 读取和写入文件的辅助类。由您决定如何存储密钥的 byte[],但它需要安全地存储在某个地方(文件、数据库、git 存储库等)。

最后,清单 5 显示了 Aes.java。这是一个帮助程序类,负责加密和解密。

清单 1 – AesTest.java 类

package org.thoth.crypto.symmetric;

import java.io.ByteArrayOutputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import javax.crypto.SecretKey;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.thoth.crypto.io.ByteArrayReader;
import org.thoth.crypto.io.ByteArrayWriter;

/**
 *
 * @author Michael Remijan mjremijan@yahoo.com @mjremijan
 */
public class AesTest {

    static Path secretKeyFile;

    @BeforeClass
    public static void beforeClass() throws Exception {
        // Store the SecretKey bytes in the ./target diretory. Do
        // this so it will be ignore by source control.  We don't
        // want this file committed.
        secretKeyFile
            = Paths.get("./target/Aes256.key").toAbsolutePath();

        // Generate a SecretKey for the test
        SecretKey secretKey
            = new AesSecretKeyProducer().produce();

        // Store the byte[] of the SecretKey.  This is the
        // "private key file" you want to keep safe.
        ByteArrayWriter writer = new ByteArrayWriter(secretKeyFile);
        writer.write(secretKey.getEncoded());
    }


    @Test
    public void encrypt_and_decrypt_using_same_Aes256_instance() {
        // setup
        SecretKey secretKey
            = new AesSecretKeyProducer().produce(
                new ByteArrayReader(secretKeyFile).read()
            );

        Aes aes
            = new Aes(secretKey);

        String toEncrypt
            = "encrypt me";

        // run
        byte[] encryptedBytes
            = aes.encrypt(toEncrypt, Optional.empty());

        String decrypted
            = aes.decrypt(encryptedBytes, Optional.empty());

        // assert
        Assert.assertEquals(toEncrypt, decrypted);
    }


    public void encrypt_and_decrypt_with_aad_using_same_Aes256_instance() {
        // setup
        SecretKey secretKey
            = new AesSecretKeyProducer().produce(
                new ByteArrayReader(secretKeyFile).read()
            );

        Aes aes
            = new Aes(secretKey);

        String toEncrypt
            = "encrypt me aad";

        // run
        byte[] encryptedBytes
            = aes.encrypt(toEncrypt, Optional.of("JUnit AAD"));

        String decrypted
            = aes.decrypt(encryptedBytes, Optional.of("JUnit AAD"));

        // assert
        Assert.assertEquals(toEncrypt, decrypted);
    }


    @Test
    public void encrypt_and_decrypt_using_different_Aes256_instance()
    throws Exception {
        // setup
        SecretKey secretKey
            = new AesSecretKeyProducer().produce(
                new ByteArrayReader(secretKeyFile).read()
            );

        Aes aesForEncrypt
            = new Aes(secretKey);

        Aes aesForDecrypt
            = new Aes(secretKey);

        String toEncrypt
            = "encrypt me";

        // run
        byte[] encryptedBytes
            = aesForEncrypt.encrypt(toEncrypt, Optional.empty());

        ByteArrayOutputStream baos
            = new ByteArrayOutputStream();
        baos.write(encryptedBytes);

        String decrypted
            = aesForDecrypt.decrypt(baos.toByteArray(), Optional.empty());

        // assert
        Assert.assertEquals(toEncrypt, decrypted);
    }
}

清单 2 – AesSecretKeyProducer.java 类

package org.thoth.crypto.symmetric;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

/**
 *
 * @author Michael Remijan mjremijan@yahoo.com @mjremijan
 */
public class AesSecretKeyProducer {

    /**
     * Generates a new AES-256 bit {@code SecretKey}.
     *
     * @return {@code SecretKey}, never null
     * @throws RuntimeException All exceptions are caught and re-thrown as {@code RuntimeException}
     */
    public SecretKey produce() {
        KeyGenerator keyGen;
        try {
            keyGen = KeyGenerator.getInstance("AES");
            keyGen.init(256);
            SecretKey secretKey = keyGen.generateKey();
            return secretKey;
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }


    /**
     * Generates an AES-256 bit {@code SecretKey}.
     *
     * @param encodedByteArray The bytes this method will use to regenerate a previously created {@code SecretKey}
     *
     * @return {@code SecretKey}, never null
     * @throws RuntimeException All exceptions are caught and re-thrown as {@code RuntimeException}
     */
    public SecretKey produce(byte [] encodedByteArray) {
        try {
            return new SecretKeySpec(encodedByteArray, "AES");
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }
}

清单 3 – ByteArrayWriter.java 类

package org.thoth.crypto.io;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;

/**
 *
 * @author Michael Remijan mjremijan@yahoo.com @mjremijan
 */
public class ByteArrayWriter {

    protected Path outputFile;

    private void initOutputFile(Path outputFile) {
        this.outputFile = outputFile;
    }

    private void initOutputDirectory() {
        Path outputDirectory = outputFile.getParent();
        if (!Files.exists(outputDirectory)) {
            try {
                Files.createDirectories(outputDirectory);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public ByteArrayWriter(Path outputFile) {
        initOutputFile(outputFile);
        initOutputDirectory();
    }

    public void write(byte[] bytesArrayToWrite) {
        try (
            OutputStream os
                = Files.newOutputStream(outputFile);

            PrintWriter writer
                =  new PrintWriter(os);
        ){
            for (int i=0; i<bytesArrayToWrite.length; i++) {
                if (i>0) {
                    writer.println();
                }
                writer.print(bytesArrayToWrite[i]);
            }
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }
}

清单 4 – ByteArrayReader.java 类

package org.thoth.crypto.io;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Scanner;

/**
 *
 * @author Michael Remijan mjremijan@yahoo.com @mjremijan
 */
public class ByteArrayReader {

    protected Path inputFile;

    public ByteArrayReader(Path inputFile) {
        this.inputFile = inputFile;
    }

    public byte[] read() {
        try (
            Scanner scanner
                =  new Scanner(inputFile);

            ByteArrayOutputStream baos
                = new ByteArrayOutputStream();
        ){
            while (scanner.hasNext()) {
                baos.write(Byte.parseByte(scanner.nextLine()));
            }
            
            baos.flush();
            return baos.toByteArray();

        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }
}

清单 5 – Aes.java 类

package org.thoth.crypto.symmetric;

import java.security.SecureRandom;
import java.util.Optional;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;

/**
 *
 * @author Michael Remijan mjremijan@yahoo.com @mjremijan
 */
public class Aes {

    // If you don't have the Java Cryptography Extension
    // (JCE) Unlimited Strength packaged installed, use
    // a 128 bit KEY_SIZE.
    public static int KEY_SIZE = 256;
    
    public static int IV_SIZE = 12; // 12bytes * 8 = 96bits
    public static int TAG_BIT_SIZE = 128;
    public static String ALGORITHM_NAME = "AES";
    public static String MODE_OF_OPERATION = "GCM";
    public static String PADDING_SCHEME = "PKCS5Padding";

    protected SecretKey secretKey;
    protected SecureRandom secureRandom;

    public Aes(SecretKey secretKey) {
        this.secretKey = secretKey;
        this.secureRandom = new SecureRandom();
    }


    public byte[] encrypt(String message, Optional<String> aad) {
        try {
            // Transformation specifies algortihm, mode of operation and padding
            Cipher c = Cipher.getInstance(
                String.format("%s/%s/%s",ALGORITHM_NAME,MODE_OF_OPERATION,PADDING_SCHEME)
            );

            // Generate IV
            byte iv[] = new byte[IV_SIZE];
            secureRandom.nextBytes(iv); // SecureRandom initialized using self-seeding

            // Initialize GCM Parameters
            GCMParameterSpec spec = new GCMParameterSpec(TAG_BIT_SIZE, iv);

            // Init for encryption
            c.init(Cipher.ENCRYPT_MODE, secretKey, spec, secureRandom);

            // Add AAD tag data if present
            aad.ifPresent(t -> {
                try {
                    c.updateAAD(t.getBytes("UTF-8"));
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            });

            // Add message to encrypt
            c.update(message.getBytes("UTF-8"));

            // Encrypt
            byte[] encryptedBytes
                = c.doFinal();


            // Concatinate IV and encrypted bytes.  The IV is needed later
            // in order to to decrypt.  The IV value does not need to be
            // kept secret, so it's OK to encode it in the return value
            //
            // Create a new byte[] the combined length of IV and encryptedBytes
            byte[] ivPlusEncryptedBytes = new byte[iv.length + encryptedBytes.length];
            // Copy IV bytes into the new array
            System.arraycopy(iv, 0, ivPlusEncryptedBytes, 0, iv.length);
            // Copy encryptedBytes into the new array
            System.arraycopy(encryptedBytes, 0, ivPlusEncryptedBytes, iv.length, encryptedBytes.length);

            // Return
            return ivPlusEncryptedBytes;

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


    public String decrypt(byte[] ivPlusEncryptedBytes, Optional<String> aad) {

        try {
            // Get IV
            byte iv[] = new byte[IV_SIZE];
            System.arraycopy(ivPlusEncryptedBytes, 0, iv, 0, IV_SIZE);

            // Initialize GCM Parameters
            GCMParameterSpec spec = new GCMParameterSpec(TAG_BIT_SIZE, iv);

            // Transformation specifies algortihm, mode of operation and padding
            Cipher c = Cipher.getInstance(
                String.format("%s/%s/%s",ALGORITHM_NAME,MODE_OF_OPERATION,PADDING_SCHEME)
            );

            // Get encrypted bytes
            byte [] encryptedBytes = new byte[ivPlusEncryptedBytes.length - IV_SIZE];
            System.arraycopy(ivPlusEncryptedBytes, IV_SIZE, encryptedBytes, 0, encryptedBytes.length);

            // Init for decryption
            c.init(Cipher.DECRYPT_MODE, secretKey, spec, secureRandom);

            // Add AAD tag data if present
            aad.ifPresent(t -> {
                try {
                    c.updateAAD(t.getBytes("UTF-8"));
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            });

            // Add message to decrypt
            c.update(encryptedBytes);

            // Decrypt
            byte[] decryptedBytes
                = c.doFinal();

            // Return
            return new String(decryptedBytes, "UTF-8");

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

概括

加密并不容易。简单的示例将导致您的应用程序具有安全漏洞的实现。如果您需要单个密钥、对称加密算法,请使用具有 256 位密钥和 96 位 IV 的密码 AES/GCM/PKCS5Padding。

参考

  • Java 加密扩展 (JCE) 无限强度。 (日期不详)。取自 http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html
  • Sheth, M.(2017 年,4 月 18 日)。 Java密码学中的加密和解密。取自 https://www.veracode.com/blog/research/encryption-and-decryption-java-cryptography
  • cpast[ 说 GCM IV 是 96 位,即 96/8 = 12 字节]。 (2015 年 6 月 4 日)。使用 AES–256 加密,我可以使用 256 位 IV [Web 日志评论]。从 https://security.stackexchange.com/questions/90848/encrypting-using-aes-256-can-i-use-256-bits-iv 中检索。
  • Bodewes[ 表示强烈建议 GCM IV 为 12 字节 (12*8 = 96),但可以是任何大小。其他尺寸需要额外计算],M.(2015 年,7 月 7 日)。 GCM 模式下使用 AES 的密文和标签大小以及 IV 传输 [网络日志评论]。取自 https://crypto.stackexchange.com/questions/26783/ciphertext-and-tag-size-and-iv-transmission-with-aes-in-gcm-mode
  • 无花果鱿鱼。 (2013 年,10 月 18 日)。 “密码”和“操作模式”之间有什么区别? [网络日志评论]。取自 https://crypto.stackexchange.com/questions/11132/what-is-the-difference-between-a-cipher-and-a-mode-of-operation
  • Toust, S.(2013 年,2 月 4 日)。为什么对称和非对称加密之间推荐的密钥大小差异很大?。取自 https://crypto.stackexchange.com/questions/6236/why-does-the-recommended-key-size-between-symmetric-and-asymmetric-encryption-di
  • Karonen, I.(2012 年,10 月 5 日)。密钥、IV 和随机数之间的主要区别是什么?取自 https://crypto.stackexchange.com/questions/3965/what-is-the-main-difference-between-a-key-an-iv-and-a-nonce
  • 分组密码操作模式。 (2017 年 11 月 6 日)。维基百科。取自 https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Initialization_vector_.28IV.29
标签2: Java教程
地址:https://www.cundage.com/article/jcg-choosing-java-cryptographic-algorithms-part-2-single-key-symmetric-encryption.html

相关阅读

Java HashSet 教程展示了如何使用 Java HashSet 集合。 Java哈希集 HashSet 是一个不包含重复元素的集合。此类为基本操作(添加、删除、包含和大小)提供恒定时间性...
SpringApplicationBuilder 教程展示了如何使用 SpringApplicationBuilder 创建一个简单的 Spring Boot 应用程序。 春天 是用于创建企业应...
通道是继 buffers 之后 java.nio 的第二个主要新增内容,我们在之前的教程中已经详细了解了这一点。通道提供与 I/O 服务的直接连接。 通道是一种在字节缓冲区和通道另一端的实体(通...
课程大纲 Elasticsearch 是一个基于 Lucene 的搜索引擎。它提供了一个分布式的、支持多租户的全文搜索引擎,带有 HTTP Web 界面和无模式的 JSON 文档。 Elasti...
解析器是强大的工具,使用 ANTLR 可以编写可用于多种不同语言的各种解析器。 在这个完整的教程中,我们将: 解释基础:什么是解析器,它可以用来做什么 查看如何设置 ANTLR 以便在 Java...
Java 是用于开发各种桌面应用程序、Web 应用程序和移动应用程序的最流行的编程语言之一。以下文章将帮助您快速熟悉 Java 语言,并迈向 API 和云开发等更复杂的概念。 1. Java语言...
Java中的继承是指子类继承或获取父类的所有非私有属性和行为的能力。继承是面向对象编程的四大支柱之一,用于提高层次结构中类之间的代码可重用性。 在本教程中,我们将了解 Java 支持的继承类型,...
Java Message Service 是一种支持正式通信的 API,称为 网络上计算机之间的消息传递。 JMS 为支持 Java 程序的标准消息协议和消息服务提供了一个通用接口。 JMS 提...
之前,我介绍了spring 3 + hibernate 集成 示例和struts 2 hello world 示例。在本教程中,我将讨论在将 spring 框架与 struts 与 hibern...
Java 项目中的一项常见任务是将日期格式化或解析为字符串,反之亦然。解析日期意味着你有一个代表日期的字符串,例如“2017-08-3”,你想把它转换成一个代表 Java 中日期的对象,例如Ja...