如何从 Java 编写加密的 ECDSA 私钥文件

如何解决如何从 Java 编写加密的 ECDSA 私钥文件

我有一个 Java 服务,它将生成一个 ECDSA 公钥/私钥对。我想将使用我在服务中管理的随机生成的密钥加密的公钥和私钥写入本地文件系统。

我显然可以使用 base64 对密钥的字节进行编码并将其写入文件,或者我可以以我自己创建的二进制格式编写它们。但如果可能的话,我更愿意以 PEM 或 DER 等标准化格式将它们写出来。我可以为未加密的公钥计算出这一点,但我正在努力弄清楚如何从 Java 中 为加密的私钥做到这一点。

我知道我可以调用操作系统并在命令行上调用 openssl,但是 (a) 我宁愿在 Java 中本地执行此操作,并且 (b) 我已经阅读了许多帖子,建议使用 openssl 的算法对密钥进行编码并不是特别安全。因此,我希望使用 Java 密码体系结构 (JCA) API 使用我选择的算法加密私钥,然后将加密的字节包装在任何需要的地方,使其成为有效的 PEM 或 DER 格式的文件。

我怀疑有像 BouncyCastle 这样的库可以让这变得更容易,如有必要,我可能会使用这样的库。但我的公司经营受监管的软件,为所有现成 (OTS) 软件带来持续的官僚维护成本,因此理想的解决方案是我可以使用标准 JCA 类(目前使用 Java 11)直接用 Java 编写的东西).

如果您就如何解决这个问题提出任何想法和建议,我将不胜感激。

解决方法

只要您留在 Java 中(我的意思是您不想与其他系统交换(加密的)私钥),我建议对已编码的私钥进行加密 - 这样您完全使用 Java 的内置资源。尝试使用“加密 PEM 格式”需要使用像 Bouncy Castle 这样的外部库。

以下解决方案将生成 ECDSA 密钥对并打印出编码后的私钥。该字节数组使用随机生成的(32 字节长)密钥加密,该密钥用作 GCM 模式操作函数中 AES 的输入;输出是一个由 3 部分连接的字符串:

(Base64) nonce : (Base64) ciphertext : (Base64) gcmTag 

优化版本可以在字节数组的基础上使用直接连接,但由于该函数取自实际项目,我正在以这种方式使用它。

我省略了字符串的保存和加载部分 - 此字符串提供给解密函数,该函数直接将(加载的)私钥作为输出。这个加载键也被打印出来以表明两个键是相等的。

这是示例输出:

Write and read encrypted ECDSA private keys
ecdsaPrivateKey:
3041020100301306072a8648ce3d020106082a8648ce3d030107042730250201010420c07c0af37716b11ac76780287026935190cb3575c1475a02da687b45adfed8b4
encryptedKey: 2adcp+3lEvS8zhc5:5n5UyHThiIQweqXxJfI479qIwv4m7nm/gNeEDeXcd15zVQCTuER2Hn/SPQUM9TbPFHkdh9CWwYI74lbCyV1AJng62g==:HRWiBgME/SsyHQBvvfdTEg==
ecdsaPrivateKeyLoaded:
3041020100301306072a8648ce3d020106082a8648ce3d030107042730250201010420c07c0af37716b11ac76780287026935190cb3575c1475a02da687b45adfed8b4

以下代码没有异常处理,仅用于教育目的:

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;

public class EncryptedEcdsaPrivateKey {
    public static void main(String[] args)
            throws NoSuchAlgorithmException,IllegalBlockSizeException,InvalidKeyException,BadPaddingException,InvalidAlgorithmParameterException,NoSuchPaddingException,InvalidKeySpecException {
        System.out.println("Write and read encrypted ECDSA private keys");
        // step 1 generate an ecdsa key pair
        KeyPair ecdsaKeyPair = generateEcdsaKeyPair(256);
        PrivateKey ecdsaPrivateKey = ecdsaKeyPair.getPrivate();
        System.out.println("ecdsaPrivateKey:\n" + bytesToHex(ecdsaPrivateKey.getEncoded()));
        // step 2 generate a randomly generated AES-256-GCM key
        byte[] randomKey = generateRandomAesKey();
        // step 3 encrypt the encoded key with the randomly generated AES-256-GCM key
        String encryptedKey = aesGcmEncryptToBase64(randomKey,ecdsaPrivateKey.getEncoded());
        System.out.println("encryptedKey: " + encryptedKey);
        // step 4 save the key to file
        // ... omitted
        // step 5 load the key from file
        // ... omitted
        // step 6 decrypt the encrypted data to an ecdsa public key
        PrivateKey ecdsaPrivateKeyLoaded = aesGcmDecryptFromBase64(randomKey,encryptedKey);
        System.out.println("ecdsaPrivateKeyLoaded:\n" + bytesToHex(ecdsaPrivateKeyLoaded.getEncoded()));
    }
    public static KeyPair generateEcdsaKeyPair(int keylengthInt)
            throws NoSuchAlgorithmException {
        KeyPairGenerator keypairGenerator = KeyPairGenerator.getInstance("EC");
        SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
        keypairGenerator.initialize(keylengthInt,random);
        return keypairGenerator.generateKeyPair();
    }

