最近はセキュリティエンジニアで、フォレンジック、マルウェア解析でどうにか生活できるようになりましたが。
昔は、しがないプログラマだったのですよねぇ。
そのせいか、昔取った杵柄でたまにプログラムを書く機会があるのですが。
今回も例に漏れず、色々あってなんとなくパスワード管理ツールを自作してみようなんて思ったわけですが(どんな状況なんだろうか?自分でもよくわからないw)。
割と細かいこと忘れてるんですよね。
そしてさらに、
その頃って暗号プログラム書いたことなかったんだよね。
そんなわけで、Windowsで暗号プログラムを書こうと思った際に、色々調べたり試してみたり、バグで泣いたりしたので、サンプルソースを置いておこうと思った次第です。
いや、前書いたソース掘ればいいっちゃいいんだけど・・・。「ブログに書いておけば大抵のところから見れる」という安直な理由だったりします。
もちろん、ソースがクソ過ぎてGithubになんて恥ずかしくて上げられやしないってのが大ですがね!
さて、Windowsには「CryptAPI」というのがあり、こちらを使えば割と簡単に暗号化ができます。
今回は、CryptAPTを使った暗号プログラムソースをメモっておきます。
ハッシュ関数
この前説いるのかしらw
ハッシュ関数は「メッセージダイジェスト」とも呼ばれ、与えられたデータを一方向に暗号化する関数です。
もっともよく知られている利用法の一つとして、パスワードの保存があります。
他にも、ファイルの改ざん検知、電子署名など様々な分野で使われています。
ハッシュ関数にも色々種類があり、有名なものを挙げると、古いものではMD2、MD5などがあり、比較的新しいものにSHA-1、新しいものにSHA-2、SHA-3などがあります。
他にも、Windows特有のLMハッシュ、NTLMハッシュなどがあり、このあたりを詳しく調べたければWikipediaを見た方が早いとか、真面目に暗号の本を買った方がいいです。
規格化されているハッシュ関数は、ソースも公開されています。
以下にいくつか例を挙げておきます。
MD5 (RFC 1321)
https://www.rfc-editor.org/rfc/rfc1321.txt
SHA-1 (RFC 3174)
https://www.rfc-editor.org/rfc/rfc3174.txt
SHA-2 (RFC 6234)
https://www.rfc-editor.org/rfc/rfc6234.txt
https://www.rfc-editor.org/rfc/rfc4634.txt
このあたりのソースを流用して実装してもいいのですが、折角なので今回はCryptAPIを使って実装してみたいと思います。
理由は・・・
毎回コード書いたり、ソース移植しなくて良くて簡単だから。
ガッツリやりたい人はFIPSやRFC、暗号の本を読んで勉強してください!!
私は易き方向に逃げる!!(なんつー技術ブログだ・・・)
Hash計算時のCryptAPIのざっくりな流れ
前提:
- "WinCrypt.h"をincludeすること。
概略フロー:
- Cryptコンテキストの取得(CryptAcquireContext)
- ハッシュオブジェクトを生成(CryptCreateHash)
- ハッシュ値を計算(CryptHashData)
- ハッシュ計算結果の取得(CryptGetHashParam)
- ハッシュオブジェクトの開放(CryptDestroyHash)
- Cryptコンテキストの開放(CryptReleaseContext)
ポイント:
- 利用するCryptAPIの種類と順番、概ねのパラメータ。
- ハッシュオブジェクトを生成時、ハッシュアルゴリズム(SHA-1など)を指定する。
CryptAPIを使ったHash計算サンプルソース
一応、VisualStudio2017で動作することが確認されたソースです。
もし流用するのであれば、パラメータ等については、用途に合わせて変えてください。
MicrosoftのMSDNを参考にするとよいでしょう。
また、例によってタブが上手く入らなくてスペースでインデントしているので、適宜直してください。
関数の仕様:
static int Sha1(char *pData, int iSize, BYTE *pHashValue, int iHashValueSize);
第1引数で渡したデータをSHA-1ハッシュ計算し、第3引数の領域に格納し返す。
引数:
pData:ハッシュ化したいデータのバッファ
iSize:ハッシュ化したいデータのサイズ
pHashValue:ハッシュ化したデータの受取りバッファ
iHashValueSize:ハッシュ化したデータの受取りサイズ
返り値:
0:正常終了
負値:異常終了(詳細はヘッダのdefine参照)
制限:
大きなサイズのデータには未対応。
intのサイズ制限に引っかかるので、2GB以上は無理(DWORDにすれば拡張可能)。
また、CryptHashDataのサイズパラメータがDWORDのため、約4GB以上は関数の仕様的に無理だと思われる。
第3引数の領域はハッシュ値を格納するのに十分な領域を設定しておくこと。
(後で思ったけど、第4引数をintのポインタにしておけば、CRYPTHASH_ER_SIZESMALLの時に必要なサイズを返してあげることができるかな・・・と。といってもまあ、SHA-1が20バイトって変わらないから、必要性ほぼないかな。)
CryptHash.h
CryptHash.cpp
やっぱりソースコードの掲載には向かねぇw
思いつきでコードを書いちゃったので、型とか色々モノ申すところがありそうなので、流用する人は適宜調整を。
第1引数が char * なのは、これを呼びだすプログラムでインプットがASCIIのテキストになっているからです。素直に BYTE * で、呼び出す側がキャストでいいよね(直せよって言われそう)。
可変長データはデータクラス作った方がよさそうだと思いつつ、今回面倒なのでやってないです。
(掲載するソースが増えるけど見にくいし・・・)
データクラスにしておけば、デストラクタで領域開放でき、呼び出し側が意識しなくて良くなるので、第3引数は領域未確保でも動的に領域確保してデータを格納するように作れるかなと思う。
関数内でメモリアロケーションしていないのは、関数内でHeapAllocを使うか、VirtualAllocを使うか、newを使うか、mallocを使うかといったことによる制限を作りたくないため。
(関数内で勝手に領域確保してポインタを渡すと、「関数の仕様をちゃんと理解していないおバカにゃん」が領域開放漏れしてくれるので、個人的に好きじゃない。また、関数の呼び出し側が領域の開放方法も把握しなければならないので、分かりにくい。
データクラスにするなら、基底の領域確保関数を Virtual で作っておいて、実装を VirtualAlloc版と HeapAlloc版作って、呼び出す側が利用状況に応じて適したデータクラスを使い分ける、といったことを妄想したくなる。)
さいごに
とりあえず動くことは確認したんだけど、細かいバグとか指摘してくれると嬉しいなぁ。
AESも書く予定だけど、ちょっと待ってね。