ホーム 主筆 その他ソフト その他情報 Syuhitu.org English

Windows関連

スクリーンセーバー作成法

半透明ウインドウの性能

bootfont.bin

キャビネット形式

ウインドウスタイルをいじる

Java製ソフトをServiceに登録する

イベントログにメッセージを出力する

コントロールパネルにアイコンを追加する

スクリプトによる拡張1

スクリプトによる拡張2

ガジェットの作成

大容量メモリ

メモリ搭載量の下限に挑む

スパースファイルにする

表示されるアイコンの種類を調べてみた

メモリマップIOとエラー処理

ファイルを作る順番と速度の関係

Cryptography API: Next Generationを使う

Windows 10のアクセントカラー

iSCSIディスクにバックアップを取る

サーバプロセスを分離して実装する

サーバプロセスを分離して実装する - F#

レジストリに大量に書き込む

Solaris関連

OpenGL

Solaris設定

ディレクトリの読み込み

主筆プラグイン開発

マルチスレッドでの開発

door

音を出す

Blade100の正しい虐め方

パッケージの作成

画像入出力

BMPファイル

ICOファイル

ANIファイル

JPEGファイル

減色アルゴリズム

減色アルゴリズムの並列化

その他アルゴリズムなど

自由軸回転

Base64

文字列操作

CPU利用率の取得

正規表現ライブラリ

メタボールを作る

メタボールを作る2

正規表現とNFA・DFA

C言語の構文解析

液晶ディスプレイを解体してみた

iSCSIの理論と実装

単一フォルダにファイルを沢山作る

USB-HUBのカスケード接続

SafeIntの性能

VHDファイルのフォーマット

USBメモリに書き込み続けてみた

CNG:共通鍵暗号で暗号化

2016年1月31日公開

CNGで共通鍵暗号による暗号と復号を行う。

概要

まずは暗号と復号を行う手順の概要を示す。

大まかな手順としては、

  1. アルゴリズムプロバイダを構築する
  2. 対象鍵を生成する
  3. イニシャルベクタを作る
  4. 暗号/復号を行う

となる。

アルゴリズムプロバイダは、乱数ハッシュ値の生成と同じでBCryptOpenAlgorithmProvider関数を呼び出してやればいい。

面倒なのは上記の2と3(というより、そこに至るまでの必要な情報の収集)である。

 

共通鍵の生成

共通鍵を作るためには、少なくとも以下の情報を事前に取得しておかなければならない。

  1. キーオブジェクトのサイズ
  2. 共通鍵を生成するために使用する、シークレットのサイズ

キーオブジェクトのサイズは、以下で取得する。

NTSTATUS r = BCryptGetProperty(
  hAlgorithm, BCRYPT_OBJECT_LENGTH, (unsigned char*)( &KeyObjectLength ), sizeof( KeyObjectLength ), &Result, 0
);
if ( r != 0 )
  printf( "BCryptGetProperty失敗\n" );
else
  printf( "BCryptGetProperty成功 キーオブジェクトのバイト長=%d\n", KeyObjectLength );

KeyObjectLengthはDWORDである。なお、第二引数にはBCRYPT_OBJECT_LENGTHを指定しているが、実はMSDNを見るとBCRYPT_KEY_OBJECT_LENGTHという値も定義されていたりもする。しかし、俺の乏しい英文解読能力によれば、BCRYPT_KEY_OBJECT_LENGTHは使わずにBCRYPT_OBJECT_LENGTHを使えと書いてあるように見えるから、ここではBCRYPT_OBJECT_LENGTHを指定している。

次に、シークレットのサイズを取得する。

シークレットとは、共通鍵を生成するために使用するパスワードのようなものだと、俺は心得ている。だが、普通のパスワードとは違って、入力するデータはランダムなバイナリ値であり、かつ指定できるバイト長には暗号化アルゴリズムに応じた制約がある。

まずは、以下で値を取得する。