    private static byte[] generateRandomAesKey() {
        SecureRandom secureRandom = new SecureRandom();
        byte[] key = new byte[32];
        secureRandom.nextBytes(key);
        return key;
    }

    private static byte[] generateRandomNonce() {
        SecureRandom secureRandom = new SecureRandom();
        byte[] nonce = new byte[12];
        secureRandom.nextBytes(nonce);
        return nonce;
    }

    private static String aesGcmEncryptToBase64(byte[] key,byte[] data)
            throws NoSuchPaddingException,NoSuchAlgorithmException,IllegalBlockSizeException {
        byte[] nonce = generateRandomNonce();
        SecretKeySpec secretKeySpec = new SecretKeySpec(key,"AES");
        GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(16 * 8,nonce);
        Cipher cipher = Cipher.getInstance("AES/GCM/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE,secretKeySpec,gcmParameterSpec);
        byte[] ciphertextWithTag = cipher.doFinal(data);
        byte[] ciphertext = new byte[(ciphertextWithTag.length-16)];
        byte[] gcmTag = new byte[16];
        System.arraycopy(ciphertextWithTag,ciphertext,(ciphertextWithTag.length - 16));
        System.arraycopy(ciphertextWithTag,(ciphertextWithTag.length-16),gcmTag,16);
        String nonceBase64 = base64Encoding(nonce);
        String ciphertextBase64 = base64Encoding(ciphertext);
        String gcmTagBase64 = base64Encoding(gcmTag);
        return nonceBase64 + ":" + ciphertextBase64 + ":" + gcmTagBase64;
    }

    private static PrivateKey aesGcmDecryptFromBase64(byte[] key,String data)
            throws NoSuchPaddingException,InvalidKeySpecException {
        String[] parts = data.split(":",0);
        byte[] nonce = base64Decoding(parts[0]);
        byte[] ciphertextWithoutTag = base64Decoding(parts[1]);
        byte[] gcmTag = base64Decoding(parts[2]);
        byte[] encryptedData = concatenateByteArrays(ciphertextWithoutTag,gcmTag);
        Cipher cipher = Cipher.getInstance("AES/GCM/PKCS5Padding");
        SecretKeySpec secretKeySpec = new SecretKeySpec(key,nonce);
        cipher.init(Cipher.DECRYPT_MODE,gcmParameterSpec);
        byte[] encodedEcdsaKey = cipher.doFinal(encryptedData);
        KeyFactory keyFactory = KeyFactory.getInstance("EC");
        PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(encodedEcdsaKey);
        return keyFactory.generatePrivate(privateKeySpec);
    }

    private static String base64Encoding(byte[] input) {
        return Base64.getEncoder().encodeToString(input);
    }
    private static byte[] base64Decoding(String input) {
        return Base64.getDecoder().decode(input);
    }

    private static String bytesToHex(byte[] bytes) {
        StringBuffer result = new StringBuffer();
        for (byte b : bytes) result.append(Integer.toString((b & 0xff) + 0x100,16).substring(1));
        return result.toString();
    }

    public static byte[] concatenateByteArrays(byte[] a,byte[] b) {
        return ByteBuffer
                .allocate(a.length + b.length)
                .put(a).put(b)
                .array();
    }
}
,

对于可能对此问题的解决方案感兴趣的任何人,我都能够使事情大部分如我所希望的那样工作。我没有使用随机生成的安全性,而是使用了可配置的基于密码的加密方案。一旦我接受了这种方法来解决我的问题,我就能够很好地解决问题。

首先,这是我用来创建用于加密私钥的基于密码的密钥的代码:

private SecretKey createSecretKey() throws MyCryptoException {
    try {
        String password = getPassword(); // Retrieved via configuration
        KeySpec keySpec = new PBEKeySpec(password.toCharArray());
        SecretKeyFactory factory = SecretKeyFactory.getInstance(this.encryptionAlgorithm.getName());
        return factory.generateSecret(keySpec);
    }
    catch (GeneralSecurityException e) {
        throw new MyCryptoException("Error creating secret key",e);
    }
}

创建我用于加密的密码:

private Cipher createCipher() throws MyCryptoException {
    try {
        return Cipher.getInstance(this.encryptionAlgorithm.getName());
    }
    catch (GeneralSecurityException e) {
        throw new MyCryptoException("Error creating cipher for password-based encryption",e);
    }
}

对于上述方法,this.encryptionAlgorithm.getName() 将返回 PBEWithMD5AndDESPBEWithSHA1AndDESede。这些似乎与 PKCS #5 版本 1.5 基于密码的加密 (PBKDF1) 一致。我最终计划致力于支持此版本的更新(和更安全)版本,但这暂时完成了工作。

接下来,我需要一个基于密码的参数规范:

private AlgorithmParameterSpec createParamSpec() {
    byte[] saltVector = new byte[this.encryptionAlgorithm.getSaltSize()];
    SecureRandom random = new SecureRandom();
    random.nextBytes(saltVector);
    return new PBEParameterSpec(saltVector,this.encryptionHashIterations);
}

在上述方法中,this.encryptionAlgorithm.getSaltSize() 返回 8 或 16,具体取决于配置的算法名称。

然后,我将这些方法组合在一起,将私钥的字节转换为 java.crypto.EncryptedPrivateKeyInfo 实例

public EncryptedPrivateKeyInfo encryptPrivateKey(byte[] keyBytes) throws MyCryptoException {

    // Create cipher and encrypt
    byte[] encryptedBytes;
    AlgorithmParameters parameters;
    try {
        Cipher cipher = createCipher();
        SecretKey encryptionKey = createSecretKey();
        AlgorithmParameterSpec paramSpec = createParamSpec();
        cipher.init(Cipher.ENCRYPT_MODE,encryptionKey,paramSpec);
        encryptedBytes = cipher.doFinal(keyBytes);
        parameters = cipher.getParameters();
    }
    catch (GeneralSecurityException e) {
        throw new MyCryptoException("Error encrypting private key bytes",e);
    }

    // Wrap into format expected for PKCS8-formatted encrypted secret key file
    try {
        return new EncryptedPrivateKeyInfo(parameters,encryptedBytes);
    }
    catch (GeneralSecurityException e) {
        throw new MyCryptoException("Error packaging private key encryption info",e);
    }
}

这个 EncryptedPrivateKeyInfo 实例是写入文件的内容,Base64 编码并用适当的页眉和页脚文本包围。下面展示我如何使用上述方法创建加密密钥文件:

private static final String ENCRYPTED_KEY_HEADER = "-----BEGIN ENCRYPTED PRIVATE KEY-----";
private static final String ENCRYPTED_KEY_FOOTER = "-----END ENCRYPTED PRIVATE KEY-----";
private static final int KEY_FILE_MAX_LINE_LENGTH = 64;

private void writePrivateKey(PrivateKey key,Path path) throws MyCryptoException {
    try {
        byte[] fileBytes = key.getEncoded();
        encryptPrivateKey(key.getEncoded()).getEncoded();
        writeKeyFile(ENCRYPTED_KEY_HEADER,ENCRYPTED_KEY_FOOTER,fileBytes,path);
    }
    catch (IOException e) {
        throw new MyCryptoException("Can't write private key file",e);
    }
}

private void writeKeyFile(String header,String footer,byte[] keyBytes,Path path) throws IOException {
        
    // Append the header
    StringBuilder builder = new StringBuilder()
        .append(header)
        .append(System.lineSeparator());

    // Encode the key and append lines according to the max line size
    String encodedBytes = Base64.getEncoder().encodeToString(keyBytes);
    partitionBySize(encodedBytes,KEY_FILE_MAX_LINE_LENGTH)
        .stream()
        .forEach(s -> {
            builder.append(s);
            builder.append(System.lineSeparator());
        });

    // Append the footer
    builder
        .append(footer)
        .append(System.lineSeparator());
        
    // Write the file
    Files.writeString(path,builder.toString());
}

private List<String> partitionBySize(String source,int size) {
    int sourceLength = source.length();
    boolean isDivisible = (sourceLength % size) == 0;
    int partitionCount = (sourceLength / size) + (isDivisible ? 0 : 1);
    return IntStream.range(0,partitionCount)
        .mapToObj(n -> {
            return ((n + 1) * size >= sourceLength) ?
                source.substring(n * size) : source.substring(n * size,(n + 1) * size);
        })
        .collect(Collectors.toList());
}

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


依赖报错 idea导入项目后依赖报错,解决方案:https://blog.csdn.net/weixin_42420249/article/details/81191861 依赖版本报错:更换其他版本 无法下载依赖可参考:https://blog.csdn.net/weixin_42628809/a
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下 2021-12-03 13:33:33.927 ERROR 7228 [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPL
错误1:gradle项目控制台输出为乱码 # 解决方案:https://blog.csdn.net/weixin_43501566/article/details/112482302 # 在gradle-wrapper.properties 添加以下内容 org.gradle.jvmargs=-Df
错误还原:在查询的过程中,传入的workType为0时,该条件不起作用 &lt;select id=&quot;xxx&quot;&gt; SELECT di.id, di.name, di.work_type, di.updated... &lt;where&gt; &lt;if test=&qu
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct redisServer’没有名为‘server_cpulist’的成员 redisSetCpuAffinity(server.server_cpulist); ^ server.c: 在函数‘hasActiveC
解决方案1 1、改项目中.idea/workspace.xml配置文件,增加dynamic.classpath参数 2、搜索PropertiesComponent,添加如下 &lt;property name=&quot;dynamic.classpath&quot; value=&quot;tru
删除根组件app.vue中的默认代码后报错:Module Error (from ./node_modules/eslint-loader/index.js): 解决方案:关闭ESlint代码检测,在项目根目录创建vue.config.js,在文件中添加 module.exports = { lin
查看spark默认的python版本 [root@master day27]# pyspark /home/software/spark-2.3.4-bin-hadoop2.7/conf/spark-env.sh: line 2: /usr/local/hadoop/bin/hadoop: No s
使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-