rsa公開鍵を用いてiOSクライアントで暗号化しGoのサーバサイドで復号する


はじめに

rsa暗号化についてはいくつかオプションがあります。

今回は選択暗号文攻撃に強いRSA-OAEPに絞った調査を行っています。


秘密鍵と公開鍵の準備

以下のコマンドで作成します

openssl genrsa 2048 > private_key.pem

openssl rsa -pubout < private_key.pem > public_key.key


Goだけで暗号化、復号化を行う

OAEPについてはいかに情報があります。

https://ja.wikipedia.org/wiki/Optimal_Asymmetric_Encryption_Padding

Goは割と素直な感じで

oaep

上記の概念図おけるG,Hのハッシュアルゴリズムの指定とrに当たるラベルの指定ができます。

以下インターフェースの抜粋箇所です

 //暗号化

ciphertext, err := rsa.EncryptOAEP(sha256.New()/*ハッシュアルゴリズム*/, rng/*ランダム値生成機*/, &privateKey.PublicKey, secretMessage, label/*ラベル*/)
// 復号
plaintext, err := rsa.DecryptOAEP(sha256.New(), rng, privateKey, ciphertext, label)

goだけで閉じているならとっても楽なんですが、、、、



package rsa_test

import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"testing"
)

func TestEncDec(t *testing.T) {
privateKey, err := readRsaPrivateKey("private_key.pem")
if err != nil {
t.Errorf("sorry. can't read privatekey err=%s", err.Error())
}

expectMessage := "hello rsa"
secretMessage := []byte(expectMessage)
label := []byte("label")

t.Logf("secretMessage = %s", secretMessage)

// crypto/rand.Reader is a good source of entropy for randomizing the
// encryption function.
rng := rand.Reader

// 暗号化
ciphertext, err := rsa.EncryptOAEP(sha256.New(), rng, &privateKey.PublicKey, secretMessage, label)
if err != nil {
t.Errorf("Error from encryption: %s\n", err)
return
}

t.Logf("ciphertext = %s", ciphertext)
// 復号
plaintext, err := rsa.DecryptOAEP(sha256.New(), rng, privateKey, ciphertext, label)
if err != nil {
t.Errorf("Error from decryption: %s\n", err)
return
}

t.Logf("Plaintext: %s\n", string(plaintext))

if expectMessage != string(plaintext) {
t.Fatalf("expect %s, but %s", expectMessage, string(plaintext))
}

}

func readRsaPrivateKey(pemFile string) (*rsa.PrivateKey, error) {
bytes, err := ioutil.ReadFile(pemFile)
if err != nil {
return nil, err
}

block, _ := pem.Decode(bytes)
if block == nil {
return nil, errors.New("invalid private key data")
}

var key *rsa.PrivateKey
if block.Type == "RSA PRIVATE KEY" {
key, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
} else if block.Type == "PRIVATE KEY" {
keyInterface, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
var ok bool
key, ok = keyInterface.(*rsa.PrivateKey)
if !ok {
return nil, errors.New("not RSA private key")
}
} else {
return nil, fmt.Errorf("invalid private key type : %s", block.Type)
}

key.Precompute()

if err := key.Validate(); err != nil {
return nil, err
}

return key, nil
}

参考:

http://increment.hatenablog.com/entry/2017/08/25/223915

実行結果はこんな感じです

$ go test -v -timeout 30s github.com/m0a-mystudy/rsa/go -run ^(TestEncDec)$

=== RUN TestEncDec
--- PASS: TestEncDec (0.00s)
enc_dec_test.go:25: secretMessage = hello rsa
enc_dec_test.go:38: ciphertext = ����*����η<省略>
enc_dec_test.go:46: Plaintext: hello rsa
PASS
ok github.com/m0a-mystudy/rsa/go (cached)

なんの問題もなく暗号化と復号が成功しています。まぁ当たり前です。

では暗号化の部分をiOSに担当していただきましょう。


iOSで暗号化しGoで復号する

今回の主題です。微妙にインターフェースが異なったり、用いている変数名に統一感がないため、

動かすまでに苦労しています。

iOSで現在rsa暗号化をサポートしているライブラリはgithubで探しますがめぼしいのは2つほどです。

参考:https://github.com/topics/rsa

上記2つのライブラリについて実際に暗号化(iOS)と復号(Go)を行ってみます。


SwiftyRSAの場合

インターフェースを確認すると

        let clear = try! ClearMessage(string: <暗号化対象文字列>, using: .utf8)

let encrypted = try! clear.encrypted(with: <publicKey>, padding: <パディングアルゴリズム>)

