リモート開発メインのソフトウェア開発企業のエンジニアブログです

OpenSSLを使った安全なファイルの転送を検証してみる

こんにちは!今年もあと残すところ11ヶ月を切ってしまいました。時間が経つのは早いですね・・・。
さて、今回はOpenSSLを使って表題の件を検証をしてみたいと思います。

先日、とあるファイル共有サービスのログイン情報を含む会員情報が流出すると言うインシデントがあったのは記憶に新しいかと思います。
これを書いている2月13日時点では、顧客のファイル自体の流出については報告がありませんが、利用者としては最悪の事態を想定する他ありません。

その上で、利用者として、今後この様な事故の被害を最小限に抑えるにはどうしたら良いでしょうか?
色々あるかと思いますが、1つは暗号化です。
大抵の場合インターネット上でファイルを送受信する際には必ず使われている暗号化ですが、今回のケースのように、それを解く為のクレデンシャルが流出していた場合意味がありません。[1]

今回の事故以外でも、単に権限をミスったりしてファイルが流出していたなんて事もあります。これはGoogle Driveを使おうがAmazon S3を使おうが一緒です。[2]

ファイルを転送する前に自前で更に暗号化をし、更に受信者だけが確実に復号できる仕組みを用意できれば、少しは安心できそうな気がします。

と言う訳で、長くなりましたが以上が本件を書こうと思った経緯です。

また、今回検証には全てmacOS Mojaveを使っており、OpenSSLも最初から入っている物を使ってます。

対象読者

共通鍵暗号、公開鍵暗号等の暗号化技術にあまり詳しくない方。もしくは、OpenSSLを使って実際に暗号化を行った事が無い方を対象としています。

免責

一応、今回プログラム等は試験的に実装した物なのでガチでセンシティブなファイルの転送には使わないで下さい。 [3]

共通鍵暗号と公開鍵暗号

インターネット上でよく使う暗号化の方法としては主にこの2種類があると思います。それぞれの特徴を、OpenSSLでの実行例を添えて紹介します。

共通鍵暗号

暗号化時と復号時 [4] に使う鍵が一緒のタイプです。パスワード付きZIPやWi-Fiでもおなじみの、一般的によく使われる暗号化方式かと思います。

ちなみにここで言う鍵というのは復号の方法を表す情報と言う事になります。したがって、例えば以下のような物も共通鍵暗号と言えます。

暗号:ハテネオヌヅモカナキワソブソア
平文:ヒトノカネデヤキニクヲタベタイ(人の金で焼き肉を食べたい)
鍵:1文字ずつ50音の1つ前のカナに変換

特徴としては、(当たり前ですが)暗号化したデータと共に、暗号化に使った鍵もどうにかして転送する必要があります。[5]

以下は、OpenSSLでの使用例です。
まずは暗号化から。以下は “Secret Message!!” と言う平文のメッセージを、AES-256-CBC (共通鍵暗号の一種) を使ってパスワード (鍵) “hogehoge” で暗号化し、標準出力に結果を出力します:

$ echo 'Secret Message!!' | openssl enc -aes-256-cbc -salt -k hogehoge -base64

U2FsdGVkX1+mdCMdi+jRV7bwJaSm+LxUjnTax98O9ZWSa/s45UHmzagIF+Nt6nqp

-salt をつけると実行する度に異なる結果が得られ安全です(デフォルトで有効)。また、出力結果を見やすくする為に今回はbase64化して出力しています。

続いてこのメッセージを復号してみましょう。
逆の手順で今度はbase64デコードした後、AES-256-CBCで同じ鍵で復号を行います:

$ echo 'U2FsdGVkX1+mdCMdi+jRV7bwJaSm+LxUjnTax98O9ZWSa/s45UHmzagIF+Nt6nqp' | openssl enc -d -aes-256-cbc -k hogehoge -base64

Secret Message!!

無事メッセージを戻す事ができました!

公開鍵暗号

共通鍵暗号とは対象的に、暗号化と復号で異なる2つの鍵(鍵ペア)を使います。受信側が予め鍵ペアを作り、片方の鍵を “公開鍵” として送信側に渡します。送信者はそれを使い暗号化を行い、受信側に転送します。受信者はデータを受け取った後、所有するもう一方の鍵 “秘密鍵” を使って復号を行います。
公開鍵暗号では、公開鍵で暗号化したデータは、秘密鍵でしか復号できない為、公開鍵の転送は安全な手段を用いる必要はありません。この性質から、安全にデータの転送が可能となります。[6]

因みに有名な公開鍵暗号アルゴリズムのRSAでは、巨大な2つの素数を掛け合わした数値に対する、素因数分解の困難性を安全性の根拠としています。

