WindowsAPIのCryptAPIをWindows以外で再現する方法

概要

WindowsAPIのCryptAPIをWindows以外で再現する方法

目的

WindowsAPIの暗号化をWindowsAPIが存在しないマシン上で動かしたいと思う場合があります。その時の備忘録です。

目標としては以下のシステムの暗号化部分をpythonに置き換えることです。また詳細説明も素晴らしく、良い記事だと思います。 トラストソフトウェアシステム様

以下のソースコードを対象とします。 対象ソースコード

使うpythonのライブラリはpycryptodome 3.18.0です。 PyCryptodome

キーコンテナの準備

CryptAcquireContext

暗号化方式をここで指定します。

  //CSPのハンドル
  if(!CryptAcquireContext(&hProv, NULL, MS_ENHANCED_PROV, PROV_RSA_FULL, 0))
    if(!CryptAcquireContext(&hProv, NULL, MS_ENHANCED_PROV, PROV_RSA_FULL, CRYPT_NEWKEYSET))
    {
      fprintf(stderr, "CryptAcquireContext error\n");
      return 1;
    }
MS_ENHANCED_PROVについて

MS_ENHANCED_PROVにより、key長は128bitということがわかります。 これが非常に重要です。後でしっかり効いてくるのでこの存在を覚えておきましょう。

Microsoft Enhanced Cryptographic Provider

PROV_RSA_FULLについて

暗号化についてはRC2orRC4で、ハッシュ関数はMD5orSHAということがわかります。 今回のトラストソフトウェアシステム様のブログではSHAを使っております。

PROV_RSA_FULL

目的 サポートされているアルゴリズム
キー交換 RSA
署名 RSA
暗号化 RC2 or RC4
ハッシュ MD5 or SHA

Hash計算

CryptCreateHash

ここではハッシュ計算の方式を決めます。 特に注意することはありません。CALG_SHAでSHA-1ハッシュが使われていることがわかります。

 // ハッシュ計算のインスタンス
    if(!CryptCreateHash(hProv, CALG_SHA, 0, 0, &hHash))
    {
        fprintf(stderr, "CryptCreateHash error\n");
        return 2;
    }

CryptHashData

ここも特に記載することはありません。passwordに対してSHA-1ハッシュを当てるという意味です。

 // ハッシュ値の計算
  #define  PASSWORD    "password"

    if(!CryptHashData(hHash, (BYTE*)PASSWORD, (DWORD)strlen(PASSWORD), 0))
    {
        fprintf(stderr, "CryptHashData error\n");
        return 3;
    }

対応するPythonのコード

ここまでのpythonのコードは以下のようになります。

  from Crypto.Hash import SHA


  key = b'password'
  hash = SHA.new(key).digest()
  # hash.hex() => 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8: 20*8 160bits

ここで気を付けるポイントとして、hash関数からは160bitsが返ってくるという事です。 C++pythonの出力をデバッガで比べていただければ一致することがわかります。

鍵の生成

ここがとても重要です。

 // 鍵生成
  #define  KEYLENGTH_128   0x0080 * 0x10000  // 128-bit長

    if(!CryptDeriveKey(hProv, CALG_RC4, hHash, KEYLENGTH_128, &hKey))
    {
        fprintf(stderr, "CryptDeriveKey error\n");
        return 4;
    }

KEYLENGTH_128 0x0080 * 0x10000

ここが非常に苦しんだ点で、CryptDeriveKeyの第4引数として、鍵の長さ(上位16bits)と生成法(下位16bits)を指定してあります。 で、なんとデバッグしたところ160bitsのhash値を128bitsまで切り捨ててあげれば正常に動作することがわかりました。 まあ指定した桁数以上は見ないということでしょう。 つまり以下のpythonコードで動作します。 下位16bitsの0x10000の指定は元のC++のコードでも不必要なのではないでしょうか?128bitsギッチリ指定してあるように見えます。

  truncated_key = hash[:-4] # 160 - 4 * 8 = 128

CryptDeriveKey 関数 (wincrypt.h)

CALG_RC4

RC4のstream encryptionが指定してあります。 CALG_RC4 | RC4 stream encryption algorithm | Key length: 40 bits.Salt length: 88 bits. -- | -- | --

Base Provider Algorithms

対応するPythonのコード

で、ようやく完成したコードが以下です。

  from Crypto.Cipher import ARC4

  truncated_hash = hash[:-4] # 160 - 4 * 8 = 128
  cipher = ARC4.new(truncated_hash)

暗号化

 // 暗号化
    BYTE    pbData[100] = "This is a test data.";
    DWORD   dwDataLen = (DWORD)strlen((char*)pbData) + 1;

    if(!CryptEncrypt(hKey, 0, TRUE, 0, pbData, &dwDataLen, 100))
    {
        fprintf(stderr, "CryptEncrypt error\n");
        return 5;
    }

ここも特にいうことはありません。

対応するPythonのコード

  msg = cipher.encrypt(data)

