Realtime Price(实时价格):

Java密码学 非对称加密以及使用secp256k1进行数字签名(ECDSA),也适合Android(上)

作者:夏日里的故事

来源:https://www.jianshu.com/p/676a0eb33d31

1. 概述


我们考虑几个现实中的业务场景:

案例一:

当更新Android手机上的微信APP,系统怎么判断新的安装包就是腾讯公司发布的安装包?系统怎么判断即使是腾讯发布的安装包,但是安装包却没有被修改?这显然是非常重要的事情,如果安装包被修改过,那么用户口令、数据、银行卡等信息都可能会被窃取。

案例二:

现在很多企业内部都是通过邮件、IM来沟通,甚至下发财务、采购等指令,相关人员如何鉴别,邮件、电子合同(文档)就是老板本人发出来的呢?而不是假冒的。

如果存在一种机制,发送者对数据(安装包、文档等)进行一个“签名”或者“盖章”,而接收者根据这个签名或者盖章进行验证,从而判断数据是否正确的发送者发送的,以及数据是否被篡改,那么这些问题就迎刃而解了。并且,这种验证机制是公告开的。

这种机制是存在的,密码学上叫:数字签名。数字签名的实现,通常使用公钥算法(又称非对称加密算法),该算法的特点就是秘钥有一对:公钥和私钥,公钥是公开的,可以广播出去告诉大家,私钥是保密的,只能是自己知道,保密。因此,在实现数字签名,流程是使用私钥对数据进行签名,输出一段特定长度的数字签名(指纹),验证着使用对应的公钥、原始数据、数字签名进行运算,从而校验数据是否被篡改或者发行者身份的合法性

2. 签名算法、非对称加密、ECC与secp256k1


签名算法有比较多的选择,例如:RSA、DSA、ECC(ECDSA)等。前两者因为秘钥长度和性能的关系,现在使用越来越少,例如常见的RSA2048,秘钥长度就达到了2048bit,也就是2KB大小,在一些嵌入式场合消耗比较大,而ECC只需要224bit,因此比特币在保证数据安全性基础的算法选择上选择了ECC。

ECC也就是椭圆曲线密码学,原理上不多说了,现在很多应用场合选择了它,例如区块链,足以看出它的火热程度。

在使用ECC进行数字签名的时候,需要构造一条曲线,也可以选择标准曲线,诸如:prime256v1、secp256r1、nistp256、secp256k1等等。我们需要使用的是secp256k1,也就是比特币选择的加密曲线。

3. 秘钥的产生和载入

公钥算法的秘钥,通常不可能和我们认知的口令对等,例如:secp256k1,秘钥长度就达到了256bit,也就是32字节,记忆在脑海里,显然是不现实的。通常,我们通过程序来生成秘钥,存储到磁盘、安全设备上,然后再通过程序载入使用。

3.1 秘钥生成

在Java中,生成ECC秘钥很简单,只需要使用:KeyPairGenerator

 

KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
      // curveName这里取值:secp256k1
        ECGenParameterSpec ecGenParameterSpec = new ECGenParameterSpec(curveName);
        keyPairGenerator.initialize(ecGenParameterSpec, new SecureRandom());
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        // 获取公钥
        keyPari.getPublic(); 
        // 获取私钥
        keyPair.getPrivate();

KeyPairGenerator可以设置一些算法参数,因为我们需要指定标准曲线,因此使用:ECGenParameterSpec(“secp256k1”)来指定曲线。

这里显然有个问题存在,在业务的生命周期当中,秘钥始终是同一个,而上述代码,每运行一次,就重新产生一个,显然是不现实的,在实际业务中的做法就是:第一次产生一个(或者使用诸如OpenSSL一类的工具,生成一个),然后存储到磁盘上或者特殊的存储介质上,然后在程序中加载。

3.2 秘钥的存储


Java中要序列化秘钥,也是相当简单的,只要调用:getEncoded(),它返回特定格式的byte[]数据,该格式属于标准格式,可以在大部分程序/软件中通用。

PrivateKey.getEncoded() 返回 PKCS #8 格式并且以DER编码输出;对于 PublicEncode.getEncoded()返回 X.509 格式并且以DER编码输出的byte[],这个时候,可以直接存储到磁盘上了。

测试代码:

 

        KeyPair keyPair = KeyUtil.createKeyPairGenerator("secp256k1");
        
        PublicKey publicKey = keyPair.getPublic();
        PrivateKey privateKey = keyPair.getPrivate();
        
        KeyUtil.savePublicKey(publicKey, "publickey.der");
        KeyUtil.savePrivateKey(privateKey, "privatekey.der");

