アプリケーション開発ポータルサイト
ServerNote.NET
Amazon.co.jpでPC関連商品タイムセール開催中!
カテゴリー【C/C++
【HostAP】Diffie-Hellman方式で得た共有鍵を使ってAES暗号化・復号化を行う
POSTED BY
2023-10-12

前回のDiffie-Hellman(ディフィ・ヘルマン)方式の鍵交換を行うの続きです。

システムXとYしか知らない共有鍵ができましたので、データを共有鍵で混ぜれば、第三者に傍受されても安全です。
Xはデータを共有鍵で暗号化してYに送信し、Yは受信したデータを同じ共有鍵で復号化します。つまり可逆な暗号化でなければなりません。

共有鍵を使った可逆暗号化で最も定番なのはAES(Advanced Encryption Standard)です。
無線通信の世界ではデータを小さいパケットに分けて、1個1個暗号化して送信します。このとき使われるのがAES-CTR 128bit暗号化です。

AES-CTR 128bitは以下の関数により暗号化または復号化を実行します。

aes_128_ctr_encrypt(
const u8 *key, /* AES暗号鍵128ビット(=16バイト) */
const u8 *nonce, /* CTRカウンター128ビット(=16バイト) */
u8 *data, /* 符号対象入出力データ128ビット(=16バイト) */
size_t data_len /* 16固定 */
);

暗号化と復号化に区別はありません。たとえばdataにAAAが入った状態で呼ぶとdataはBBBになり、BBBが入った状態で呼ぶとAAAに戻ります。

keyには共有鍵を指定し、nonceの上位64ビットは共有鍵から生成した固定値、下位64ビットはパケット番号が入ります(0から始まるカウンタ)。
keyとnonceの上位64ビットは事前にシステムXとYとの間で共有されますが、nonceの下位64ビット=パケットカウンタ値に関しては、その値をパケットの先頭にでも送信側が常に付与しないと、受信側は復号化できません。

つまり1パケットのデータ構造は以下のような感じになります。

0~63ビット目:packet_counter/パケットカウンタ値(8バイト)
64~191ビット目:aes_data/暗号化された実データ(16バイト)
192~255ビット目:reserved/未使用(予約)領域(8バイト)

通信開始=カウンタ初期値から順調に受信できてれば受信側でカウンタの予想はつきますが、電波状態が悪いとかでパケットロスが発生すると合わなくなり復号化できなくなります。パケットに必ずカウンタ値を含めれば安心です。

まず、前回作成した共有鍵K(1536ビット=192バイト)を、key(128ビット=16バイト)とnonce上位64ビット(=8バイト)に変換します。
Linuxの無線通信定番ソフトwpa_supplicant&hostapdのソースコードであるhostapを使用します。
hostapには上記AES関数aes_128_ctr_encryptはもちろん、1536ビットの鍵を192ビットに変換するPRF-192関数が含まれています。

PRF-192関数はIEEE 802.11-2012/11.6.1.2 PRFの項で以下のように定義されています。内部ハッシュ関数としてHMAC-SHA-1を採用しています。

H-SHA-1(K, A, B, X) ← HMAC-SHA-1(K, A || Y || B || X)

PRF(K, A ,B, Len)
    For i ← 0 to (Len+159)/160 do
        R ← R||H-SHA-1(K, A, B, i)
    Return L(R, 0, Len)

PRF-192(K, A, B) = PRF(K, A, B, 192)

上記関数PRF-192(K, A, B)に、入力として、

K: 共有鍵の上位64バイト
A: 固定値 今回は文字列"EXAMPLE"とします
B: 共有鍵の下位128バイト

を与えます。その結果として、192ビット(24バイト)の情報が得られます。これはハッシュ値であるので、K,A,Bが同じなら毎回必ず同じ値が出ます。
この192ビットの上位128ビット(16バイト)をaes_128_ctr_encryptのkey、下位64ビット(8バイト)をnonceの上位64ビットとします。