完成したソースコード

def encrypt(data: bytes) -> bytes:
  key = b'password'
  hash = SHA.new(key).digest()

  truncated_hash = hash[:-4]
  cipher = ARC4.new(truncated_hash)
  msg = cipher.encrypt(data)
  return msg

c++のデバッガのおかげで128bitsに切り詰める部分が分かりました。どっか公式見落としてるだけですかね?ひとまず以上です。

参考文献

トラストソフトウェアシステム様 対象ソースコード Microsoft Enhanced Cryptographic Provider 文字列の暗号化、復号化をするには(AES-128-ECB)

NoCodeを採用しない方が良い3つの理由

NoCodeを用いるとコードを書かずにサービスを構築することができます。筆者はNoCodeを用いた大規模なシステムの拡張開発を行なった経験があるので、所感をここに残しておきます。

 

本記事では、NoCodeを採用しシステム構築する前に考慮すべきポイントと、NoCodeを採用しない方が良い理由について解説します。

 

TL;DR

今は2023年です。CopilotやCode Whisperなどがあるのでコード書ける人はコード書きましょう。ビジネスサイドの方が突貫で間に合わせて、後々replaceする前提で使うのはアリだと思います。大きい問題点は本番環境しか作られない場合が多いという点です。

 

本番環境のみになりがち

NoCodeでシステム構築を行うというのは、全てSaaS上で開発を行うという事です。これは個人のlocal環境は存在しないという事です。で、ギリギリ作ることが可能なのはstaging環境なのですが、実はNoCodeである程度のシステムを作るとお金がかかります。productionと同じ環境を維持するのは単純に二倍の費用がかかるため、production一本でサービスを動かすことが多いと思われます。(50万->100万だとstaging要る?って気になりませんか?)

 

また、stagingを作ったとしても、うまく行ったのでデプロイ...ではなく同じ作業をブラウザ上でポチポチもう一度する必要があります。二つの環境の差異を無くすのは至難の技でしょう。

 

よって、私の関わったシステムは本番環境しかありませんでした。これで何が起こるかというと、新機能を追加するためにIntegromatを触ると、AirTableのデータが壊れて徹夜でデータ復旧、みたいな事になります。本番で動いている、昨日も今日も明日も莫大なお金を作るシステムの追加開発は本番環境のみであるのはリスクであることは明白ではないでしょうか。

 

(また似た問題として、コードを書く際もawsのlambdaやAppSync, DynamoDB等もlocalで動かさずに進める事もあると思いますが、Bad Patternだと思っています。LocalStackなどの開発への導入を検討しましょう)

 

複数人での開発が非常にしづらい

上記の問題点より、複数の(完全に同じ)環境を持つことは非常に困難です。

この際にシステムの近い部分の編集を行う際にはどうすれば良いのでしょうか?gitならコンフリクトを自動で差分を見て管理してくれます。これを人力でやる必要があります。

実際には一人である程度の範囲を完全に任せる方針になるでしょう。

 

乱雑にいらないものが残る

AWSでもよくありますよね。いらないものが残ったままになる現象。あれなんで残ったままになりやすいのか本当に不思議です。NoCodeではもう少し酷い感じになります。

なぜなら開発をするたびにproductionのAirTableの別のcolumnに列を追加して、そこにformulaを書いて、integromatに流して...のように既存のものに影響しないように作業を行います。そしてそれが何に影響しているのか、知るのはただ一人です。このfoo2というcolumnを消して良いのか、productionを壊して今日一日の業務オペレーションを止めないか、わかりません。localで動かして、とかできたらまだマシなんですけどね。

 

他にももっと

- AirTableデータ量が多すぎるとそもそも閲覧がかなり重い

- コードで言うコメントのような機能があったりなかったりしてドキュメント整備されないがち

- dirty hackを行う場面が非常に多い。(全然自由じゃないので)

- サ終されたら普通に業務が止まる(やりたいことの実現のための選択肢が個人的にやってる小さいサービスしかない場合も多い。例えば何かをpdfにしたりとか)

- ブラウザをポチポチするのが苦痛。vimが使えないエディタですら不自由なのにマウス一つで全てを操作するのは自分には無理だった(エンジニアが離れるかも)

- チームから動かなくなったと報告されると大体実行単位使い切ってたりする(これはGitHub Actionsとかもそうだけど)

 

等々ありますが、この辺で。

 

まとめ(それでも採用して良い時)

今は2023年でgithub Copilotという素晴らしすぎるツールがあります。範囲選択してFix bug buttonを押すとバグが治る時代に、わざわざNoCodeを使うメリットはほぼ皆無だと思います。

所感として、NoCodeを触っていると、IaCの逆を行くような気持ちになります。コード化されてないものをコード化しましょうという取り組みの逆です。色々なコード支援が失われるのはエンジニアとしても非常に辛いものでしょう。

TL;DRにも書きましたが、ビジネスサイドの方が突貫で使うようなサービスに今後はなっていくと考えています。