LoginSignup
29
26

GitHub の公開鍵でファイルを暗号化する(またはそのスクリプト)

Last updated at Posted at 2018-08-01

GitHub に登録している相手の SSH 公開鍵を使ってファイルを RSA 暗号化したい。簡単に。

TL; DR (今北産業)

  1. GitHub に公開されている RSA 公開鍵専用ですが、ユーザとファイルを指定するだけで暗号化できる bash スクリプトを作ってみました。

  2. リポジトリ@ GitHub (自分で実装してみたい方は TS; DR もご覧ください)

  3. 使い方。以下は、GitHub ユーザ「KEINOS」宛にファイル「himitsu.txt」を暗号化する例です。

    実行例
    $ # Encrypt file for user KEINOS @ GitHub
    $ enc KEINOS himitsu.txt
    
    具体的な実行例
    $ ls
    himitsu.txt
    
    $ enc KEINOS himitsu.txt
    KEINOS の GitHub 上の公開鍵を取得中 ... OK
    RSA 形式の公開鍵を PKCS8 形式に変換中 ... OK
    公開鍵でファイルを暗号化中 ... OK
    一時ファイルの削除中 ... OK
    
    暗号化を完了しました。このファイルを相手に送ってください。
    暗号化済みファイル: himitsu.txt.enc
    
    $ ls
    himitsu.txt  himitsu.txt.enc
    

🐒   デフォルトの状態の macOS HighSierra(OSX 10.13.6)で動作確認済みですが、bash, curl, ssh-keygen, openssl, md5 のコマンドが使える環境であれば Raspbian や Alpine でも動いたので大丈夫だと思います。

なお、秘密鍵をパスフレーズ付きにする場合はコメント欄もご覧ください。

あわせて読みたい

インストール

パスの通ったディレクトリで以下のコマンドを実行すると、インストール(ダウンロード&実行権限変更)できます。

$ curl "https://Qithub-BOT.github.io/QiiCipher/bin/enc" -o enc; chmod +x $_
$ enc --help
  • 実験的に試したい場合は、パスの通ってない適当なディレクトリで上記を実行し、$ enc --help の代わりに $ ./enc --help とパスを付けて指定してください。

スクリプトのソースとダウンロード

構文

enc <GitHubユーザアカウント名> <対象ファイルのパス> [ <暗号済みファイルの出力パス> ]

第3引数(オプション)が指定されていない場合は、「<対象ファイルのパス>.enc」として出力されます。

暗号化のフォーマット

以下の仕様で暗号化しています。

  • 鍵のタイプ:RSA 公開鍵 を PKCS8 形式に変換したもの
  • 鍵の所在: https://github.com/{$USERNAME}.keys で公開されている鍵の一番上のもの
  • 暗号化のタイプ: RSA
  • 強度:公開鍵の強度(長さ)に依存

復号

decdecrypt)コマンドで復号します。復号には GitHub の SSH 公開鍵とペアとなるローカルの秘密鍵を指定して使います。また enc 同様、コマンドをダウンロードして使用します。

dec ~/.ssh/id_rsa himitsu.txt.enc himitsu.txt

動作検証済み環境

  • macOS HighSierra (OSX 10.13.6)
  • openssl version: LibreSSL 2.2.7

TS; DR (所感と経緯と作り方)

機密情報のシェア

Qiita/Qiitadon ユーザのお遊びサークルのメンバー間で、センシティブなデータのやり取りが必要になりそうでした。お遊びに使っているサーバや無料ドメインのアカウント情報などを共有する必要が出てきそうだったのです。

Google ドキュメントなどでシェアするのもアリですが、メンバー全員が Googler と言うわけではなく、オーナーや管理も手間です。この時、まっさきに思いつくのがファイルを暗号化して相手に渡す方法です。

しかし、ZIP のパスワードなどのように共通鍵をシェアするのはこのご時世余りよろしくありません。かと言って、最強の方法であるハイブリッド暗号(公開鍵・秘密鍵暗号で共通鍵をシェアしてから、共通鍵で暗号化したデータを渡す方法)は、相手側への技術的負荷が高まります。