以上がAES暗号化に必要な作業の概要です。では、実践していきます。

1、hostapソースの取得とライブラリのコンパイル

git clone git://w1.fi/srv/git/hostap.git
cd hostap/src
make

あとはソースでhostap/src以下ヘッダをインクルードして、
hostap/src/utils/libutils.aとhostap/src/crypto/libcrypto.aをリンクすれば、
aes_128_ctr_encrypt、sha1_prf関数が使えるようになります。

2、共有鍵からデータをAES暗号化・復号化するサンプルソース

C/C++aes.cGitHub Source
/*
hostapライブラリを使用したAES暗号化テスト

git clone git://w1.fi/srv/git/hostap.git
cd hostap/src
make

リンクライブラリはhostap/src/{crypto/libcrypto.a,utils/libutils.a}
およびOpenSSLの -lssl -lcrypto

テストMAINビルド(親ディレクトリにhostapがある場合)
gcc -D_COM_AES_MAIN -I.. aes.c ../hostap/src/crypto/libcrypto.a
../hostap/src/utils/libutils.a -lssl -lcrypto
*/
#include <openssl/bn.h>
#include <openssl/dh.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* hostap include */
#ifdef __cplusplus
extern "C" {
#endif
#include "hostap/src/utils/includes.h"
#include "hostap/src/utils/common.h"
#include "hostap/src/crypto/sha1.h"
#include "hostap/src/crypto/crypto.h"
#include "hostap/src/crypto/aes_wrap.h"
#ifdef __cplusplus
} /* extern "C" */
#endif

/* 16進文字列をバイト配列に変換 */
extern unsigned char *com_hexstr2bin(const char *str, unsigned char *bin) {
  int i, n;
  unsigned int x;
  n = strlen(str);
  if (!bin)
    bin = (unsigned char *)malloc(n);
  for (i = 0; i < n; i += 2) {
    sscanf((char *)(str + i), "%02X", &x);
    bin[i / 2] = x;
  }
  return bin;
}

/* バイト配列を16進文字列に変換 */
extern char *com_bin2hexstr(const unsigned char *bin, size_t n, char *str) {
  int i, j;
  if (!str)
    str = (char *)malloc(n * 2 + 1);
  for (i = 0, j = 0; i < n; i++, j += 2) {
    sprintf(str + j, "%02X", bin[i]);
  }
  return str;
}