歴史的経緯なのかhashアルゴリズムとラベルの指定をサポートしておらず、

sha1,ラベルは空で固定のようです。

どなたか経緯を教えていただければ幸いです。


iOSで暗号化


import XCTest
import SwiftyRSA

@testable import rsa_test

class rsa_testTests: XCTestCase {
var publicKey: Data = Data()
var publicKeyPem: String = ""

override func setUp() {
let publicKeyPath = Bundle.main.path(forResource: "public_key", ofType: "pem") ?? ""
self.publicKeyPem = try! String(contentsOfFile: publicKeyPath)
}

func testEncOAEP_SwiftyRSA() {
let publicKey = try! PublicKey.publicKeys(pemEncoded: self.publicKeyPem)[0]
let expectString = "hello ios rsa"
let clear = try! ClearMessage(string: expectString, using: .utf8)
let encrypted = try! clear.encrypted(with: publicKey, padding: .OAEP)
print("SwiftyRSA chiperText[\(encrypted.base64String)]")
}

}

出力例はこちら

SwiftyRSA chiperText[eCezV7gNUMHWNmS2ayePXIe64b7Rk86Is98/uZgPv1g3JLrQieUArhM1gAaJyZaK7Yf/BOqe8/CQ7O2ybKIZDu3HCbwEPhYjoKpNeXVNaIRdiLJa3onmFM5mQJdU+Zr7cH6GOvmx29ZKR4mC6p8BrRqZnh6s3D/5PcGrKqrgh8X9ry9F1ZYptDf36ZO4YdBNKTYy9mVoYqGFlmmiZWAQvvCufnte+PCr5Gxw6DsADYt1ByiDeron+hZMzK9PPiZup8fquAXdXup+PXqlJIvXFyrCneocrsBY1YA8xTfJlxF/ZHUPNZaSb0QiM6PJj3xsU3vg5ZUT/OcgTYhI7lW5SQ==]


Goで復号

package rsa_test

import (
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"testing"
)

func TestDecforSwiftyRSA(t *testing.T) {
base64Text := "lqEicpwE0xPg9iXdn2xSQLeoIEkwvKdxGBMrjwHxW2S5x7IuhrWCnWnFZ1w0nd21nCuZfveW29nCzekvJEBji8W+HcbwQUapIcRoENp6+IkcjISnOR9hR5ZOJBUNP7X0eLniFHuqPXuySWuzGXJIfP2P8iBwFEC0AvUTGUfpXdYEoRP5uEExHBdxw/WywtjocGkgz+sbmBzgCdN+BmAas8h/RdsYI2D83VyvG492Hp45SR+vgoyv4TbqcWZqdPC6T4ZFurQvZKlMKT5Xfhe4WTQUVL1fKvFWGkxXhIesKmxZpvIfKqLF7ZuGs13RJaIDvF6i71fvmF7rN2fvE/iuoA=="
expectMessage := "hello ios rsa"

privateKey, err := readRsaPrivateKey("private_key.pem")
if err != nil {
t.Errorf("sorry. can't read privatekey err=%s", err.Error())
}
rng := rand.Reader

ciphertext, _ := base64.StdEncoding.DecodeString(base64Text)

t.Logf("ciphertext = %s", ciphertext)
// 復号
plaintext, err := rsa.DecryptOAEP(sha1.New(), rng, privateKey, ciphertext, nil)
if err != nil {
t.Errorf("Error from decryption: %s\n", err)
return
}

t.Logf("Plaintext: %s\n", string(plaintext))

if expectMessage != string(plaintext) {
t.Fatalf("expect %s, but %s", expectMessage, string(plaintext))
}

}

func readRsaPrivateKey(pemFile string) (*rsa.PrivateKey, error) {
bytes, err := ioutil.ReadFile(pemFile)
if err != nil {
return nil, err
}

block, _ := pem.Decode(bytes)
if block == nil {
return nil, errors.New("invalid private key data")
}

var key *rsa.PrivateKey
if block.Type == "RSA PRIVATE KEY" {
key, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
} else if block.Type == "PRIVATE KEY" {
keyInterface, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
var ok bool
key, ok = keyInterface.(*rsa.PrivateKey)
if !ok {
return nil, errors.New("not RSA private key")
}
} else {
return nil, fmt.Errorf("invalid private key type : %s", block.Type)
}

key.Precompute()

if err := key.Validate(); err != nil {
return nil, err
}

return key, nil
}

出力結果

$ go test -v  github.com/m0a-mystudy/rsa/go -run ^(TestDecforSwiftyRSA)$