やはり、1度きりの情報共有なら、使い捨ての公開鍵と秘密鍵を使うのが楽です。(楽か?)

公開鍵と秘密鍵暗号とは

「公開鍵・秘密鍵暗号」とは、ペアとなる A と B の2つの鍵があり、片方の鍵で暗号化したものは、もう片方の鍵でしか復号できないタイプの暗号方式です。

以下は、Qiita 記事の「hashアルゴリズムとハッシュ値の長さ一覧(+ハッシュ関数の基本と応用)」の「ハッシュ関数の基本と特徴」からの抜粋です。

🐒 【余談】「公開鍵・秘密鍵暗号」とは
 「公開鍵・秘密鍵暗号」とは、ペアとなる A と B の2つの鍵があり、片方の鍵で暗号化したものは、もう片方の鍵でしか復号できないタイプの暗号です。
 つまり A 鍵 で暗号化したものは同じ A 鍵 では復号できません。ペアとなる B 鍵 でしか復号できません。また、その逆もしかり・・・です。
 この特徴により、片方の鍵を公開しておき、その公開している鍵で相手に暗号化してもらえれば、もう片方の鍵を持つものでなければ復号できないデータが作れます。鍵を公開してるのに、安全に暗号通信やファイルのやりとりが行える魔法が使えます。鍵を公開してるのに、です。
 ちなみに「鍵」とは、実質的に、いわゆるパスワードと同じ役割をするものです。ワード(単語)ではなく、覚えられないくらいの長い不規則な文字列、もしくはそれをファイルにしたものを「鍵」と呼びます。そして一般的なパスワードとの違いは自分で決められるものではないことです。伝わるかわかりませんが、「復活の呪文」をファイルに落としたようなものです。(具体的な鍵の例 @ GitHub)
 しかし、本質的には「〜のカギとなる」や「キーポイント」のように「重要な部分を引き出すもの」を「KEY」と言います。配列の「key」(鍵)なども、「value」(価値)を引き出す同様の意味です。

 公開鍵・秘密鍵暗号で重要なのが A 鍵B 鍵 は「親」と「子」の 1:1 の関係になっていることです。「親の鍵」からは同じ「子の鍵」は何度でも作成できますが、その逆(子から親の鍵)は作成できません。
 そのため「親」の鍵で暗号化したデータは同じ「親」の鍵では復号できませんが、復号に必要な「子」は作れてしまうということです。つまり、ユーザーは「親の鍵」さえ大切に保管しておけばよく、必要な時に「子の鍵」を作成し、外部に「子の鍵」を公開するのが一般的です。
 このことから親の鍵を「秘密鍵」、子となる鍵を「公開鍵」と呼びます。秘密鍵は「シークレット・キーSecret Key」もしくは「プライベート・キーPrivate Key」、そして公開鍵は「パブリック・キーPublic Key」とも呼ばれます。
 注意点として、「秘密鍵」「公開鍵」は必ずしも暗号化・復号に使うための鍵を指すわけではありません。「value」(価値)を引き出すのに必要な「key」(鍵)のうち、公開するものを「公開鍵」、公開してはいけないものを「秘密鍵」と言います。たまたま、暗号化にも当てはまるというだけです。暗号化に使わないものでも、秘密鍵や公開鍵が存在します。例えば署名鍵など。
 また、「公開鍵で暗号化」し「秘密鍵で復号する」といった説明の記事が多いのですが、暗号化自体はどちらの鍵でもできますし、反対の鍵で復号できます。ただ、内部的に秘密鍵でやっていることは復号だけです。
 この2つの鍵ですが、一般的(?)な方法として素数の概念が活用されています。
 例えば、自然数 15 を素因数分解すると 3 と 5 です。しかし、3 という数からは 15 はわかりません。3 を素数とする自然数は無限にあるからです。また、同様に 5 からも 15 はわかりませんが、3 と 5 からは 15 が簡単にわかったり、15 と 3 からは 5 が作成できるような考え方から、さらに工夫されています。もちろん他のアルゴリズムもあり、どれも難しすぎてわかりません。
 しかし、「公開鍵・秘密鍵暗号」は欠点も持っています。「大きなデータの暗号化が苦手」なのと「遅い」ことです。特に大きなデータを暗号化する場合は、データをチャンク(ぶつ切り)にしてから各々を個別に暗号化して 1 つにくっ付ける必要があり、復号も逆の手順が必要であるため結構なコストが発生します。サイズも必要以上に大きくなります。
 逆に従来の1つのパスワード(鍵)で暗号化および復号できる暗号方式を「共通鍵暗号」と呼びます。決して「共通鍵暗号」がオワコンと言うわけではありません。「共通鍵暗号」は強いアルゴリズムでも処理速度が速く、「公開鍵・秘密鍵暗号」と比べデータサイズが小さいという特徴があります。現在では、大きなファイルの復号や、復号に速度が求められる場合は「共通鍵暗号」で暗号化し、その共通鍵を「公開鍵・秘密鍵暗号でやりとりしておく」もしくは「お互いの公開鍵から生成する」というハイブリッドな手法が多くとられます。