为了验证一下,我们使用:OpenSSL命令来验证一下:

打印公钥:

 

$ openssl pkey -inform DER -pubin -in publickey.der -text

-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEDNeUU82FtdEOUjDjiX9PqRTi2HD2Dq7x
TrnTVY3Q52j+FtSJtBLp6RmEJ0dCmxd3y1igSMCx9nOrAO0vqEdBTA==
-----END PUBLIC KEY-----
Public-Key: (256 bit)
pub:
    04:0c:d7:94:53:cd:85:b5:d1:0e:52:30:e3:89:7f:
    4f:a9:14:e2:d8:70:f6:0e:ae:f1:4e:b9:d3:55:8d:
    d0:e7:68:fe:16:d4:89:b4:12:e9:e9:19:84:27:47:
    42:9b:17:77:cb:58:a0:48:c0:b1:f6:73:ab:00:ed:
    2f:a8:47:41:4c
ASN1 OID: secp256k1

打印私钥:

 

$ openssl pkey -inform DER -in privatekey.der -text

-----BEGIN PRIVATE KEY-----
MD4CAQAwEAYHKoZIzj0CAQYFK4EEAAoEJzAlAgEBBCA9ONwt9uitCK04sqbs3MvH
3wj8B4ZIzhKDTzY2NqfDzQ==
-----END PRIVATE KEY-----
Private-Key: (256 bit)
priv:
    3d:38:dc:2d:f6:e8:ad:08:ad:38:b2:a6:ec:dc:cb:
    c7:df:08:fc:07:86:48:ce:12:83:4f:36:36:36:a7:
    c3:cd
pub:
    04:0c:d7:94:53:cd:85:b5:d1:0e:52:30:e3:89:7f:
    4f:a9:14:e2:d8:70:f6:0e:ae:f1:4e:b9:d3:55:8d:
    d0:e7:68:fe:16:d4:89:b4:12:e9:e9:19:84:27:47:
    42:9b:17:77:cb:58:a0:48:c0:b1:f6:73:ab:00:ed:
    2f:a8:47:41:4c
ASN1 OID: secp256k1

这说明,秘钥可以被别的工具识别

3.3 PEM编码的秘钥


getEncoded()方法输出的是DER编码的二进制文件,在很多时候,我们可能为了便于交互,需要以文本编码的方式输出,这个时候PEM编码可以满足。PEM编码结构大致为BEGIN-END块结构,中间内容为Base64转换后的的DER编码内容。

Java标准库不支持PEM格式的读写,但可以使用 bouncycastle 来实现。不过,针对私钥和公钥,我们可以简单的写代码实现,这样避免引入过多的依赖。简单实现的话,只需要将:getEncoded() 输出进行Base64编码(64个字节添加换行符),然后首尾添加响应的分割字符串。下面是实现代码:

 

public static void savePublicKeyAsPEM(PublicKey publicKey, String name) throws Exception {
        String content = Base64Util.encode(publicKey.getEncoded());
        File file = new File(name);
        if ( file.isFile() && file.exists() )
            throw new IOException("file already exists");
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw")) {
            randomAccessFile.write("-----BEGIN PUBLIC KEY-----\n".getBytes());
            int i = 0;
            for (; i<(content.length() - (content.length() % 64)); i+=64) {
                randomAccessFile.write(content.substring(i, i + 64).getBytes());
                randomAccessFile.write('\n');
            }

            randomAccessFile.write(content.substring(i, content.length()).getBytes());
            randomAccessFile.write('\n');

            randomAccessFile.write("-----END PUBLIC KEY-----".getBytes());
        }
    }

    public static void savePrivateKeyAsPEM(PrivateKey privateKey, String name) throws Exception {
        String content = Base64Util.encode(privateKey.getEncoded());
        File file = new File(name);
        if ( file.isFile() && file.exists() )
            throw new IOException("file already exists");
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw")) {
            randomAccessFile.write("-----BEGIN PRIVATE KEY-----\n".getBytes());
            int i = 0;
            for (; i<(content.length() - (content.length() % 64)); i+=64) {
                randomAccessFile.write(content.substring(i, i + 64).getBytes());
                randomAccessFile.write('\n');
            }

            randomAccessFile.write(content.substring(i, content.length()).getBytes());
            randomAccessFile.write('\n');

            randomAccessFile.write("-----END PRIVATE KEY-----".getBytes());
        }
    }