BCRYPT_KEY_LENGTHS_STRUCT KeyLength;
NTSTATUS r = BCryptGetProperty(
  hAlgorithm, BCRYPT_KEY_LENGTHS, (unsigned char*)( &KeyLength ), sizeof( KeyLength ), &Result, 0
);
if ( r != 0 )
  printf( "BCryptGetProperty失敗\n" );
else
  printf( "BCryptGetProperty成功 キーのバイト長=%d\n", KeyLength.dwMaxLength );

コード自体は先ほどとさして違いはない。

値を取得しているKeyLengthという変数は、下記のような構造体である。

typedef struct __BCRYPT_KEY_LENGTHS_STRUCT {
  ULONG dwMinLength;
  ULONG dwMaxLength;
  ULONG dwIncrement;
} BCRYPT_KEY_LENGTHS_STRUCT, BCRYPT_AUTH_TAG_LENGTHS_STRUCT;

dwMinLengthは指定できる最小のバイト長、dwMaxLengthは指定できる最大のバイト長、dwIncrementには、指定できるバイト長の倍数が設定される。つまり、dwMinLengthは64、dwMaxLengthが256、dwIncrementが64であれば、シークレットとして使える値は64バイト・128バイト・192バイト・256バイトのいずれかでなければならない。

共通鍵の生成は以下となる。

r = BCryptGenerateSymmetricKey(
  hAlgorithm, &hKey, pKeyObject, KeyObjectLength, pSecretData, KeyLength.dwMaxLength, 0
);
if ( r != 0 )
  printf( "BCryptGenerateSymmetricKey失敗\n" );
else
  printf( "BCryptGenerateSymmetricKey成功\n" );

pKeyObjectは、KeyObjectLengthで指定したバイト数分の適当なバッファである。mallocやnewで確保するかどうにかするコードは面倒だから省略する。

pSecretDataは、シークレットとして使用するバイナリ値が格納されたバッファである。ここに何を入れるのかはかなり重大な問題である。

イニシャルベクタの生成

イニシャルベクタは、CBCモードなどで暗号化を行う場合に必要になるもので、今時ECBモードを使うことはないだろうと思えば、必ず必要になるものである。

イニシャルベクタのサイズは、ブロック暗号のブロック長と同じになるはずで、以下のコードで取得することができる。

r = BCryptGetProperty(
  hAlgorithm, BCRYPT_BLOCK_LENGTH, (unsigned char*)( &IVLength ), sizeof( IVLength ), &Result, 0
  );
if ( r != 0 )
  printf( "BCryptGetProperty失敗\n" );
else
  printf( "BCryptGetProperty成功 イニシャルベクタのバイト長=%d\n", IVLength );

イニシャルベクタの中身は乱数でいいはずだが、暗号化するときと復号するときとで同じものを使用しなければならない。

暗号化

ここまできてようやく暗号にすることができる。

r = BCryptEncrypt(
  hKey,                      // 共通鍵のハンドル
  (unsigned char*)Hirabun1,  // 暗号化する対象となる平文
  sizeof( Hirabun1 ),        // 平文のバイト長
  nullptr,                   // パディングの情報(対象鍵の場合は使わない)
  pIV,                       // イニシャルベクタ
  IVLength,                  // イニシャルベクタのバイト長
  (unsigned char*)Angoubun,  // 暗号文を格納するバッファ
  sizeof( Angoubun ),        // 暗号文を格納するバッファのバイト長
  &Result,                   // 暗号文の出力バイト数が格納される
  0                          // フラグ
);

ここだけコメントが充実しているのは1行しか書くことがないからである。

ブロック暗号を使っている場合、平文はブロック長の倍数でなければらない。BCryptEncrypt関数のdwFlagsにBCRYPT_BLOCK_PADDINGを指定することで勝手にブロック長までパディングを詰めてくれるようにすることもできるようだが、復号する時のことや、その後も暗号化を継続するときのことなんかを考えると、パディングぐらい自分で管理した方がわかりやすいと思われる。