ハッシュ関数の基本と特徴 | hashアルゴリズムとハッシュ値の長さ一覧(+ハッシュ関数の基本と応用 @ Qiita より)

公開鍵は GitHub のを使おうよ

秘密鍵はローカル(個人の管理の問題)なのでいいのですが、公開鍵をどうするかが問題でした。メールなどで教え合うのも面倒です。

GitHub はユーザー・アカウントページの URL に ".keys" を加えてアクセスすると、ユーザの SSH 用の公開鍵が確認できます。また、.gpg を付けると git push 時の verify を付けるのに必要な GnuPG の公開鍵が確認出来ます。

幸いメンバー全員が PR(プルリク)のために GitHub のアカウントを持っているので、これを利用しつつ、linux/macOS の標準機能を使って簡単に暗号化できるようにしたいなと思いました。(WSL2, Windows Subsystem for Linux2, などに OpenSSL を入れれば Windows でも使えるかもしれませんが未検証です)

git と GitHub を頻繁に利用している場合、おそらく GitHub に SSH の公開鍵を登録していると思いますが、それらが表示されます。SSH キーを登録していない場合は設定ページから「New SSH key」で RSA 公開鍵を登録できます。

また、GitHub では GPG/RSA の2種類の鍵を公開できるのですが、暗号方式は RSA 、暗号化には OpenSSL を利用することにしました。macOS で GPG を使うには別途インストールが必要だったからです。

無事スクリプトを作成することができたのですが、すべて Qiita 記事にある情報で作成できたので、その恩返しとして Qiita にスクリプトの中身を説明したいなと思いました。

また、スクリプトのソースは GitHub に公開しているので TS;DR から最新の全文ソースを見るかダウンロードしてください。

以下は、ソースが何をしているかとポイントの説明です。

ソースの概略

ヘルプの表示

コマンドの必須の引数が指定されていない場合はヘルプを表示させるようにしました。

「アカウント名」と「対象ファイル」の2項目は必須なので、引数の数が 2 より小さい場合はヘルプを表示。また、必須項目が足りないという意味で exit 1 (エラー)でスクリプトを終了させています。

以後のエラー番号も、すべて 1 で返しているためエラーの追跡にメッセージを頼るしかなく、わかりづらいと感じています。ソースの行数も少ないこともあり、エラー番号(エラーステータス番号)は $LINENO で現在の行番号を返すようにすればよかったと思いました。

また、ゴリゴリ echo するよりはヒアドキュメントを使った方が良かったかもしれません。