为了验证生成的PEM的合法性,我们依然使用OpenSSL命令来验证:

 

# 打印公钥
$ openssl ec -in publickey.pem -pubin -text -noout

Private-Key: (256 bit)
pub:
    04:47:e4:24:c3:97:fb:77:5d:5d:43:f0:04:c2:fe:
    13:bf:f4:97:0e:b9:79:43:c9:bf:bb:72:1c:d1:ee:
    bd:9a:27:42:aa:a0:d8:c0:00:9c:6f:d9:38:da:67:
    0e:75:9a:8e:e4:e7:c4:67:86:f3:c0:19:64:22:47:
    e9:53:f7:09:f4
ASN1 OID: secp256k1
read EC key

# 打印私钥
$ openssl ec -in privatekey.pem -text -noout
Private-Key: (256 bit)
priv:
    00:83:00:e5:1c:7b:a0:34:ee:67:3c:3e:07:a1:64:
    de:cc:80:d3:59:4e:a1:14:bb:86:81:f3:2e:8a:b1:
    51:de:d2
pub:
    04:47:e4:24:c3:97:fb:77:5d:5d:43:f0:04:c2:fe:
    13:bf:f4:97:0e:b9:79:43:c9:bf:bb:72:1c:d1:ee:
    bd:9a:27:42:aa:a0:d8:c0:00:9c:6f:d9:38:da:67:
    0e:75:9a:8e:e4:e7:c4:67:86:f3:c0:19:64:22:47:
    e9:53:f7:09:f4
ASN1 OID: secp256k1
read EC key

3.3 秘钥的加载


加载公钥和私钥,需要先从磁盘中读取成byte[],然后使用:X509EncodedKeySpecPKCS8EncodedKeySpec 转换成公钥和私钥。

实例代码:

 

// 读取公钥, encodedKey为从文件中读取到的byte[]数组
    public static PublicKey loadPublicKey(byte[] encodedKey, String algorithm) 
            throws NoSuchAlgorithmException, InvalidKeySpecException {
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(encodedKey);
        KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
        return keyFactory.generatePublic(keySpec);
    }

// 读取私钥
    public static PrivateKey loadPrivateKey(byte[] encodedKey,  String algorithm)
            throws NoSuchAlgorithmException, InvalidKeySpecException{
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encodedKey);
        KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
        return keyFactory.generatePrivate(keySpec);
    }

例如加载私钥:

 

PrivateKey privateKey1 = KeyUtil.loadPrivateKey(IOUtils.readBytes(
                new FileInputStream("privatekey.der")), "EC");

// readBytes代码
    public static byte[] readBytes(final InputStream inputStream) throws IOException {
        final int BUFFER_SIZE = 1024;
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        int readCount;
        byte[] data = new byte[BUFFER_SIZE];
        while ((readCount = inputStream.read(data, 0, data.length)) != -1) {
            buffer.write(data, 0, readCount);
        }
        
        buffer.flush();
        return buffer.toByteArray();
    }

上述两个方法,只能处理DER编码的秘钥,如果是PEM,我们移除掉”BEGIN-END”以及换行符,然后进行Base64解码后进行处理

 

    public static PrivateKey loadECPrivateKey(String content,  String algorithm) throws Exception {
        String privateKeyPEM = content.replace("-----BEGIN PRIVATE KEY-----\n", "")
                .replace("-----END PRIVATE KEY-----", "").replace("\n", "");
        byte[] asBytes = Base64Util.decode(privateKeyPEM);
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(asBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
        return keyFactory.generatePrivate(spec);
    }

    public static PublicKey loadECPublicKey(String content,  String algorithm) throws Exception {
        String strPublicKey = content.replace("-----BEGIN PUBLIC KEY-----\n", "")
                .replace("-----END PUBLIC KEY-----", "").replace("\n", "");
        byte[] asBytes = Base64Util.decode(strPublicKey);
        X509EncodedKeySpec spec = new X509EncodedKeySpec(asBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
        return (ECPublicKey) keyFactory.generatePublic(spec);
    }

4. 小结

在大部分系统业务系统里面,频繁生成、加载秘钥的业务是不多的,但是如果做一个开放性API体系,可能用的就比较多了(例如微信、支付宝一些业务接入就需要提供公钥),而且秘钥来源软件比较多,这里可能需要深入了解:PKCS系列标准、X.509等。大家可以自行搜索相关内容。

作者:夏日里的故事
链接:https://www.jianshu.com/p/676a0eb33d31
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

相关文章:

比特币布道者

比特币的坚定信仰者!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注