なお、暗号化アルゴリズムによっては、というかほとんど常にイニシャルベクタの内容は更新される。連続して暗号化を行う場合は、更新後のイニシャルベクタの値をそのまま指定するらしい。

復号

復号は以下のように行う。

r = BCryptDecrypt(
  hKey,                      // 共通鍵のハンドル
  (unsigned char*)Angoubun,  // 復号対象となる暗号文
  sizeof( Angoubun ),        // 暗号文のバイト長
  nullptr,                   // パディングの情報(対象鍵の場合は使用しない)
  pIV,                       // イニシャルベクタ
  IVLength,                  // イニシャルベクタのバイト長
  (unsigned char*)Hirabun2,  // 平文を格納するバッファ
  sizeof( Hirabun2 ),        // 平文を格納するバッファのバイト長
  &Result,                   // 平文の出力バイト数が格納される
  0                          // フラグ
);

暗号化するときとさして違いはない。

ただ、イニシャルベクタには暗号化した時のものと同じものを指定する必要がある。当然、鍵も同じでなければならない。対象鍵暗号なのだから。

 

使用例

上記全部をまとめると下記のようになる。

#include "stdafx.h"
 
#include <stdio.h>
#include <stdlib.h>
#include <tchar.h>
#include <Windows.h>
 
// CNGのヘッダファイル
#include <Bcrypt.h>
 