それでは、実際にOpenSSLを使ってそのRSAを使って暗号化&復号を試してみましょう。

まずは鍵ペアを生成します。以下は、2048bitのRSA秘密鍵 (公開不可の方) を作り、private.pemに保存します:

$ openssl genrsa -out private.pem 2048

Generating RSA private key, 2048 bit long modulus
..............................................................................................+++
........+++
e is 65537 (0x10001)

続いて、この秘密鍵から公開鍵を取り出します。秘密鍵には公開鍵の情報が入っている為、いつでも公開鍵の生成が可能です [7]:

$ openssl rsa -in private.pem -outform PEM -pubout -out public.pem

writing RSA key

それではこの鍵ペアを使って実際に暗号化を行います。以下は、“Secret Message!!” と言うメッセージをRSAの公開鍵で暗号化し、base64に変換し結果を標準出力に出力します:

$ echo 'Secret Message!!' | openssl rsautl -encrypt -inkey public.pem -pubin | base64

qUkeauQ7dCdfZENyBICyhG5xSVwGDFfltoFU1jsJ21nGVPaOoKp6xk7b6mVzkf+zx6mypH7ARcWRO4Ki/DcSsRXZgUqv0jeEdLJTz2XPjf72hwL+sZCcQBkrHb/uY9ztYzOkeyIBZzZbl7QrQdyIPmJL248UR6sEQwtQz+U4c25yuQtmyno5a1+Ae2xBPJJZZy1xZlmOuW5KarGeCliRU5ya6GwEdWoRSuO4zLgsFYmPP/l0ItGBL/uo+dTFuNpbcdZiRZ3V52rnR2L8txf2EKv5gMkbB+kt46wtQp5CsnON8/VhuHBK8q1HwiAD29DxIMZSAqtCnHVZbpdmkbwc5A==

-pubin は公開鍵を使う事を明示します。省略した場合、OpenSSLは秘密鍵が入力されている事を期待して暗号化を行う (実際には、先述の保持されている公開鍵が使われる) ので、エラーとなってしまうので注意が必要です。

念の為、本当に公開鍵で復号できないか試してみましょう。以下は、先程のbase64メッセージをデコードして、公開鍵を使って復号を試みます:

$ echo 'qUkeauQ7dCdfZENyBICyhG5xSVwGDFfltoFU1jsJ21nGVPaOoKp6xk7b6mVzkf+zx6mypH7ARcWRO4Ki/DcSsRXZgUqv0jeEdLJTz2XPjf72hwL+sZCcQBkrHb/uY9ztYzOkeyIBZzZbl7QrQdyIPmJL248UR6sEQwtQz+U4c25yuQtmyno5a1+Ae2xBPJJZZy1xZlmOuW5KarGeCliRU5ya6GwEdWoRSuO4zLgsFYmPP/l0ItGBL/uo+dTFuNpbcdZiRZ3V52rnR2L8txf2EKv5gMkbB+kt46wtQp5CsnON8/VhuHBK8q1HwiAD29DxIMZSAqtCnHVZbpdmkbwc5A==' | base64 --decode | \
  openssl rsautl -decrypt -inkey public.pem -pubin

A private key is needed for this operation

無事(?)怒られました。では今度は指示に従って秘密鍵を使いましょう:

$ echo 'qUkeauQ7dCdfZENyBICyhG5xSVwGDFfltoFU1jsJ21nGVPaOoKp6xk7b6mVzkf+zx6mypH7ARcWRO4Ki/DcSsRXZgUqv0jeEdLJTz2XPjf72hwL+sZCcQBkrHb/uY9ztYzOkeyIBZzZbl7QrQdyIPmJL248UR6sEQwtQz+U4c25yuQtmyno5a1+Ae2xBPJJZZy1xZlmOuW5KarGeCliRU5ya6GwEdWoRSuO4zLgsFYmPP/l0ItGBL/uo+dTFuNpbcdZiRZ3V52rnR2L8txf2EKv5gMkbB+kt46wtQp5CsnON8/VhuHBK8q1HwiAD29DxIMZSAqtCnHVZbpdmkbwc5A==' | base64 --decode | \
  openssl rsautl -decrypt -inkey private.pem

Secret Message!!

無事に戻す事ができましたね♪

2つの手法を組み合わせる

ここで勘の良い方なら、「公開鍵暗号最強やん!共通鍵暗号の存在意義って・・・」と思うかもしれません。が、公開鍵暗号だけを使うのは難しい理由があります。それは、RSAを使った公開鍵暗号では、もとのメッセージは鍵長以下である必要があると言う制約があるからです。
例えば先程の例のように2048bit (256バイト) の鍵長であれば、256バイトまでのデータしか暗号化できません。更に言えば、デフォルトにもなっている PKCS #1 v1.5 padding と言うパディングが入るため、 (RFC-2313によると) 更に11バイト低い、245バイトまでが上限となります。