#ifdef _COM_AES_MAIN
extern int main(int argc, char **argv) {
  /* 共有鍵を取得した所からスタート */
  /* 共有鍵の取得まではdh.cのmainを参照 */
  const char *share_key_str =
      "F538EAF42B189A2EDA578897D35B217F25336CAE68658792B4385CB8FC9FB072C3895C7F"
      "1778AFC3F43BA40C2A9C26B577344742C8E47C76A79500E6EBDC1AE67346DD29DCE11DD9"
      "68802DB0429B1CF8DD74F2A65C332BD8DEF0F0E3071F8AFC8D76C8E4FA6C01DD9C8493AF"
      "AAAA8E5CC6B50052CD23F07DF672B3AB446CA4C87440F29D2780DF272F23203E31FB782B"
      "8FE149628D9BA3FD95E1489CEAA6AA29176E8DAD551425661AA04D539317B8D6968AD5EC"
      "DA8AB01D701F063FE7B4AD90";
  unsigned char *share_key_bin;
  unsigned char prf_k[64], prf_a[64], prf_b[128], prf_data[24];
  unsigned char aes_key[16], aes_nonce[16], aes_data[16];
  u_int64_t packet_counter;
  char buf[512];

  /* 共有鍵文字列をバイナリに戻す */
  share_key_bin = com_hexstr2bin(share_key_str, NULL);
  com_bin2hexstr(share_key_bin, 192, buf);
  fprintf(stdout, "Diffie-Hellman SHARED KEY: %s\n", buf);

  /* 1536ビット共有鍵をhostap/prf-192関数で192ビットデータに変換 */
  memcpy(prf_k, share_key_bin, 64);       /* K: 共有鍵上位64バイト */
  strcpy(prf_a, "EXAMPLE");                /* A: 固定値 今回は EXAMPLE */
  memcpy(prf_b, share_key_bin + 64, 128); /* B: 共有鍵下位128バイト */
  memset(prf_data, 0, sizeof(prf_data));
  if (sha1_prf(prf_k, 64, prf_a, prf_b, 128, prf_data, 24) != 0) {
    fprintf(stderr, "sha1_prf() error.\n");
    return (-1); /* 変換失敗 */
  }
  com_bin2hexstr(prf_data, 24, buf);
  fprintf(stdout, "SHA1-PRF-192 OUTPUT: %s\n", buf);

  /* 得られた192ビットの上位128ビットをaes_key、
  下位64ビットをaes_nonceの上位64ビットとする */
  memcpy(aes_key, prf_data, 16);
  memcpy(aes_nonce, prf_data + 16, 8);

  /* あとは送信側がaes_nonceの下位64ビットにパケット番号(カウンタ)、
  aes_dataに好きなデータを入れて暗号化して送る */
  packet_counter = 12345;                    /* とりあえず12345番 */
  memcpy(aes_nonce + 8, &packet_counter, 8); /* セット aes_nonce完成 */
  strcpy(aes_data,
         "あいうえお"); /* UTF-8では3バイト×5+ヌル文字\0でちょうど16バイト */

  /* なおパケット先頭でpacker_counterを送っておかないと、受信側はaes_nonceを完成できないので注意
   */

  /* ダンプ */
  fprintf(stdout, "暗号化前のデータ---------------\n");
  com_bin2hexstr(aes_key, 16, buf);
  fprintf(stdout, "AES KEY: %s\n", buf);
  com_bin2hexstr(aes_nonce, 16, buf);
  fprintf(stdout, "AES NONCE: %s\n", buf);
  com_bin2hexstr(aes_data, 16, buf);
  fprintf(stdout, "AES DATA: %s\n", buf);
  fprintf(stdout, "AES DATA(STR): %s\n", aes_data);

  /* aes128_ctr暗号化 */
  if (aes_128_ctr_encrypt(aes_key, aes_nonce, aes_data, 16) != 0) {
    fprintf(stderr, "sender aes_128_ctr_encrypt() error.\n");
    return (-1); /* 暗号化失敗 */
  }

  /* ダンプ */
  fprintf(stdout, "暗号化したデータ---------------\n");
  com_bin2hexstr(aes_key, 16, buf);
  fprintf(stdout, "AES KEY: %s\n", buf);
  com_bin2hexstr(aes_nonce, 16, buf);
  fprintf(stdout, "AES NONCE: %s\n", buf);
  com_bin2hexstr(aes_data, 16, buf);
  fprintf(stdout, "AES DATA: %s\n", buf);

  /* データを受信したと仮定して、復号化する */
  /* packet_counterをパケット先頭で受信して、受信側もaes_nonceを正しく完成させたものとする
   */

  /* aes128_ctr復号化 */
  if (aes_128_ctr_encrypt(aes_key, aes_nonce, aes_data, 16) != 0) {
    fprintf(stderr, "receiver aes_128_ctr_encrypt() error.\n");
    return (-1); /* 復号化失敗 */
  }

  /* ダンプ */
  fprintf(stdout, "復号化したデータ---------------\n");

  memcpy(&packet_counter, aes_nonce + 8, 8);
  fprintf(stdout, "PACKET_COUNTER: %lld\n", packet_counter);

  com_bin2hexstr(aes_key, 16, buf);
  fprintf(stdout, "AES KEY: %s\n", buf);
  com_bin2hexstr(aes_nonce, 16, buf);
  fprintf(stdout, "AES NONCE: %s\n", buf);
  com_bin2hexstr(aes_data, 16, buf);
  fprintf(stdout, "AES DATA: %s\n", buf);
  fprintf(stdout, "AES DATA(STR): %s\n", aes_data);

  return 0;
}
#endif /* _COM_AES_MAIN */