int _tmain(int argc, _TCHAR* argv[])
{
  // アルゴリズムプロバイダのハンドル
  BCRYPT_ALG_HANDLE hAlgorithm;
 
  // 対称鍵のハンドル
  BCRYPT_KEY_HANDLE hKey;

  // キーオブジェクト
  unsigned char *pKeyObject;

  // キーオブジェクトのバイト長
  unsigned long KeyObjectLength;

  // 暗号鍵のバイト長(シークレットのバイト長に等しい)
  BCRYPT_KEY_LENGTHS_STRUCT KeyLength;

  // イニシャルベクタ
  unsigned char *pIV;

  // イニシャルベクタのバイト長(ブロック長と等しい)
  unsigned long IVLength;

  // 対象鍵生成元となるシークレット
  unsigned char *pSecretData;

  // 平文のデータ
  char Hirabun1[64] = "abcdefghijklmnopqrstuvwxyz";

  // 暗号文
  char Angoubun[64];

  // 復号した平文
  char Hirabun2[64];

  // 作業用
  unsigned long Result = 0;

  // アルゴリズムプロバイダのハンドルを取得する
  NTSTATUS r = BCryptOpenAlgorithmProvider( &hAlgorithm, BCRYPT_AES_ALGORITHM, MS_PRIMITIVE_PROVIDER, 0 );
  if ( r != 0 )
    printf( "BCryptOpenAlgorithmProvider失敗\n" );
  else
    printf( "BCryptOpenAlgorithmProvider成功\n" );

  // キーオブジェクトのサイズを取得する
  r = BCryptGetProperty(
    hAlgorithm, BCRYPT_OBJECT_LENGTH, (unsigned char*)( &KeyObjectLength ), sizeof( KeyObjectLength ), &Result, 0
  );
  if ( r != 0 )
    printf( "BCryptGetProperty失敗\n" );
  else
    printf( "BCryptGetProperty成功 キーオブジェクトのバイト長=%d\n", KeyObjectLength );

  // イニシャルベクタのサイズを取得する
  r = BCryptGetProperty(
    hAlgorithm, BCRYPT_BLOCK_LENGTH, (unsigned char*)( &IVLength ), sizeof( IVLength ), &Result, 0
  );
  if ( r != 0 )
    printf( "BCryptGetProperty失敗\n" );
  else
    printf( "BCryptGetProperty成功 イニシャルベクタのバイト長=%d\n", IVLength );

  // キーのサイズを取得する
  r = BCryptGetProperty(
    hAlgorithm, BCRYPT_KEY_LENGTHS, (unsigned char*)( &KeyLength ), sizeof( KeyLength ), &Result, 0
  );
  if ( r != 0 )
    printf( "BCryptGetProperty失敗\n" );
  else {
    KeyLength.dwMaxLength /= 8;
    printf( "BCryptGetProperty成功 キーのバイト長=%d\n", KeyLength.dwMaxLength );
  }

  // 必要なメモリ領域を確保する
  pKeyObject = (unsigned char*)( malloc( KeyObjectLength ) );
  pIV = (unsigned char*)( malloc( IVLength ) );
  pSecretData = (unsigned char*)( malloc( KeyLength.dwMaxLength ) );
 
  // 対象鍵生成元となるシークレットを生成する
  for ( unsigned long i = 0; i < KeyLength.dwMaxLength; i++ )
    pSecretData[i] = rand();	// 本当は、乱数はrandを使ってはいけない
 
  // 対象鍵を生成する
  r = BCryptGenerateSymmetricKey(
    hAlgorithm, &hKey, pKeyObject, KeyObjectLength, pSecretData, KeyLength.dwMaxLength, 0
  );
  if ( r != 0 )
    printf( "BCryptGenerateSymmetricKey失敗\n" );
  else
    printf( "BCryptGenerateSymmetricKey成功\n" );
 
  printf( "--- 暗号化 -----------------------------------------------\n" );
 
  // イニシャルベクタを生成する
  for ( unsigned long i = 0; i < IVLength; i++ )
    pIV[i] = (unsigned char)i;  // 本当は乱数にする
 
  // 暗号化する
  r = BCryptEncrypt(
    hKey,                       // 共通鍵のハンドル
    (unsigned char*)Hirabun1,   // 暗号化する対象となる平文
    sizeof( Hirabun1 ),         // 平文のバイト長
    nullptr,                    // パディングの情報(対象鍵の場合は使わない)
    pIV,                        // イニシャルベクタ
    IVLength,                   // イニシャルベクタのバイト長
    (unsigned char*)Angoubun,   // 暗号文を格納するバッファ
    sizeof( Angoubun ),         // 暗号文を格納するバッファのバイト長
    &Result,                    // 暗号文の出力バイト数が格納される
    0                           // フラグ
  );
  if ( r != 0 )
    printf( "BCryptEncrypt失敗\n" );
  else {
    printf( "BCryptEncrypt成功 暗号文=" );
    for ( int j = 0; j < sizeof( Angoubun ); j++ )
      printf( "%02X", (unsigned char)Angoubun[j] );
    printf( "\n" );
  }

  printf( "--- 復号 -----------------------------------------------\n" );

  // 暗号化時に使用したイニシャルベクタが必要になるため、
  // 復号に備えてイニシャルベクタを元に戻す
  for ( unsigned long i = 0; i < IVLength; i++ )
    pIV[i] = (unsigned char)i;  // 本当は乱数にする
 
  // 復号する
  r = BCryptDecrypt(
    hKey,                       // 共通鍵のハンドル
    (unsigned char*)Angoubun,   // 復号対象となる暗号文
    sizeof( Angoubun ),         // 暗号文のバイト長
    nullptr,                    // パディングの情報(対象鍵の場合は使用しない)
    pIV,                        // イニシャルベクタ
    IVLength,                   // イニシャルベクタのバイト長
    (unsigned char*)Hirabun2,   // 平文を格納するバッファ
    sizeof( Hirabun2 ),         // 平文を格納するバッファのバイト長
    &Result,                    // 平文の出力バイト数が格納される
    0                           // フラグ
  );
  if ( r != 0 )
    printf( "BCryptDecrypt失敗\n" );
  else
    printf( "BCryptDecrypt成功 平文=%s\n", Hirabun2 );
 
  // キーを破棄する
  r = BCryptDestroyKey( hKey );
  if ( r != 0 )
    printf( "BCryptDestroyKey失敗\n" );
  else
    printf( "BCryptDestroyKey成功\n" );
 
  // アルゴリズムプロバイダを破棄する
  r = BCryptCloseAlgorithmProvider( hAlgorithm, 0 );
  if ( r != 0 )
    printf( "BCryptCloseAlgorithmProvider失敗\n" );
  else
    printf( "BCryptCloseAlgorithmProvider成功\n" );
 
  // メモリ領域を解放する
  free( pKeyObject );
  free( pIV );
  free( pSecretData );
 
  return 0;
}