=== RUN TestDecforSwiftyRSA
--- PASS: TestDecforSwiftyRSA (0.00s)
enc_dec_test.go:37: Plaintext: hello ios rsa
PASS
ok github.com/m0a-mystudy/rsa/go 0.015s

復号成功です。


SwCryptの場合


iOSで暗号化

tag == ラベルですね。統一してほしい、、


import XCTest
import SwCrypt

@testable import rsa_test

class rsa_testTests: XCTestCase {
var publicKey: Data = Data()
var publicKeyPem: String = ""

override func setUp() {
let publicKeyPath = Bundle.main.path(forResource: "public_key", ofType: "pem") ?? ""
self.publicKeyPem = try! String(contentsOfFile: publicKeyPath)
self.publicKey = try! SwKeyConvert.PublicKey.pemToPKCS1DER(self.publicKeyPem)
}

func testEncOAEP_SwCrypt() {
let expectString = "hello ios rsa"
guard let data = expectString.data(using: .utf8) else {
return
}

guard let tag = "label".data(using: .utf8) else {
return
}

do {
let chiperText = try CC.RSA.encrypt(data, derKey: self.publicKey, tag: tag, padding: .oaep, digest: .sha256)
print("SwCrypt chiperText[\(chiperText.base64EncodedString())]")
} catch {
XCTAssertNotNil(error)
}
}

}

上記の出力結果抜粋

SwCrypt chiperText[N9QE8lG5A9LRRprfwkzAxoldJkNSJqEQ/gLAcNjMFbLgcGu2yffY3x91/DOCApsxtAU3I7GTCnk0TOTV5Y32zVqOE7S+GksCFFa7iDLsqYvQbKJZXcb8bTe4p93SPU+RBkH0r/H8NBTUDSvNtARcXntqWNwr0FNAW2HH/Ht+ZmL1pWMa0MmdXLc/+4S/KFjlip/b5neMw6EoQkfNNDz8i7+IHiIpz+vJ2ZFvpf8RGXLgUTMGdUrQfv+XyLZZAnYny5a8HzuMbEcSunez0G7T25NGj6ubJJ2zalLACV1GysolOAJFn6YD6jSukDGQmHKlL1JOkKhqUm9EZT6Mw7wBkQ==]


Goで復号

生成された文字列をGoで復号してみます。



func TestDecforSwCrypt(t *testing.T) {
base64Text := "N9QE8lG5A9LRRprfwkzAxoldJkNSJqEQ/gLAcNjMFbLgcGu2yffY3x91/DOCApsxtAU3I7GTCnk0TOTV5Y32zVqOE7S+GksCFFa7iDLsqYvQbKJZXcb8bTe4p93SPU+RBkH0r/H8NBTUDSvNtARcXntqWNwr0FNAW2HH/Ht+ZmL1pWMa0MmdXLc/+4S/KFjlip/b5neMw6EoQkfNNDz8i7+IHiIpz+vJ2ZFvpf8RGXLgUTMGdUrQfv+XyLZZAnYny5a8HzuMbEcSunez0G7T25NGj6ubJJ2zalLACV1GysolOAJFn6YD6jSukDGQmHKlL1JOkKhqUm9EZT6Mw7wBkQ=="
expectMessage := "hello ios rsa"

privateKey, err := readRsaPrivateKey("private_key.pem")
if err != nil {
t.Errorf("sorry. can't read privatekey err=%s", err.Error())
}
rng := rand.Reader
label := []byte("label")

ciphertext, _ := base64.StdEncoding.DecodeString(base64Text)

t.Logf("ciphertext = %s", ciphertext)
// 復号
plaintext, err := rsa.DecryptOAEP(sha256.New(), rng, privateKey, ciphertext, label)
if err != nil {
t.Errorf("Error from decryption: %s\n", err)
return
}

t.Logf("Plaintext: %s\n", string(plaintext))

if expectMessage != string(plaintext) {
t.Fatalf("expect %s, but %s", expectMessage, string(plaintext))
}

}

出力結果

$ go test -v  github.com/m0a-mystudy/rsa/go -run ^(TestDecforiOS)$

=== RUN TestDecforiOS
--- PASS: TestDecforiOS (0.00s)
enc_dec_test.go:29: ciphertext = Dٽs隁������<省略>
enc_dec_test.go:37: Plaintext: hello ios rsa
PASS
ok github.com/m0a-mystudy/rsa/go 0.015s

無事復号


最後に

テストとして書いたコードはこちらにおいておきます

https://github.com/m0a-mystudy/rsa