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

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等。大家可以自行搜索相关内容。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • HTTPS介绍 超文本传输安全协议(英语:Hypertext Transfer Protocol Secure,缩...
    齐滇大圣阅读 8,898评论 8 96
  • 一. 证书 目前总的来说有三种常用的证书格式:X.509证书、PKCS#12证书和PKCS#7证书。X.509证书...
    飘荡着呢阅读 3,011评论 0 1
  • 随着对于安全度的不断要求,对于数据加解密与破解之间的斗争,加解密的方式也在不断发生着变化,来看看现在流行的一些加解...
    zhouhao_180阅读 2,059评论 1 12
  • 非对称加密算法需要两个密钥:公开密钥(publickey)和私有密钥(privatekey)公开秘钥是公开的,私有...
    Mario_ZJ阅读 648评论 0 0
  • 昨晚再次梦见,重复怪圈。最近身体一直不好,胃疼胃胀,又不知饿。要去检查一下。 爪的脾气渐长,爪爹渐胖,血压也高,还...
    爪妈阅读 120评论 0 0