使用している暗号化アルゴリズムは128bitのAESである。平文と暗号文として64バイトの固定長の値を使用していることから、パディングについては何も考慮していないことに注意が必要である。

本来であれば、平文が何バイトなのかは不明なはずで、そのためにブロック長のバイト数になるようパディングを詰めてやらなければならない。しかも、復号した後でパディングを取り除くことができるよう、あらかじめ暗号文の中に平文のバイト数を入れておくなどの工夫も必要になる。だが、ここではそのようなことは一切考慮していない。

実行例

実行するとこうなる。

BCryptOpenAlgorithmProvider成功
BCryptGetProperty成功 キーオブジェクトのバイト長=618
BCryptGetProperty成功 イニシャルベクタのバイト長=16
BCryptGetProperty成功 キーのバイト長=32
BCryptGenerateSymmetricKey成功
--- 暗号化 -----------------------------------------------
BCryptEncrypt成功 暗号文=BFAF078034A4DFD4DB26F4E09A70EC4F859E5507
C2D8FCFA8707A44A604AB09FBAA533254103D0C0CC10E4E4CD5A9EEC94C714A08
2CE2DB3AC4A8EEA35CF4CA7
--- 復号 -----------------------------------------------
BCryptDecrypt成功 平文=abcdefghijklmnopqrstuvwxyz
BCryptDestroyKey成功
BCryptCloseAlgorithmProvider成功

余談だが、平文に対して暗号文が長い気もするが、それは平文が先頭26文字しか表示されていないためである。

暗号利用モードの指定

どうも必須ではないようだが、暗号利用モードを指定することもできるらしい。

  TCHAR ChainModeBuf[64];

// 今の暗号利用モードを取得する
  r = BCryptGetProperty(
    hAlgorithm, BCRYPT_CHAINING_MODE, (unsigned char*)ChainModeBuf, sizeof( ChainModeBuf ), &Result, 0
  );
  if ( r != 0 )
    printf( "BCryptGetProperty失敗\n" );
  else
    printf( "BCryptGetProperty成功 現在設定されている暗号利用モード=%S\n", ChainModeBuf );

  // とりあえずCFBモードに変えてみる
  _tcscpy_s( ChainModeBuf, BCRYPT_CHAIN_MODE_CFB );
  r = BCryptSetProperty(
    hAlgorithm, BCRYPT_CHAINING_MODE, (unsigned char*)ChainModeBuf, sizeof( ChainModeBuf ), 0
  );
  if ( r != 0 )
    printf( "BCryptSetProperty失敗\n" );
  else
    printf( "BCryptSetProperty成功\n", ChainModeBuf );

  // もう一度、暗号利用モードが変わったかどうか確認してみる
  r = BCryptGetProperty(
    hAlgorithm, BCRYPT_CHAINING_MODE, (unsigned char*)ChainModeBuf, sizeof( ChainModeBuf ), &Result, 0
  );
  if ( r != 0 )
    printf( "BCryptGetProperty失敗\n" );
  else
    printf( "BCryptGetProperty成功 現在設定されている暗号利用モード=%S\n", ChainModeBuf );

アルゴリズムプロバイダを構築した後で、上記を実行する。

アルゴリズムプロバイダでBCRYPT_AES_ALGORITHMを指定した場合だが、暗号利用モードはデフォルトではCBCモードだった。多分、CBCモードにしておいて駄目なことはないと思われる。

<< 「Cryptography API: Next Generationを使う」に戻る


連絡先 - サイトマップ - 更新履歴
Copyright (C)  2000 - 2016 nabiki_t All Rights Reserved.