この様な制限から、実際のデータの通信にはRSAは不向きと言えます。そこで、ファイル自体の暗号化にはサイズの制約の少ない共通鍵暗号を使い、その鍵自体を公開鍵暗号を使って暗号化する事で、安全に暗号データと、その復号の為のキーを受信者に送付する事が可能となります。[8]

実際にやってみた

長くなりましたがいよいよ本題です。先程書いた通り、2つの暗号化方式を組み合わせますので、具体的なフローしては次のような形になります:

  1. 受信者: 公開鍵暗号 (RSA) の鍵ペアを生成
  2. 受信者: 1の「公開鍵」を送信者に送付
  3. 送信者: 256bit (32バイト) 長のランダム値を生成。これを共通鍵暗号 (AES) の鍵とする
  4. 送信者: 3で作った「共通鍵」で、送信したい機密データを暗号化する
  5. 送信者: 3で作った「共通鍵」を、2で受け取った「公開鍵」で暗号化する
  6. 送信者: 4と5を受信者に送付
  7. 受信者: 6で受け取った「共通鍵」を、1で生成した「秘密鍵」で復号する
  8. 受信者: 6で受け取った暗号データを、7で復号した「共通鍵」で復号する

自分が送信者の場合、事前に受信相手にRSA秘密鍵を生成してもらう必要がありますが、相手がGitHubのユーザーで、かつSSHの認証用にRSAの鍵を登録している場合、手間が省けます。
何故ならユーザーが登録している公開鍵は、誰でも参照が可能だからです。因みに僕の鍵の場合、以下となります:

$ curl -s https://github.com/issei-m.keys

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC8EfkPUlQE7/z8chnxgmmRX1efaPLO3AKLpXlasPSU/T4eQjahIwz2AC8jowGIXfiUZG2QLgSbMzRkzJC59FAXGzF+ty3VIMMxrA37h2NGuAstr7snzOODx3jph+tJjPZoMx07PGLN4JYysv8eZ/C7ULihdRLBsImQNOStEFNVVm7zNKF0LbvaVuAsbgtpd8L0OaeTWMWEok5K/QRNunnRb6p/q45EyHpN0gWFCByp3pXPu3/QVJTprr/Y1T93uhAZwL1EmB8vHbwAwbb9L/+TOUeY2IXxoykuhhZpXz+0HD9p6iNcv5ABDJNhKMEjjfNq2gKWhc7G2bQwiRT9ELjOvuChBq2ITqw/KaDklORqHK8fzsvPZIe6Rmf9zwNUJPAng360m81kLsWrTiaGA2iR8jKAk0sElPIvNpopFx/VX2rk/RMBptK0zHbZ3X1qFw5ReQ7NFbg3nHSO3po9C463PphDt8HBNoBunzqH40q+XXe7cDvvX4u8ShapJSFdcB1VHUiRm/HRkJaun4Iq2R00oZURcV49NEnB9z2l+0UtqKZHj78l7UBDMe/pa4vrrmbxiUZoB1msrnhrnXW+lytZAHEvTow4RTM5U/14mxbwhs6s4F8ljci4P/0YnH7WBvd9xR/f6M21LM6WWXGvCv+92xYd9DSBjb8z8G35J1Tm1w==

尚、この形式のままだとOpenSSLで使えませんので、使える形式に変換しておきます。これに関してはOpenSSHと言う、名前はそっくりですが別のライブラリの機能を使う必要があります。

以下は、GitHubから取得した issei-m の鍵をOpenSSLで使えるPKCS8形式に変換し、./issei-m.pem に保存します:

$ curl -s https://github.com/issei-m.keys > /tmp/issei-m.key
$ ssh-keygen -f /tmp/issei-m.key -e -m PKCS8 | tee -a issei-m.pem

