【Golang】公開鍵と共通鍵で暗号化する【rsa, aes, pem, x509, cipher】
前回のプログラムを改良してサーバ/クライアントが送信するメッセージを暗号化してみました。
【Golang】jsonの内容をデコードする - くどはむと猫の窓
前回までに作ったプログラムはクライアントが送ったメッセージに" too!"を付けてサーバが送り返すプログラムでした。この通信はTCP通信なので経路上を流れるメッセージは簡単に除くことができます。
試しにクライアントから「hoge」というメッセージを送り、WireSharkを使ってサーバ側が開いているポート番号でやりとりされる内容を覗いてみます。
2つの画像の右下にクライアントから送られた「hoge」とサーバから返された「hoge too!」の文字があります。
このようにTCP通信でやりとりされるメッセージは外部から簡単に見ることが可能です。
そこで今回はこのメッセージを暗号化し、第3者からは簡単に見ることができないようにしてみました。
具体的な手順としては
- (開始)
- クライアントから接続されたらサーバが公開鍵証明書を送る
- クライアントは送られてきた証明書が正しいことを確認する
- クライアントがセッション鍵(共通鍵)を生成し、それを公開鍵で暗号化してサーバに送る
- サーバは送られてきた「暗号化されたセッション鍵」を秘密鍵で復号化して取り出す
- クライアントはメッセージを共通鍵で暗号化し、サーバに送る
- サーバは送られてきた「暗号化されたメッセージ」を共通鍵を使って復号化してメッセージを取り出す
- サーバは送られてきたメッセージに「 too!」を付けたメッセージを作り、共通鍵で暗号化してクライアントに送る
- クライアントは送られてきた「暗号化されたメッセージ」を共通鍵を使って復号化してメッセージを取り出し、標準出力する
- (終了)
当然、共通鍵は1回のセッション(開始〜終了)ごとに新しいものを生成します。
少し長いですがサーバ側・クライアント側のコードを全部載せます。
サーバ側
クライアント側
これをWireSharkで見てみると
このようにちゃんと生の文章を見ることができなくなっています。
シェル上ではこのように表示されています。
ちゃんと平文で見れてます。
やっていることは上で説明した通りです。
共通鍵にはAES、暗号モードはCTRを使っています。大体はgolang.orgの例を参考にやっています。
ブロック暗号モード?初期化ベクトル?という方にはcryptoパッケージ群は少々読み解きにくいかもしれません(僕もそうでした)。
そんな方は下の2つのサイトを読むだけでもパッケージの読みやすさがかなり変わると思います(特に1つ目のサイトは必見です)。
私は貧乏学生なのでちゃんとした証明書を発行できるはずもなく、いわゆるオレオレ証明書を使っているので
options := x509.VerifyOptions{DNSName: config.Server.DNSName,Roots: roots,}}
とDNSNameだけで証明書を検証していますがちゃんとした証明書を使う場合はVerifyOptionsにもっと指定したほうが良いかも。
encryptWriteは平文を共通鍵で暗号化してTCPConnにWriteする関数、decryptReadはTCPConnからReadした暗号文を共通鍵で復号して平文を返す関数です。本当は両方ともTCPConnをレシーバにしたかったのですが自分で定義した型でないとレシーバにできないようですね。悲しいです。
Connから連続でReadしたいときなんかもっとうまいやり方がありそうな気がしますが見つからないので一回応答を返してからReadしています。
何か良い方法があれば是非教えて下さい。
それとgolang.orgの例では暗号文の先頭バイトにNonce(初期化ベクトル)を入れていますが、これって「暗号文の先頭バイトにNonceが入っているぞ」ということが知られたらあまり良くない気がしますがどうなんでしょう?
これもご存知の方、例よりもっと良い方法をお知りの方がいれば教えいただけると嬉しいです。
と、crypto以下の色々なパッケージを使ってtls/sslっぽい通信を実装しましたが実はこんなに面倒なことをしなくてもtlsパッケージがあります。大体のことをラップしてくれています。しかし、他の言語にも言えることですがGo言語は特にどれだけ多くのパッケージを知っているかが重要だと思っています。ググラビリティも低いですし。
なので今回はあえてtlsパッケージを使わずに実装してみました。
次の目標はDBを使うような改良、あと変数名が長くて真のGolanger(?)Gopherに怒られそうなのでもっと短くします(messageとか)。余力と時間があればtlsパッケージを使った形も書いてみたいです。
ʕ◔ϖ◔ʔ
【Golang】jsonの内容をデコードする
前回のプログラムを改良してserverのIPとポート番号をハードコーディングではなくjsonファイルから取得できるようにしました。
【Golang】Socket通信するプログラムを書いてみた - くどはむと猫の窓
今回参考にしたサイトは以下です。
- json - The Go Programming Language
- os - The Go Programming Language
- io#Reader - The Go Programming Language
- JSON and Go - The Go Blog
前回まではgolang.jpの方をまず最初に参考にしていたのですが、よく読んでいるとgolang.jpのosパッケージのOpen関数のシグネチャがgolang.orgのと違っていたりと、ちょっと信頼できるか怪しい?のでgolang.orgの方をまず最初に参考にするように変えました。
それにしてもこれは情報が古いのか、OpenFile関数と間違えているのか、どうなんでしょう。
話を戻します。
jsonパッケージのtype Decoderのところにコード例が載っています。
その中で使われている関数でまず目につくのはこれ
func NewDecoder(r io.Reader) *Decoder
NewDecoder関数はioパッケージのReaderインターフェースを実装している型の変数を引数に取り、Decoder型(のポインタ変数)を返すようです。
その次に目につくのはこれ
func (dec *Decoder) Decode(v interface{}) error
さっき返ってきたDecoderをレシーバとするDecode関数で、引数は空インターフェースです。つまりGoの型ならなんでも引数にしていいというわけですね。何それ。
「Unmrshalのところも読めよ」と書いてあるので読んでみると、jsonの型は以下の6つのどれかになるはずだから、この中でベストな型にデコードする的なことが書かれています。
bool, for JSON booleans float64, for JSON numbers string, for JSON strings []interface{}, for JSON arrays map[string]interface{}, for JSON objects nil for JSON null
またUnmarshalについてはJSON and Goにも書いてあり、ここでやっと「ああ、これと同じようなことが起きているのか」と分かりました。
type Decoderのところの例ではjsonの構造に合わせた構造体を定義し、その型の変数(ポインタ)を引数として渡していますが、これは引数として渡された構造体の変数名などの情報をもとに、型アサーションした結果が入れられているという感じでしょう。
JSON and Go下の方に例が載っていますが、構造体を用意しなくても自分で型アサーションを使って値を振り分ける関数を書けば同じようなことができるようです。interface便利。
とはいえ面倒なので例の通り構造体を使うことにしました。
ところで、NewDecoderの引数となっているReaderインターフェースはどのようなものでしょうか?そのままリンク先に飛ぶと
type Reader interface { Read(p []byte) (n int, err error) }
となっています。
このRead関数を実装している型ならよいのですね。
僕はjsonデータをjsonファイルから読みたいと思っていますが、NewDecoderに渡せるでしょうか?まずは読みたいファイルを開く関数を探します。
osパッケージのOpen関数がそれっぽいです。
func Open(name string) (file *File, err error)
この関数は読み込んだファイルのファイルディスクリプタをFile型の変数として返します。
このFile型については同じosパッケージ内に書かれています。
そのまま読み進めていくと...
func (f *File) Read(b []byte) (n int, err error)
ありました。ReaderインターフェースのRead関数と同じシグネチャです。
ということはFile型はReaderインターフェースを実装しているのでこれをNewDecoderに渡すことができそうですね。
まとめると以下のような順序になります。
- jsonと同じ構造の構造体を定義する
- Open関数でjsonファイルをオープン
- Open関数で得たファイルディスクリプタをNewDecoderに渡す
- NewDecoderのDecode関数を呼び出す(引数は構造体の変数)
ということでここまでの情報を元に改良したプログラムが以下になります。
サーバ側
サーバ側のjsonはシンプルです。
{
"Port": "55555"
}
クライアント側
クライアント側のjsonはこんな感じです。
{
"server": {
"IP": "192.xxx.xxx.xxx",
"Port": "55556"
}
}
これでjsonから設定情報を読み込むということができるようになりました。
他にもjsonに追加したくなったら構造体を修正してやればいいだけなので簡単ですね。
前回のプログラムではクライアントは自分のIPやポート番号も入力しなければなりませんでしたが、 DialTCP関数の第2引数をnilにすれば自動でローカルのIPと(空いてる)ポートを取ってくれるようなのでnilに変更しました。
次の目標は、今サーバ用もクライアント用もコマンドを打ったディレクトリからjsonファイルを探すのでこれをどこから呼び出しても特定のディレクトリから探すようにすること、そして通信を暗号化して送るように改良したいなと思っています。
【Golang】Socket通信するプログラムを書いてみた
最近Go言語を勉強しています。リッチな言語とはいえないですがその分軽いですし、何より楽しいです。
golang.jpやA Tour of Goをひと通り読み終えたのでまずはサーバとクライアントでSocket通信(のうちのTCP通信)を行うプログラムを書いてみました。
これを書くにあたって以下のページが非常に参考になりました。実際かなりお手本にさせてもらっています。
- net パッケージ - golang.jp
- os パッケージ - golang.jp
- 8.1 Socketプログラミング
- Golang Cafe #15 まとめ netパッケージ - taknb2nchのメモ
勉強、コーディングの手順としては
- golang.jpの必要なパッケージドキュメントを読む。
- 1を読んでどのようにコードを組んでいけば良いか分からなかったら参考になりそうなものを探す
- 参考にしているサイトで分からない関数が出てきたらgolang.jpに戻ってそこを読む
- golang.jpに載っていない関数などは本家を探す
という感じです。
以下がコードです。
まずはサーバ側
55555ポートを監視し続け、クライアントから接続されたらゴルーチンを作成しその中でR/Wしています。
接続ごとにR/Wの処理部分をゴルーチンとして切りだすのでクライアントからの複数の接続を捌けます。
ここを書いているときに「スライス便利だな」と思いました。
messageBuf := make([]byte, 1024)messageLen, err := conn.Read(messageBuf)checkError(err)message := string(messageBuf[:messageLen])
Read関数はJavaなどと同じような使い方です。引数のbyte配列に読み込んだバイト列が入ります。これをstring型にキャストして保持するのですが、読み込んだバイト列は予め確保しておいた配列の長さより短い可能性が高いです。
そのため
messageBuf[:messageLen]
の部分で実際に読み込んだ長さまでスライスしたものをキャストしてます。
次がクライアント側です。
一部参考にできそうなところがなく、netパッケージを何度も読みながら書いたので不格好なコードかもしれません。
とはいえこれで立派にSocket通信ができます。
両方を見てもらえば分かる通りクライアントから送られたメッセージに"too!"をつけて返すだけの簡単なものですがなかなか勉強になりました。
ここからどんどん改良して少しづつ複雑なものを作っていきたいです。
今クライアント側でサーバのIPやポートをハードコーディングしているのでこれを外側から変更できるようにするのがひとまずの目標です。
これを引数で指定できるようにするのがプログラミング初学習のセオリーっぽいですがさすがに面白く無いのでjsonからパースして読み込めるようにでもしたいですね。