共有鍵を取得するまではこちらを参照
共有鍵からsha1_prf関数でAES暗号鍵とnonce上位64ビットを得て、下位64ビットにカウンタ値をセットしてデータをaes_128_ctr_encrypt。
異なる2つのシステム間でのデータ暗号化送信・受信復号化を仮定して1つのソースで簡易的にシミュレートしています。

3、テストmainコンパイル

このサンプルソースの親ディレクトリにhostapがあるとします。インクルードパス-I..を指定します。
例:
hostap/src
sample/aes.c

gcc -D_COM_AES_MAIN -I.. aes.c ../hostap/src/crypto/libcrypto.a ../hostap/src/utils/libutils.a -lssl -lcrypto

hostapライブラリは内部でOpenSSLを使っているので、-lssl,-lcryptoと追加リンクします。

4、実行結果

./a.out

Diffie-Hellman SHARED KEY: F538EAF42B189A2EDA578897D35B217F25336CAE68658792B4385CB8FC9FB072C3895C7F1778AFC3F43BA40C2A9C26B577344742C8E47C76A79500E6EBDC1AE67346DD29DCE11DD968802DB0429B1CF8DD74F2A65C332BD8DEF0F0E3071F8AFC8D76C8E4FA6C01DD9C8493AFAAAA8E5CC6B50052CD23F07DF672B3AB446CA4C87440F29D2780DF272F23203E31FB782B8FE149628D9BA3FD95E1489CEAA6AA29176E8DAD551425661AA04D539317B8D6968AD5ECDA8AB01D701F063FE7B4AD90
SHA1-PRF-192 OUTPUT: A2B5EC3741328FC726C9F2A64A1815C075C1E08D97FD3E31
暗号化前のデータ---------------
AES KEY: A2B5EC3741328FC726C9F2A64A1815C0
AES NONCE: 75C1E08D97FD3E313930000000000000
AES DATA: E38182E38184E38186E38188E3818A00
AES DATA(STR): あいうえお
暗号化したデータ---------------
AES KEY: A2B5EC3741328FC726C9F2A64A1815C0
AES NONCE: 75C1E08D97FD3E313930000000000000
AES DATA: 436E7369C8536F47AECEBA50B1AEAD76
復号化したデータ---------------
PACKET_COUNTER: 12345
AES KEY: A2B5EC3741328FC726C9F2A64A1815C0
AES NONCE: 75C1E08D97FD3E313930000000000000
AES DATA: E38182E38184E38186E38188E3818A00
AES DATA(STR): あいうえお

AES_DATAがaes_128_ctr_encryptにより暗号化され、そのままの状態でもう一度aes_128_ctr_encryptを呼べば、あいうえおに無事復号化されることが確認できました。

実際はネットワーク間の2つのマシンの間でこのやりとりがなされるので、送るほうはhtonl、受けるほうはntohlなどのバイトオーダー変換が必要になってきます。
またaes_nonceの下位64ビットを完成させるためのカウンタ値は、送る側で常にパケットの先頭にセットして送り、受信側に教えてあげないと、受信側は復号化できないので注意が必要です。

※本記事は当サイト管理人の個人的な備忘録です。本記事の参照又は付随ソースコード利用後にいかなる損害が発生しても当サイト及び管理人は一切責任を負いません。
※本記事内容の無断転載を禁じます。
【WEBMASTER/管理人】
自営業プログラマーです。お仕事ください!
ご連絡は以下アドレスまでお願いします★

☆ServerNote.NETショッピング↓
ShoppingNote / Amazon.co.jp
☆お仲間ブログ↓
一人社長の不動産業務日誌
【キーワード検索】