-----BEGIN RSA PUBLIC KEY-----
MIICCgKCAgEAvBH5D1JUBO/8/HIZ8YJpkV9Xn2jyztwCi6V5WrD0lP0+HkI2oSMM
9gAvI6MBiF34lGRtkC4EmzM0ZMyQufRQFxsxfrct1SDDMawN+4djRrgLLa+7J8zj
g8d46YfrSYz2aDMdOzxizeCWMrL/Hmfwu1C4oXUSwbCJkDTkrRBTVVZu8zShdC27
2lbgLG4LaXfC9Dmnk1jFhKJOSv0ETbp50W+qf6uORMh6TdIFhQgcqd6Vz7t/0FSU
6a6/2NU/d7oQGcC9RJgfLx28AMG2/S//kzlHmNiF8aMpLoYWaV8/tBw/aeojXL+Q
AQyTYSjBI43zatoCloXOxtm0MIkU/RC4zr7goQatiE6sPymg5JTkahyvH87Lz2SH
ukZn/c8DVCTwJ4N+tJvNZC7Fq04mhgNokfIygJNLBJTyLzaaKRcf1V9q5P0TAabS
tMx22d19ahcOUXkOzRW4N5x0jt6aPQuOtz6YQ7fBwTaAbp86h+NKvl13u3A771+L
vEoWqSUhXXAdVR1IkZvx0ZCWrp+CKtkdNKGVEXFePTRJwfc9pftFLaimR4+/Je1A
QzHv6WuL665m8YlGaAdZrK54a511vpcrWQBxL06MOEUzOVP9eJsW8IbOrOBfJY3I
uD/9GJx+1gb3fcUf3+jNtSzOlllxrwr/vdsWHfQ0gY2/M/Bt+SdU5tcCAwEAAQ==
-----END RSA PUBLIC KEY-----

これで準備ができました。
さて、今回は先述のフローの3〜8までの手順を自動化するBashスクリプトを書きましたのでそちらを使います。

file-encrypter – github.com/issei-m/openssl-command-tests

暗号化の場合は次のようにして実行します:

$ ./file-encrypter enc RSA公開鍵ファイル名 暗号化したいファイル名

実際に、もばらぶのロゴを先程の僕の鍵で暗号化してみましょう。まずはロゴをDLします:

$ curl -s -O http://mobalab.net/images/logo.png
もばらぶロゴ

一応md5も見てみましょう:

$ md5 logo.png
MD5 (logo.png) = 71869c2dad963c935638ba1f227428d6

続いて file-encrypter スクリプトで暗号化を行います。

$ ./file-encrypter enc ./issei-m.pem /path/to/logo.png             
Encrypted into: ./logo.png.enc

./[元のファイル名].enc と言う名前で出力されるので、今回は ./logo.png.enc に出力されました。
この1ファイルの中に、暗号化したAESの鍵と暗号化した本文が含まれているので、後はこのファイルを相手に送付するだけです。

それでは受け取り側を想定して復号を行います。復号も暗号の時とほぼ同じインターフェースです:

$ ./file-encrypter dec RSA秘密鍵ファイル名 復号したいファイル名

暗号化に使ったGitHubに登録している僕の鍵の秘密鍵は、唯一僕のマシンの中にだけありますので、それを使って復号を行います:

# 元のファイルがあると「すでに同名のファイルが有るよ」と怒られるので予め削除しておく事
$ ./file-encrypter dec /path/to/issei-m-private.pem ./logo.png.enc 
Decrypted into: ./logo.png

無事に元のファイル名でファイルが作られました。MD5を取ってみましょう:

$ md5 logo.png
MD5 (logo.png) = 71869c2dad963c935638ba1f227428d6

先ほどと一緒ですね。と言う事で、復号は無事にできました!

無事復活を遂げたもばらぶロゴ

おわりに

今回、TLS等でも使われている様な、暗号化のフローの一部をOpenSSLを使って検証してみました。
今回はシェルスクリプトを使いましたが、OpenSSL用のライブラリがあれば当然他の言語でも実装が可能ですので、興味がある方は試してみて下さい。

参考


  1. インシデントに気づいて速攻でサービスを停止できればOKですが ↩︎
  2. 実際にS3での事故はたまに見ます ↩︎
  3. よく検証されてる&使いやすいツールがインターネット上にはたくさんあるのでそちらを使って下さい ↩︎
  4. どうでもいいですが、暗号化の対語として「復号化」は誤りらしいです。個人的には通じればどちらでもOK派ですが ↩︎
  5. インターネット上では原則的に安全にファイルの送受信が困難な為に暗号処理を使いますが、共通鍵暗号の場合、結局その鍵自体にも同じ課題が課され、ジレンマになります ↩︎
  6. 因みに公開鍵暗号は南京錠によく例えられます。解錠した南京錠を使って誰でもロックを掛ける事ができますが、解錠だけは鍵の所有者だけが可能だからです ↩︎
  7. 当たり前ですが逆 (公開鍵から秘密鍵を取り出す) は不可能です ↩︎
  8. 普段私達がWebで使っているTLSでも同様のフローが使われています ↩︎

← 前の投稿

Serverless で複数の AWS アカウントを使用する

次の投稿 →

React Native Cameraで画像を正方形で保存する方法

コメントを残す