ソース
# ヘルプ表示
# ----------
if [[ $# < 2 ]]; then
  echo
  echo "使い方: $0 <github user> <input file> [<output file>]"
  echo
  echo "- <github user> : 相手の GitHub アカウント名"
  echo "- <input file>  : 暗号化したいファイルのパス"
  echo "[オプション]"
  echo "- <output file> : 暗号化されたファイルの出力先のパス"
  echo "                  指定がない場合は <input_file>.enc として出力されます。"
  echo "[注意]"
  echo "- このスクリプトは 1 ブロックぶんのデータのみ暗号化できます。"
  echo
  exit 1
fi

コマンド引数の取得

必須項目である、第1引数と第2引数の値を変数に代入しています。

変数名は、個人的には「NAME_USER」「FILE_INPUT」「FILE_OUTPUT」といった、〜 of 〜 と分けられる大カテゴリ順にスネークケースで分けた方が管理しやすいのですが、今回は読みやすさを優先し、変数は大文字に統一しました。

ソース
# コマンド引数取得
# ----------------
USERNAME=$1
INPUTFILE=$2

出力ファイル名の設定

デフォルトの出力ファイル名を変数「OUTPUTFILE」にセットしたのち、第3引数がある場合は上書きするようにしました。

ソース
# 出力ファイル名設定
# ------------------
OUTPUTFILE=$2.enc
if [[ $# == 3 ]]; then
  OUTPUTFILE=$3
fi

trap の設定

スクリプトが途中で失敗しても作業中の一時ファイル(USERNAME.*)が残らないよう、後処理として trap コマンドで終了時の処理を入れました。trap はクラスで言うデストラクタのような処理です。

いま思えば、ここはファイルの削除でなく、ランダムに作成した ID のディレクトリを作成し、ディレクトリごと削除した方がより確実だなと思いました。(rm -rf / と始まるだけでも心臓に悪いからです)

ソース
# trap の設定
# -----------
# スクリプト終了後一時ファイルを削除します。
# - 参考URL : https://qiita.com/m-yamashita/items/889c116b92dc0bf4ea7d
trap "rm -rf /tmp/$USERNAME.*" 0

一時ファイルのパス作成

md5 コマンドでランダムに作成した ID (変数 TMP)と、ユーザ名(変数 USERNAME)を組み合わせたものを作業ファイルの頭につけるようにしました。(<ユーザ名>.<ランダムID>.xxx

変数 PATHPUBKEY は、この後ダウンロードする公開鍵の保存先のファイル・パスです。

ここで /tmp ディレクトリを作業ディレクトリと決め打ちしているのですが、mktmp コマンドで今だけ必要なディレクトリを作成する方が確実だなと思いました。今後の改善点です。

ソース
# 一時ファイル
# ------------
TMP=`md5 -q -s $RANDOM`
PATHPUBKEY=/tmp/$USERNAME.$TMP.pub

RSA 公開鍵の取得

curl で GitHub 上のユーザーの公開鍵を取得し head コマンドで1行目のみを抜き出しています。macOS にはデフォルトで入っていない wget は使いませんでした。curl で正常に取得できなかった場合は exit 1 のステータスで終了しています。

公開された鍵は登録した順(古い順)に並ぶので、一番上と決めつけずにフィンガープリントなどを指定できれば良かったのですが、まずはシンプルさを優先しました。ここでも、エラーステータス番号は 1 に統一していますが、改善のある箇所だと思います。

ソース
# RSA 公開鍵の取得
# ----------------
# ユーザの GitHub の公開鍵一覧の1行目を取得
# - 取得先は: https://github.com/<user name>.keys
# - 参考URL : https://qiita.com/m0r1/items/af16c41475d493ab6774
echo -n "${USERNAME} の GitHub 上の公開鍵を取得中 ... "

curl -s https://github.com/$USERNAME.keys | head -n 1 > $PATHPUBKEY

if [[ $? != 0 ]]; then
  echo "NG:公開鍵を取得・保存できませんでした。"
  exit 1
fi
echo "OK"

公開鍵のフォーマット変換

GitHub で公開されている鍵のフォーマットが PEM でも PKCS8 形式でもない RSA 形式だったので、OpenSSL で使うために ssh-keygen コマンドで PKCS8 形式に変換しています。

ソース
# 公開鍵のフォーマット変換
# ------------------------
# - 参考URL :
#   - https://qiita.com/drobune/items/bf5d689eff7f69ed6866
#   - https://qiita.com/connvoi_tyou/items/3e86b6b68c3f398b3244
echo -n "RSA 形式の公開鍵を PKCS8 形式に変換中 ... "

ssh-keygen -f $PATHPUBKEY -e -m pkcs8 > $PATHPUBKEY.pkcs8

if [[ $? != 0 ]]; then
  echo "NG:RSA -> PKCS8 変換中にエラーが発生しました。"
  exit 1
fi
echo "OK"

データの暗号化

本コマンドの全てと言っていい箇所です。openssl コマンドの rsautilRSAユーティリティ)を使って暗号化しています。

この時に渡す引数は3つです。pkcs8 形式の公開鍵、対象となるファイルのパス(変数 INPUTFILE)と出力先のパス(変数 OUTPUTFILE)です。

公開鍵の強度(長さ)によって暗号化できるファイルサイズは異なるため、本来であれば事前にサイズをチェックするのが優しいと思うのですが、エラーを吐き出させる程度にしました。

今後の改善としては、サイズが大きすぎる場合は、圧縮 & ランダムな共通鍵で AES 暗号化してから、その共通鍵を暗号化する方法を提案するようにしたいと思います。

なお、以前までは -ssl オプションを付けていたのですが、 OpenSSL のバージョンが LibreSSL 2.6.5 以降では廃止されたため、Mojave ではエラーが出るので排除しました。

ソース
# データの暗号化
# ------------
# このスクリプトは 1 ブロックで可能なデータ量のみ暗号化します。そのため長いテキ
# ストや大きいデータは暗号化できません。
# - 参考URL : https://qiita.com/kunichiko/items/3c0b1a2915e9dacbd4c1
# - RSA鍵のビット長 = 最大暗号化可能バイト数
#        768 =   85
#       1024 =  117
#       2048 =  246
#       4096 =  502
#       8192 = 1018
echo -n "公開鍵でファイルを暗号化中 ... "
openssl rsautl -encrypt -pubin -inkey $PATHPUBKEY.pkcs8 -in $INPUTFILE -out $OUTPUTFILE

if [[ $? != 0 ]]; then
  echo "NG:暗号化に失敗しました。ファイルのサイズなど、エラー内容を確認ください。"
  exit 1
fi
echo "OK"

一時ファイルの削除

ダウンロードした公開鍵と pkcs8 形式に変換した公開鍵を削除しています。

上記で trap による同じ後処理を入れているのですが、スクリプト終了表示前に、明示的に作業ファイルを削除していることを表示させたかったので、trapはバックアップの削除処理として、あえて同じ作業をさせています。ただ、DRY の原則で考えると関数化した方がいいのかもしれません。

ソース
# 一時ファイルの削除
# ------------------
echo -n "一時ファイルの削除中 ... "
rm $PATHPUBKEY
rm $PATHPUBKEY.pkcs8

if [[ $? != 0 ]]; then
  echo "NG:一時ファイルの削除に失敗しました。 '/tmp/' ディレクトリ内を手動で削除してください。"
  exit 1
fi
echo "OK"

終了表示

一連の処理が終了した旨のメッセージを表示し、正常終了ということでステータース「0」で exit 0 でスクリプトを終了させています。

ソース
# 終了表示
# --------
echo
echo "暗号化を完了しました。このファイルを相手に送ってください。"
echo "暗号化済みファイル: ${OUTPUTFILE}"
echo
exit 0

おわりに

そもそも、コマンドラインで暗号化するリテラシーを持ち合わせたユーザー同士でないと通じ合えないという悲しいジレンマ。

29
26
12

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
26