バージョン管理された人

subversionで管理されてます

JSONのwalk

JSONファイルがいくつかあって、それらはスキーマは違っても取得したいキー名は固定であるなんてことがあるかもしれない。 そのときにJSONファイルのスキーマの違いを無視して特定キー名のプロパティを取得するようなことをしたいときに次のようなコード(Python)を利用すれば取得できる。

import json

def walk_json(key, jo):
    # 配列が来た場合はその配列の各要素の中身を探る
    if type(jo) is list:
        return sum(
            list(
                map(walk_json, jo)
            )
        )

    # JSONは `true` のみでも立派にJSONとして成り立ってしまうので、
    # `jo` がオブジェクト出ない場合は探したいキー名をプロパティには
    # 持っていない(そもそもプロパティを持ってない)のだから空リストを返す
    if type(jo) is not dict:
        return []

    # ここまで来るとJSONオブジェクトしかここにきていないはず
    if key in jo:
        # JSONオブジェクト内に欲しいキーがあればそれを返す
        return [jo[key]]
    # JSONオブジェクト内に欲しいキーがなればプロパティの中をさらに探索
    return sum(
        map(
            walk_json,
            filter(
                # JSONオブジェクトか配列以外はキーをプロパティに持つようなことはないため、
                # 探索する必要はない
                lambda jo: type(jo) is dict or type(jo) list,
                jo.values()
            ),
            []
        )

nodenvをgit cloneしただけだとinstallサブコマンドが現れない

この手のツールにありがち。 gitからクローンしたものだけだとinstallサブコマンドが現れない。

$ nodenv install
nodenv: no such command 'install'
$ node help
Usage: nodenv <command> [<args>]

Some useful nodenv commands are:
   commands    List all available nodenv commands
   local       Set or show the local application-specific Node version
   global      Set or show the global Node version
   shell       Set or show the shell-specific Node version
   rehash      Rehash nodenv shims (run this after installing executables)
   version     Show the current Node version and its origin
   versions    List installed Node versions
   which       Display the full path to an executable
   whence      List all Node versions that contain the given executable

See 'nodenv help <command>' for information on a specific command.
For full documentation, see: https://github.com/nodenv/nodenv#readme

解決方法

解決する手順は単純で、node-buildプラグインを所定の場所にインストールしてやるだけ。 詳しくはリポジトリを見ればインストール方法が書いてある。 もちろん、nodenvを入れてる前提。

$ mkdir -p (nodenv root)/plugins
$ git clone https://github.com/nodenv/node-build.git (nodenv root)/plugins/node-build
Cloning into '/home/hoge/.nodenv/plugins/node-build'...
remote: Enumerating objects: 116, done.
remote: Counting objects: 100% (116/116), done.
remote: Compressing objects: 100% (90/90), done.
remote: Total 19431 (delta 31), reused 45 (delta 15), pack-reused 19315
Receiving objects: 100% (19431/19431), 3.45 MiB | 3.33 MiB/s, done.
Resolving deltas: 100% (12452/12452), done.
$ nodenv help
Usage: nodenv <command> [<args>]

Some useful nodenv commands are:
   commands    List all available nodenv commands
   local       Set or show the local application-specific Node version
   global      Set or show the global Node version
   shell       Set or show the shell-specific Node version
   install     Install a Node version using node-build
   uninstall   Uninstall a specific Node version
   rehash      Rehash nodenv shims (run this after installing executables)
   version     Show the current Node version and its origin
   versions    List installed Node versions
   which       Display the full path to an executable
   whence      List all Node versions that contain the given executable

See 'nodenv help <command>' for information on a specific command.
For full documentation, see: https://github.com/nodenv/nodenv#readme
$ nodenv install --list
0.1.14
0.1.15
0.1.16
0.1.17
0.1.18
0.1.19
0.1.20
0.1.21
0.1.22
0.1.23
0.1.24
0.1.25
...

C++とOpenSSLライブラリを利用してデータの暗号化・復号化をAES-CBCで行う

C++でまともに暗号化・復号化するサンプルをやっている例を見ないので書く。 実際にはsaltがどうだとかパディングがどうだとかストレッチングがどうだとか暗号化する前にデータを圧縮する話だとかは出さない。 運用上は重要だけど、そういうのは後付けできる。 問題はOpenSSLをC++でまともにあつかう例がない方なので、ここではこのはなしをざっくり切ってどのように記述するのかを解説していく。

方針

OpenSSLライブラリを利用する小さなAES-CBCモードに限定したopensslコマンドを実装する。 小さいといっても興が乗って300行程になってしまったが、問題はないだろう。 実際に重要なところは合わせても100行も行かない。 また、ここではC++17前提で実装を進める。 それ以前のC++を利用する方は適宜読み替えて欲しい。 ただ、OpenSSLのライブラリはCで書かれているので、中核となる部分はC++11までならコンパイルできるはず(試していない)。 ただ、それもcstdintライブラリ内にあるuint8_tを用いるためなので、おそらくunsigned charに変更すれば(charが8 bitな環境であれば)C++98でもコンパイルできる...かもしれない。 興味があれば試して欲しい。

準備

さて、まずはいくつかデータ型を定義しておく。 まずはbyte型を定義する

#include <cstdint>
using byte = uint8_t;

C++17ではstd::byteという型があるが、これよりは符号なし8 bit整数を利用することを明確にしたいのでuint8_tの方を採用する。

さらに、この型を用いてバイトストリーム用の型を定義する。

#include <vector>
#include <cstdint>
using byte_stream = std::vector<byte>;

8 bitな動的配列は全てこのbyte_streamから作成する。

また、AES-CBCにおける鍵を表す型を定義する

#include <type_traits>
#include <array>
#include <cstddef>
template <
  size_t key_size,
  std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr
>
using key = std::array<byte, key_size / 8>;

templatestd::enable_if_tがはいってきて何やらややこしい。 しかし、やりたいことは単純で、鍵長key_sizeが128 bitか192 bitか256 bitなものしか鍵の型を生成できないように制限しているだけだ。 また、key_size / 8をしているのはkey_sizeの単位がbitでarrayが格納する要素の型は8 bit長なため、要素数key_size bitを要素の型の長さ8 bitで割ったものとなるためだ。

最後にAES-CBCにはIVが必要となるので、これを表現する型を導入する。

#include <array>
const auto BLOCK_SIZE = 16;
using iv = std::array<byte, BLOCK_SIZE>;

BLOCK_SIZE == 16なのはAESではブロック長が128 bit固定であり、IVもそのブロック長にする必要があるので、byte == uint8_tからその要素数128 / 8 == 16となるため。

コマンドのインターフェース

今回用意するコマンドのインターフェースは次のようにしようと思う。

  • 標準入力からデータを受け取って、受け取ったデータを暗号化した上で標準出力へ出力するサブコマンドencを持つ
  • 標準入力からデータを受け取って、受け取ったデータを復号化した上で標準出力へ出力するサブコマンドdecを持つ

どちらも標準入力から受け取るので、main関数はこんな感じでいいだろう。

#include <iostream>

int
main(int argc, char* argv[]) {
  if (argc < 1) {
    std::cerr << "no subcommand" << std::endl;
  }

  auto subcommand = std::string(argv[1]);
  if (subcommand == "enc") {
  } else if (subcommand = "dec") {
  }
}

ただ、これだけだとターミナルに移ったときに使い方がわからないので、helpサブコマンドとついでで-hオプションをコマンドに追加しておこう。

#include <iostream>
#include <unistd.h>
#include <string>

int
main(int argc, char* argv[]) {
  int opt;
  // -hオプションを追加するためにgetopt関数を使った
  while ((opt = getopt(argc, argv, "h")) != -1) {
    switch (opt) {
      case 'h':
        usage(program_path);
        return 0;
      default:
        return 1;
    }
  }

  if (argc < 1) {
    std::cerr << "no subcommand" << std::endl;
  }

  auto subcommand = std::string(argv[1]);
  if (subcommand == "help") {
    usage(argv[0]);
    return 0;
  }
  if (subcommand == "enc") {
  } else if (subcommand = "dec") {
  }
}

usageという関数はプログラムのパスargv[0]を受け取ってその使い方を表示してくれる。

#include <filesystem>
#include <iostream>
auto
usage(const std::filesystem::path& program_path) -> void {
  const auto program_name = program_path.filename().generic_string();

  std::cout
    << "OpenSSLライブラリの使用方法のサンプル"        << std::endl
    << "Usage:"                                       << std::endl
    << "  " << program_name << " enc"                 << std::endl
    << "  " << program_name << " dec"                 << std::endl
    << "  " << program_name << " help"                << std::endl
    << "  " << program_name << " -h"                  << std::endl
    << "Arguments:"                                   << std::endl
    << "  enc"                                        << std::endl
    << "    標準入力から受け取ったデータを暗号化し、" << std::endl
    << "    標準出力へ出力する"                       << std::endl
    << "  dec"                                        << std::endl
    << "    標準入力から受け取ったデータを復号化し、" << std::endl
    << "    標準出力へ出力する"                       << std::endl
    << "  help"                                       << std::endl
    << "    ヘルプを表示する"                         << std::endl
    << "Options:"                                     << std::endl
    << "  -h"                                         << std::endl
    << "    ヘルプを表示する"                         << std::endl;
}

AESの暗号化と復号化のインターフェースはいずれも鍵とIV、そしてデータを必要とするので、次のような形で良いだろう。

#include <type_traits>
#include <cstddef>
template <
  size_t key_size,
  std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr
>
auto
encrypt(
  const key<key_size>& key,
  const byte_stream& data,
  const iv& iv
) -> byte_stream;
template <
  size_t key_size,
  std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr
>
auto
decrypt(
  const key<key_size>& key,
  const byte_stream& data,
  const iv& iv
) -> byte_stream;

いずれも鍵、IV、データを受け取ってデータを暗号化・復号化した結果であるbyte_stream型なデータを返す。 インターフェースはどちらも同じなので、データを暗号化・復号化した結果を標準出力に返すために次のような関数を用意する

#include <type_traits>
#include <functional>
#include <stdexcept>
#include <algorithm>
#include <cstddef>
template <
  size_t key_size,
  std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr
>
auto
print_data_while_encoding(
  const byte_stream& original_key,
  const byte_stream& data,
  const byte_stream& original_iv,
  const std::function<byte_stream(const key<key_size>&, const byte_stream&, const iv&)>& encoder
) -> void {
  iv iv;
  key<key_size> key;
  if (iv.size() != original_iv.size()) {
    throw std::invalid_argument("iv length must be 128 bit ");
  }
  if (original_key.size() != key.size()) {
    throw std::invalid_argument("invalid key size");
  }
  std::copy(original_iv.begin(), original_iv.end(), iv.begin());
  std::copy(original_key.begin(), original_key.end(), key.begin());

  auto encoded_data = encoder(key, data, iv);
  for (const auto byte_value : encoded_data) {
    std::cout << byte_value;
  }
}

これは鍵、IV、データだけでなく、そららのデータを受け取って暗号化・復号化を行う関数encoderを受け取っている。 途中、byte_stream型で受け取った鍵とIVのデータ型を変換しているが、それ以外はencoderに通し、その結果を表示する部分と引数のチェックを行っている部分とわかれている。

OpenSSLによるAES-CBC暗号化・復号化

本題のOpenSSLによる暗号化・復号化に入っていく。

暗号化

最初に暗号化を行うencrypt関数の実装を行う。

まずは、次のように書く。

#include <openssl/evp.h>
#include <type_traits>
#include <cstddef>
template <
  size_t key_size,
  std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr
>
auto
encrypt(
  const key<key_size>& key,
  const byte_stream& data,
  const iv& iv
) -> byte_stream {
  EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
}

このctxを暗号化が終わるまで一貫して用いる。 このctxに対して暗号化モードや必要な設定・鍵・IVなどを保存した上でデータを暗号化・復号化するという仕組みになっている。

初期化コードを書いたらctxに暗号化スイートを設定しておくようにコーディングする。

#include <openssl/evp.h>
#include <type_traits>
#include <cstddef>
template <
  size_t key_size,
  std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr
>
auto
encrypt(
  const key<key_size>& key,
  const byte_stream& data,
  const iv& iv
) -> byte_stream {
  EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
  const EVP_CIPHER* cipher_suite;
  switch (key_size) {
    case 128:
      cipher_suite = EVP_aes_128_cbc();
      break;
    case 192:
      cipher_suite = EVP_aes_192_cbc();
      break;
    case 256:
      cipher_suite = EVP_aes_256_cbc();
      break;
  }
}

EVP_aes_(key-size)_cbcという関数がAES-CBC暗号化スイートの設定を作成し、返してくれる。

スイートの設定を作成したら、その設定と鍵、IVをctxに設定する。

#include <openssl/evp.h>
#include <type_traits>
#include <cstddef>
template <
  size_t key_size,
  std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr
>
auto
encrypt(
  const key<key_size>& key,
  const byte_stream& data,
  const iv& iv
) -> byte_stream {
  EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
  const EVP_CIPHER* cipher_suite;
  switch (key_size) {
    case 128:
      cipher_suite = EVP_aes_128_cbc();
      break;
    case 192:
      cipher_suite = EVP_aes_192_cbc();
      break;
    case 256:
      cipher_suite = EVP_aes_256_cbc();
      break;
  }
  EVP_EncryptInit(ctx, cipher_suite, key.data(), iv.data());
}

以上でAES-CBCによる暗号化を行う準備はととのった。 これから実際に暗号化処理を行う部分を実装するが、実は次のようなコードを追加するだけで大部分の実装が終わる。

#include <openssl/evp.h>
#include <type_traits>
#include <cstddef>
template <
  size_t key_size,
  std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr
>
auto
encrypt(
  const key<key_size>& key,
  const byte_stream& data,
  const iv& iv
) -> byte_stream {
  // 初期化処理は省略

  auto encrypted_data = byte_stream(
    raw_data.size() + (BLOCK_SIZE - raw_data.size() % BLOCK_SIZE)
  );
  int len = 0;
  EVP_EncryptUpdate(ctx, encrypted_data.data(), &len, data.data(), raw_data.size()
}

encrypted_dataのデータサイズの調整をおこなっている式についてはすぐ後で触れるので、ここでは忘れて欲しい。

最後に、データの後処理を行う。

#include <openssl/evp.h>
#include <type_traits>
#include <cstddef>
template <
  size_t key_size,
  std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr
>
auto
encrypt(
  const key<key_size>& key,
  const byte_stream& data,
  const iv& iv
) -> byte_stream {
  // 初期化処理と暗号化処理は省略

  EVP_EncryptFinal(ctx, encrypted_data.data() + len, &len)
  EVP_CIPHER_CTX_free(ctx);
  return encrypted_data;
}

最後にこのような処理をしなければならないのはAESではデータサイズがブロック長単位になっている必要があり、その部分の暗号化処理を行う必要があるからだ。 AESではブロック長は128 bitであることを思いだそう。 AESでは暗号化・復号化処理は1ブロック毎に行われる。 つまり、AESで処理されるデータ長は128 bit単位になっていなければならない。 しかし、データ長のbit数がAESで処理する128 bit長で割り切れない、すなわちデータ長が128 bit単位になっていないとき、最後1つ手前の128 bit長のブロックを処理したあとにはみ出した分のデータを処理するときにそのはみ出したデータの長さはちょうど128 bitでない場合がある。 そもそもほとんどのデータはちょうどそのデータ長が128 bitで割り切れるということは稀だろう。 そこで、パディングというある一定のルールに乗っ取ったデータをデータの末尾に追加するという処理を施してデータ長を128 bit長に帳尻合わせする必要がでてくる。 しかし、ここではOpenSSLがどのパディングを使用するかやそのパディングの詳細については述べない。 各自興味があれば調べて欲しい。

実はデータ長がピッタリ128 bitのながさであってももう1ブロック分のパディング処理が行われる。 これには理由があって、パディングがあるデータとないデータの区別をするのを頑張るよりは、128 bit長ちょうどのデータに対しても1ブロック分のパディング処理を施しておくことで処理を共通して行える上にどこからどこまでがパディングでデータであるかの区別を見分けやすくできるというメリットがある。

とにかく、最後のはみだしたデータはパディングという処理を施した上でまだ暗号化されていないはみだしたデータを暗号化する必要がある。 そこで、lenに暗号化できたデータ長を保存しておき、encrypted_dataの先頭アドレスencrypted_data.data()len分足した、パディングが施されたはみだしているデータに対して暗号化するという形をとる。 先程のencrypted_dataのサイズを調整しているのはこの処理のためである。 元々はみだすデータサイズはデータサイズ % ブロック長(128 bit)でわかるので、パディングされるデータはブロック長(128 bit) - データサイズ % ブロック長(128 bit)で求められるので、パディング分だけ保存する配列を伸ばしておく必要があったのだ。 最後に、ctrは動的にメモリー確保されてているのでEVP_CIPHER_CTX_freeでメモリーを開放する。 このとき、設定された鍵やIV以外のOpenSSL側で用意するEVP_aes_key-size_cbcのような動的に確保されているメモリーが開放される。

まとめると、encrypt関数は次のような形になる。

template <
  size_t key_size,
  std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr
>
auto
encrypt(
  const key<key_size>& key,
  const byte_stream& raw_data,
  const iv& iv
) -> byte_stream {
  auto ctx = EVP_CIPHER_CTX_new();
  if (ctx == nullptr) {
    throw "cannot initialize OpenSSL cipher context";
  }
  const EVP_CIPHER* cipher_suite;
  switch (key_size) {
    case 128:
      cipher_suite = EVP_aes_128_cbc();
      break;
    case 192:
      cipher_suite = EVP_aes_192_cbc();
      break;
    case 256:
      cipher_suite = EVP_aes_256_cbc();
      break;
  }
  if (cipher_suite == nullptr) {
    EVP_CIPHER_CTX_cleanup(ctx);
    throw "cannot get cipher suite";
  }
  if (EVP_EncryptInit(ctx, cipher_suite, key.data(), iv.data()) == 0) {
    EVP_CIPHER_CTX_cleanup(ctx);
    throw "cannot construct OpenSSL cipher context";
  }

  auto encrypted_data = byte_stream(
    raw_data.size() + (BLOCK_SIZE - raw_data.size() % BLOCK_SIZE)
  );
  int len = 0;
  if (EVP_EncryptUpdate(ctx, encrypted_data.data(), &len, raw_data.data(), raw_data.size()) == 0) {
    EVP_CIPHER_CTX_cleanup(ctx);
    throw "cannot encrypt data";
  }

  if (EVP_EncryptFinal(ctx, encrypted_data.data() + len, &len) == 0) {
    EVP_CIPHER_CTX_cleanup(ctx);
    throw "cannot padding";
  }
  EVP_CIPHER_CTX_free(ctx);
  return encrypted_data;
}

解説では省いていたエラー処理をここでは入れてある。

復号化

実は暗号化処理と復号化処理はほとんど同じような操作でできるので、解説することは少ない。 なので、いきなりコードを載せてしまう。

template <
  size_t key_size,
  std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr
>
auto
decrypt(
  const key<key_size>& key,
  const byte_stream& encrypted_data,
  const iv& iv
) -> byte_stream {
  EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
  if (ctx == nullptr) {
    throw "cannot initialize OpenSSL cipher context";
  }
  const EVP_CIPHER* cipher_suite;
  switch (key_size) {
    case 128:
      cipher_suite = EVP_aes_128_cbc();
      break;
    case 192:
      cipher_suite = EVP_aes_192_cbc();
      break;
    case 256:
      cipher_suite = EVP_aes_256_cbc();
      break;
  }
  if (cipher_suite == nullptr) {
    EVP_CIPHER_CTX_cleanup(ctx);
    throw "cannot get cipher suite";
  }
  if (EVP_DecryptInit(ctx, cipher_suite, key.data(), iv.data()) == 0) {
    EVP_CIPHER_CTX_cleanup(ctx);
    throw "cannot construct OpenSSL cipher context";
  }

  auto plain_data = byte_stream(encrypted_data.size());
  int plain_data_len = 0;
  if (EVP_DecryptUpdate(ctx, plain_data.data(), &plain_data_len, encrypted_data.data(), encrypted_data.size()) == 0) {
    EVP_CIPHER_CTX_cleanup(ctx);
    throw "cannot encrypt data";
  }

  int remain_plain_data_len;
  if (EVP_DecryptFinal(ctx, plain_data.data() + plain_data_len, &remain_plain_data_len) == 0) {
    EVP_CIPHER_CTX_cleanup(ctx);
    throw "cannot padding";
  }
  plain_data.resize(plain_data_len + remain_plain_data_len);
  EVP_CIPHER_CTX_free(ctx);
  return plain_data;
}

ほとんどEVP_EncryptXXXEVP_DecryptXXXになっただけである。 ただ、最後にデータからパディングをはがす処理が異なっているだけである。 ここではそのパディングをはがす処理について解説する。 まず、plain_data_lenには暗号化時にははみだしていなかったデータの復号化後の長さが入っている。 また、remain_plain_data_lenにはパデイングデータをのぞいた暗号化時にはみだしていたデータ分の長さがはいる。 つまり、暗号化前のデータ長はplain_data_len + remain_plain_data_lenである。 パディング処理によってデータの末尾には余計なデータがくっついているので、byte_streamvetctorであることを利用してresizeメソッドによってデータ長を暗号化前のデータ長に合わせてやる。 これによって真の復号化データを得ることができる。

コマンドの拡充

暗号化と復号化はできるようになったが、コマンドは最低限のインターフェースを供えただけで標準入力からデータを読み込んだり、出力したりIVや鍵を設定したりなどの部分が抜け落ちている。 したがってここではこれらを実装していくことにしよう。

まず、IVや鍵を設定しよう。 ただ、これは確認のためであれば全部を0埋めしたデータにしておけばよいだろうし、0埋めだけだと不安なので、デフォルトは0埋めにしておいて、あとでオプションから設定できれば良いだろう。 そこで-iオプションでIVを受け取り、-kで鍵を受け取るようにしよう。 ただし、これらはuint8_tな配列でなければならないので、これらオプションに指定する引数はhex string(16進数な数字の羅列)で受け取るようにする。

#include <iostream>
#include <cstddef>
#include <cstdint>
#include <type_traits>
#include <unistd.h>
#include <string>
#include <stdexcept>
#include <algorithm>

auto
convert_hex_string(const std::string& hex_string) -> byte_stream {
  if (hex_string.size() % 2 != 0) {
    throw std::invalid_argument("invalid hex string");
  }

  byte_stream byte_array(hex_string.size() / 2);
  for (std::size_t i = 0; i < hex_string.size(); i += 2) {
    auto&& byte_value = static_cast<uint8_t>(
      std::stoi(hex_string.substr(i, 2), nullptr, 16)
    );
    byte_array[i / 2] = byte_value;
  }

  return byte_array;
}

int
main(int argc, char* argv[]) {
  const auto program_path = argv[0];
  byte_stream iv(BLOCK_SIZE, 0);
  byte_stream key(128 / 8, 0);
  int opt;
  while ((opt = getopt(argc, argv, "hi:k:V")) != -1) {
    switch (opt) {
      case 'h':
        usage(program_path);
        return 0;
      case 'i':
        try {
          iv = convert_hex_string(optarg);
        } catch (const std::invalid_argument& error) {
          std::cerr << "invalid iv format" << std::endl;
          return 1;
        } catch (const std::out_of_range& error) {
          std::cerr << error.what() << std::endl;
          return 1;
        }
        if (iv.size() != BLOCK_SIZE) {
          std::cerr << "iv must be 128 bit" << std::endl;
        }
        break;
      case 'k':
        try {
          key = convert_hex_string(optarg);
        } catch (const std::invalid_argument& error) {
          std::cout << "invalid key format" << std::endl;
          return 1;
        } catch (const std::out_of_range& error) {
          std::cerr << error.what() << std::endl;
          return 1;
        }
        if (!(key.size() == 128 / 8 || key.size() == 192 / 8 || key.size() == 256 / 8)) {
          std::cerr << "key size must be 128, 192 or 256 bit" << std::endl;
        }
        break;
      default:
        return 1;
    }
  }

  if (argc - optind < 1) {
    std::cerr << "no subcommand" << std::endl;
    return 1;
  }
  const auto subcommand = std::string(argv[optind]);
  if (subcommand == "help") {
    usage(program_path);
    return 0;
  }

  if (subcommand == "enc") {
  } else if (subcommand == "dec") {
  } else {
    std::cerr << "unknown command: " << subcommand << std::endl;
    return 1;
  }
}

convert_hex_stringが受け取ったhex stringを符号無し8 bit整数配列に直す関数である。 この関数は2文字ずつ読んでいき、その2文字を8 bit整数を表す16進数文字列だと見なして符号無し8 bit整数に変換し、それをvectorにつめていくというものになっている。 また、-i-vはそれぞれhex stringを引数として受け取り、convert_hex_stringにかけて符号無し8 bit整数配列に変換したうえで、各鍵長、IV長に当っているかをみている。

-i-vオプションを追加したので、usage関数をアップデートする。

auto
usage(const std::filesystem::path& program_path) -> void {
  const auto program_name = program_path.filename().generic_string();

  std::cout
    << "OpenSSLライブラリの使用方法のサンプル"        << std::endl
    << "Usage:"                                       << std::endl
    << "  " << program_name << " enc"                 << std::endl
    << "  " << program_name << " dec"                 << std::endl
    << "  " << program_name << " help"                << std::endl
    << "  " << program_name << " -h"                  << std::endl
    << "Arguments:"                                   << std::endl
    << "  enc"                                        << std::endl
    << "    標準入力から受け取ったデータを暗号化し、" << std::endl
    << "    標準出力へ出力する"                       << std::endl
    << "  dec"                                        << std::endl
    << "    標準入力から受け取ったデータを復号化し、" << std::endl
    << "    標準出力へ出力する"                       << std::endl
    << "  help"                                       << std::endl
    << "    ヘルプを表示する"                         << std::endl
    << "Options:"                                     << std::endl
    << "  -h"                                         << std::endl
    << "    ヘルプを表示する"                         << std::endl
    << "  -i <iv>"                                    << std::endl
    << "    <iv>で指定された値でIVを作成する。"       << std::endl
    << "    <iv>はHex String形式(例: AB01DF93BC)で"   << std::endl
    << "    長さが128bitになるようにする必要がある"   << std::endl
    << "  -k <key>"                                   << std::endl
    << "    <key>で指定された値で鍵を作成する。"      << std::endl
    << "    <key>はhex String形式で長さが128 bit,"    << std::endl
    << "    192 bit, 256 bitのいずれかの"             << std::endl
    << "    長さになる必要がある。"                   << std::endl;
}

最後に、データを標準入力からうけとる部分と、暗号化・復号化したものを標準出力へ出力する部分を実装しよう。 データは符号無し8 bit配列に収めてやる。

#include <cstdio>
#include <cstdint>

int
main(int argc, char* argv[]) {
  // オプション処理部分等は省略

  byte_stream data;
  while (true) {
    auto byte_value = getc(stdin);
    if (byte_value == EOF) {
      break;
    }
    data.emplace_back(byte_value);
  }


  // データの暗号化・復号化部分は省略
}

データを暗号化・復号化した上で標準出力に出力するようにしてやる。

#include <iostream>
#include <cstdint>

int
main(int argc, char* argv[]) {
  // オプション処理部分やデータの入力部分は省略

    if (subcommand == "enc") {
    switch (key.size() * 8) {
      case 128:
        print_data_while_encoding<128>(key, data, iv, encrypt<128>);
        break;
      case 192:
        print_data_while_encoding<192>(key, data, iv, encrypt<192>);
        break;
      case 256:
        print_data_while_encoding<256>(key, data, iv, encrypt<256>);
        break;
      default:
        std::cout << "invalid key type" << std::endl;
        return 1;
    }
  } else if (subcommand == "dec") {
    switch (key.size() * 8) {
      case 128:
        print_data_while_encoding<128>(key, data, iv, decrypt<128>);
        break;
      case 192:
        print_data_while_encoding<192>(key, data, iv, decrypt<192>);
        break;
      case 256:
        print_data_while_encoding<256>(key, data, iv, decrypt<256>);
        break;
      default:
        std::cout << "invalid key type" << std::endl;
        return 1;
    }
  } else {
    std::cerr << "unknown command: " << subcommand << std::endl;
    return 1;
  }
}

コードのまとめ

以上のコードをまとめると、最終的には次のようなコードができている

#include <openssl/evp.h>
#include <iostream>
#include <vector>
#include <array>
#include <cstdint>
#include <cstddef>
#include <type_traits>
#include <unistd.h>
#include <string>
#include <filesystem>
#include <stdexcept>
#include <algorithm>
#include <functional>
#include <cstdio>

const auto BLOCK_SIZE = 16;

using byte = uint8_t;
using byte_stream = std::vector<byte>;
template <
  size_t key_size,
  std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr
>
using key = std::array<byte, key_size / 8>;
using iv = std::array<byte, BLOCK_SIZE>;

template <
  size_t key_size,
  std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr
>
auto
encrypt(
  const key<key_size>& key,
  const byte_stream& raw_data,
  const iv& iv
) -> byte_stream {
  auto ctx = EVP_CIPHER_CTX_new();
  if (ctx == nullptr) {
    throw "cannot initialize OpenSSL cipher context";
  }
  const EVP_CIPHER* cipher_suite;
  switch (key_size) {
    case 128:
      cipher_suite = EVP_aes_128_cbc();
      break;
    case 192:
      cipher_suite = EVP_aes_192_cbc();
      break;
    case 256:
      cipher_suite = EVP_aes_256_cbc();
      break;
  }
  if (cipher_suite == nullptr) {
    EVP_CIPHER_CTX_cleanup(ctx);
    throw "cannot get cipher suite";
  }
  if (EVP_EncryptInit(ctx, cipher_suite, key.data(), iv.data()) == 0) {
    EVP_CIPHER_CTX_cleanup(ctx);
    throw "cannot construct OpenSSL cipher context";
  }

  auto encrypted_data = byte_stream(
    raw_data.size() + (BLOCK_SIZE - raw_data.size() % BLOCK_SIZE)
  );
  int len = 0;
  if (EVP_EncryptUpdate(ctx, encrypted_data.data(), &len, raw_data.data(), raw_data.size()) == 0) {
    EVP_CIPHER_CTX_cleanup(ctx);
    throw "cannot encrypt data";
  }

  if (EVP_EncryptFinal(ctx, encrypted_data.data() + len, &len) == 0) {
    EVP_CIPHER_CTX_cleanup(ctx);
    throw "cannot padding";
  }
  EVP_CIPHER_CTX_free(ctx);
  return encrypted_data;
}

template <
  size_t key_size,
  std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr
>
auto
decrypt(
  const key<key_size>& key,
  const byte_stream& encrypted_data,
  const iv& iv
) -> byte_stream {
  EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
  if (ctx == nullptr) {
    throw "cannot initialize OpenSSL cipher context";
  }
  const EVP_CIPHER* cipher_suite;
  switch (key_size) {
    case 128:
      cipher_suite = EVP_aes_128_cbc();
      break;
    case 192:
      cipher_suite = EVP_aes_192_cbc();
      break;
    case 256:
      cipher_suite = EVP_aes_256_cbc();
      break;
  }
  if (cipher_suite == nullptr) {
    EVP_CIPHER_CTX_cleanup(ctx);
    throw "cannot get cipher suite";
  }
  if (EVP_DecryptInit(ctx, cipher_suite, key.data(), iv.data()) == 0) {
    EVP_CIPHER_CTX_cleanup(ctx);
    throw "cannot construct OpenSSL cipher context";
  }

  auto plain_data = byte_stream(encrypted_data.size());
  int plain_data_len = 0;
  if (EVP_DecryptUpdate(ctx, plain_data.data(), &plain_data_len, encrypted_data.data(), encrypted_data.size()) == 0) {
    EVP_CIPHER_CTX_cleanup(ctx);
    throw "cannot encrypt data";
  }

  int remain_plain_data_len;
  if (EVP_DecryptFinal(ctx, plain_data.data() + plain_data_len, &remain_plain_data_len) == 0) {
    EVP_CIPHER_CTX_cleanup(ctx);
    throw "cannot padding";
  }
  plain_data.resize(plain_data_len + remain_plain_data_len);
  EVP_CIPHER_CTX_free(ctx);
  return plain_data;
}

auto
convert_hex_string(const std::string& hex_string) -> byte_stream {
  if (hex_string.size() % 2 != 0) {
    throw std::invalid_argument("invalid hex string");
  }

  byte_stream byte_array(hex_string.size() / 2);
  for (std::size_t i = 0; i < hex_string.size(); i += 2) {
    auto&& byte_value = static_cast<uint8_t>(
      std::stoi(hex_string.substr(i, 2), nullptr, 16)
    );
    byte_array[i / 2] = byte_value;
  }

  return byte_array;
}

auto
usage(const std::filesystem::path& program_path) -> void {
  const auto program_name = program_path.filename().generic_string();

  std::cout
    << "OpenSSLライブラリの使用方法のサンプル"        << std::endl
    << "Usage:"                                       << std::endl
    << "  " << program_name << " enc"                 << std::endl
    << "  " << program_name << " dec"                 << std::endl
    << "  " << program_name << " help"                << std::endl
    << "  " << program_name << " -h"                  << std::endl
    << "Arguments:"                                   << std::endl
    << "  enc"                                        << std::endl
    << "    標準入力から受け取ったデータを暗号化し、" << std::endl
    << "    標準出力へ出力する"                       << std::endl
    << "  dec"                                        << std::endl
    << "    標準入力から受け取ったデータを復号化し、" << std::endl
    << "    標準出力へ出力する"                       << std::endl
    << "  help"                                       << std::endl
    << "    ヘルプを表示する"                         << std::endl
    << "Options:"                                     << std::endl
    << "  -h"                                         << std::endl
    << "    ヘルプを表示する"                         << std::endl
    << "  -i <iv>"                                    << std::endl
    << "    <iv>で指定された値でIVを作成する。"       << std::endl
    << "    <iv>はHex String形式(例: AB01DF93BC)で"   << std::endl
    << "    長さが128bitになるようにする必要がある"   << std::endl
    << "  -k <key>"                                   << std::endl
    << "    <key>で指定された値で鍵を作成する。"      << std::endl
    << "    <key>はhex String形式で長さが128 bit,"    << std::endl
    << "    192 bit, 256 bitのいずれかの"             << std::endl
    << "    長さになる必要がある。"                   << std::endl;
}

template <
  size_t key_size,
  std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr
>
auto
print_data_while_encoding(
  const byte_stream& original_key,
  const byte_stream& data,
  const byte_stream& original_iv,
  const std::function<byte_stream(const key<key_size>&, const byte_stream&, const iv&)>& encoder
) -> void {
  iv iv;
  key<key_size> key;
  if (iv.size() != original_iv.size()) {
    throw std::invalid_argument("iv length must be 128 bit ");
  }
  if (original_key.size() != key.size()) {
    throw std::invalid_argument("invalid key size");
  }
  std::copy(original_iv.begin(), original_iv.end(), iv.begin());
  std::copy(original_key.begin(), original_key.end(), key.begin());

  auto encoded_data = encoder(key, data, iv);
  for (const auto byte_value : encoded_data) {
    std::cout << byte_value;
  }
}

int
main(int argc, char* argv[]) {
  const auto program_path = argv[0];
  byte_stream iv(BLOCK_SIZE, 0);
  byte_stream key(128 / 8, 0);
  int opt;
  while ((opt = getopt(argc, argv, "hi:k:V")) != -1) {
    switch (opt) {
      case 'h':
        usage(program_path);
        return 0;
      case 'i':
        try {
          iv = convert_hex_string(optarg);
        } catch (const std::invalid_argument& error) {
          std::cerr << "invalid iv format" << std::endl;
          return 1;
        } catch (const std::out_of_range& error) {
          std::cerr << error.what() << std::endl;
          return 1;
        }
        if (iv.size() != BLOCK_SIZE) {
          std::cerr << "iv must be 128 bit" << std::endl;
        }
        break;
      case 'k':
        try {
          key = convert_hex_string(optarg);
        } catch (const std::invalid_argument& error) {
          std::cout << "invalid key format" << std::endl;
          return 1;
        } catch (const std::out_of_range& error) {
          std::cerr << error.what() << std::endl;
          return 1;
        }
        if (!(key.size() == 128 / 8 || key.size() == 192 / 8 || key.size() == 256 / 8)) {
          std::cerr << "key size must be 128, 192 or 256 bit" << std::endl;
        }
        break;
      default:
        return 1;
    }
  }

  if (argc - optind < 1) {
    std::cerr << "no subcommand" << std::endl;
    return 1;
  }
  const auto subcommand = std::string(argv[optind]);
  if (subcommand == "help") {
    usage(program_path);
    return 0;
  }

  byte_stream data;
  while (true) {
    auto byte_value = getc(stdin);
    if (byte_value == EOF) {
      break;
    }
    data.emplace_back(byte_value);
  }
  try {
    if (subcommand == "enc") {
      switch (key.size() * 8) {
        case 128:
          print_data_while_encoding<128>(key, data, iv, encrypt<128>);
          break;
        case 192:
          print_data_while_encoding<192>(key, data, iv, encrypt<192>);
          break;
        case 256:
          print_data_while_encoding<256>(key, data, iv, encrypt<256>);
          break;
        default:
          std::cout << "invalid key type" << std::endl;
          return 1;
      }
    } else if (subcommand == "dec") {
      switch (key.size() * 8) {
        case 128:
          print_data_while_encoding<128>(key, data, iv, decrypt<128>);
          break;
        case 192:
          print_data_while_encoding<192>(key, data, iv, decrypt<192>);
          break;
        case 256:
          print_data_while_encoding<256>(key, data, iv, decrypt<256>);
          break;
        default:
          std::cout << "invalid key type" << std::endl;
          return 1;
      }
    } else {
      std::cerr << "unknown command: " << subcommand << std::endl;
      return 1;
    }
  } catch (const char* error) {
    std::cerr << error << std::endl;
    return 1;
  }
}

ビルド

最後にビルドするが、ファイル名をmain.cppとして保存した上で次のようなコマンドでビルドできる。

$ g++ -std=c++17 -Wall -Wextra -pedantic -O2 -lcrypto -lssl main.cpp

OpenSSL 1.1ではlibssllibcryptoをリンクしなければならないが、OpenSSL 1.0では次のコマンドのようにlibsslだけリンクするようにしてビルドすれば良い。

$ g++ -std=c++17 -Wall -Wextra -pedantic -O2 -lssl main.cpp

もしlibsslをリンクできなければfind / -name 'libssl.so*'を、libcryptoをリンクできなければfind / -name 'libcrypto.so*'を実行し、次のように実行することでリンクできる。

# libsslがリンクできなければ`-L libssl.so*のファイルがあるディレクトリへのパス`をコマンドに追加する(`*`はワイルドカード)
$ g++ -std=c++17 -Wall -Wextra -pedantic -O2 -lcrypto -lssl main.cpp -L libssl.so*のファイルがあるディレクトリのパス
# libcryptoがリンクできなければ`-L libcrypto.so*のファイルがあるディレクトリへのパス`をコマンドに追加する
$ g++ -std=c++17 -Wall -Wextra -pedantic -O2 -lcrypto -lssl main.cpp -L libcrypto.so*のファイルがあるディレクトリのパス
# どちらもなければ`-L libssl.so*のファイルがあるディレクトリへのパス -L libcrypto.so*のファイルがあるディレクトリのパス`をコマンドに追加する
$ g++ -std=c++17 -Wall -Wextra -pedantic -O2 -lcrypto -lssl main.cpp -L libcrypto.so*のファイルがあるディレクトリのパス -L libssl.so*のファイルがあるディレクトリへのパス

こうしてできたバイナリを実行する際には先程追加したディレクトリへのパスをLD_LIBRARY_PATHに指定してやる必要がある(LD_LIBRARY_PATHは場合によっては優先度の高いディレクトリに差し替え用のライブラリを差し込まれて攻撃に利用される可能性があるため、十分に注意してとりあつかう)。

# libsslがリンクできなければこのコマンド
$ env LD_LIBRARY_PATH=libssl.so*のファイルのあるディレクトリへのパス:$LD_LIBRARY_PATH ビルドしたコマンド
# libcryptoがリンクできなければこのコマンド
$ env LD_LIBRARY_PATH=libcrypto.so*のファイルのあるディレクトリへのパス:$LD_LIBRARY_PATH ビルドしたコマンド
# どちらもない場合
$ env LD_LIBRARY_PATH=libssl.so*のファイルのあるディレクトリへのパス:libcrypto.so*のファイルのあるディレクトリへのパス:$LD_LIBRARY_PATH ビルドしたコマンド

もし、openssl/evp.hをインクルードできなければ、find / -name 'evp.h'を実行し、出力されたパスを元に-I openssl/evp.hのあるディレクトリのパスというオプションを追加してビルドする

g++ -std=c++17 -Wall -Wextra -pedantic -O2 -lcrypto -lssl main.cpp -I openssl/evp.hのあるディレクトリのパス

opensslコマンドによるテスト

最後に、実装した暗号化・復号化するコマンドの出力がopensslコマンドによって暗号化・復号化をできるかを試してみる。

まず暗号化。

$ bash
$ iv="$(head -c 16 /dev/urandom | od -x -A n | tr -d ' \n')"; key="$(head -c 32 /dev/urandom | od -x -A n | tr -d ' \n')"; head -c 1000 /dev/urandom | base64 | tr -d '\n' | xargs echo | tee /dev/stderr | ./a.out -i "$iv" -k "$key" enc | openssl aes-256-cbc -nosalt -d -iv "$iv" -K "$key" | xargs echo
27mAkzccTx6UudmFio9DOhzKnvJYzvKfZ01JhARg6SdGo7XpvRyCoYMg9r9by3Oa/fdHo3ORYlZmVbjFPPndalwyDotG2G77OyaFxsdtfyAgI2lFo3DhpN9tqCWVhyd1pFngjs8r+5LoYzxyTKzEl8gVdyLD8T7KXx9SfcHK1xYiTHQ7/OFcwB5/5TtUBhImTFOge9DT8DOQUapWVkYf6f5ObveroBygCB1r5WinWo8zdeXJVzbO43hakhDmZoUcdrWecStgzodNVIHXMx1GbS7fJA9i+jnHSd2SmUxiWjrAm0STlvLsXNlVsXG1In7ovqZihlNntheSMPBtR+RSkfRjr0v7Mxtn5WIwcSPBv1jZyKZ9V81fYDA7CFKTF9eZo8dsk7iGZmzAQL4gIrn5mbwzdFbTsSbleeZIW6j/FVB+iGAHunTgpBQio5VvsX/+d+xir14/4BXeYLSmH3VWPxgHo+d/yxV8QY/1vKEcKs99OsI8BVaXMYe5Pc+CeL5rsUqY5yDI5jPw2ww5iaiM75a0/Li7V5cJTSx0fTMyUzT6aj8A3NykOGmC4HxN3o/gvD6FU/aOE1NqlQrYZTYSyOKyncZH0cP/rP1lFsv6/fBsvoXSB3grcppoGnWKMWRMnMN/JaUlF/rFsJE8/b+5fABqQsW58X7x5oYnRbluwtE8iPepSbCRww+zJWujSftYn2QjRnc4zOGHVdJPMaStSGWeiJOyfS5F9ezr6b6Vn2zLPoY3xAC9b23evnHbGSmSnKLfkDbhjF6jpg+c/T8khpFf4ayNYcn4qnt1hBgqlDCnB3/GcDEoXdc0A8xAMRXg7Mk8tLTxoasx7eUSB3T9Oi4YKdk7ZGhGbecPb5CjNcDjBchRFJG1tfnInR596Y8IOVmxTKKBS0lZ0ImINGN1oE3HEewSXK3+JFeGsWw/Pv9hSESvSm/7IiZKI1piNJlkgMESkYq/y6Wy+cE887qfLmIXmXh0pmbJLuHM/QgIy2DivIJhOOnSWNFNOokcYZo1KqZ7HxZ28A64hhbZB00YxIh66bK/C1LgglOPvtr8xItadST5ENLwofezMhMdDlntju9EgJWM/ZKAsVaNxQY8BCI0IoouxnOGelt6AmbKWM3HHNn4n7t+WZIQQyphHcjdp4V+ZLPg526ikDja+eu8bAFtb5TW3WGP8x6jsDDvlfm6tRBg39BJWnWJtbfUYyRN4/JDGHpjPuKb3P7E3JjBx8IXXmvIqBaiNBnbC5zxLtSg4uFIqGKOeCithuaXA7zlj1KEaY4h1Yru9JHD5e3kAnniQzExlopNg7cbEEZEvlf0NAsdKxCfHg==
27mAkzccTx6UudmFio9DOhzKnvJYzvKfZ01JhARg6SdGo7XpvRyCoYMg9r9by3Oa/fdHo3ORYlZmVbjFPPndalwyDotG2G77OyaFxsdtfyAgI2lFo3DhpN9tqCWVhyd1pFngjs8r+5LoYzxyTKzEl8gVdyLD8T7KXx9SfcHK1xYiTHQ7/OFcwB5/5TtUBhImTFOge9DT8DOQUapWVkYf6f5ObveroBygCB1r5WinWo8zdeXJVzbO43hakhDmZoUcdrWecStgzodNVIHXMx1GbS7fJA9i+jnHSd2SmUxiWjrAm0STlvLsXNlVsXG1In7ovqZihlNntheSMPBtR+RSkfRjr0v7Mxtn5WIwcSPBv1jZyKZ9V81fYDA7CFKTF9eZo8dsk7iGZmzAQL4gIrn5mbwzdFbTsSbleeZIW6j/FVB+iGAHunTgpBQio5VvsX/+d+xir14/4BXeYLSmH3VWPxgHo+d/yxV8QY/1vKEcKs99OsI8BVaXMYe5Pc+CeL5rsUqY5yDI5jPw2ww5iaiM75a0/Li7V5cJTSx0fTMyUzT6aj8A3NykOGmC4HxN3o/gvD6FU/aOE1NqlQrYZTYSyOKyncZH0cP/rP1lFsv6/fBsvoXSB3grcppoGnWKMWRMnMN/JaUlF/rFsJE8/b+5fABqQsW58X7x5oYnRbluwtE8iPepSbCRww+zJWujSftYn2QjRnc4zOGHVdJPMaStSGWeiJOyfS5F9ezr6b6Vn2zLPoY3xAC9b23evnHbGSmSnKLfkDbhjF6jpg+c/T8khpFf4ayNYcn4qnt1hBgqlDCnB3/GcDEoXdc0A8xAMRXg7Mk8tLTxoasx7eUSB3T9Oi4YKdk7ZGhGbecPb5CjNcDjBchRFJG1tfnInR596Y8IOVmxTKKBS0lZ0ImINGN1oE3HEewSXK3+JFeGsWw/Pv9hSESvSm/7IiZKI1piNJlkgMESkYq/y6Wy+cE887qfLmIXmXh0pmbJLuHM/QgIy2DivIJhOOnSWNFNOokcYZo1KqZ7HxZ28A64hhbZB00YxIh66bK/C1LgglOPvtr8xItadST5ENLwofezMhMdDlntju9EgJWM/ZKAsVaNxQY8BCI0IoouxnOGelt6AmbKWM3HHNn4n7t+WZIQQyphHcjdp4V+ZLPg526ikDja+eu8bAFtb5TW3WGP8x6jsDDvlfm6tRBg39BJWnWJtbfUYyRN4/JDGHpjPuKb3P7E3JjBx8IXXmvIqBaiNBnbC5zxLtSg4uFIqGKOeCithuaXA7zlj1KEaY4h1Yru9JHD5e3kAnniQzExlopNg7cbEEZEvlf0NAsdKxCfHg==

次に復号化。

$ bash
$ iv="$(head -c 16 /dev/urandom | od -x -A n | tr -d ' \n')"; key="$(head -c 32 /dev/urandom | od -x -A n | tr -d ' \n')"; head -c 1000 /dev/urandom | base64 | tr -d '\n' | xargs echo | tee /dev/stderr | openssl aes-256-cbc -nosalt -e -iv "$iv" -K "$key" | ./a.out -i "$iv" -k "$key" dec | xargs echo
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

あっていそうだ。

スクリプトでやってることは簡単で

  1. /dev/urandomから16 byte(128 bit)とってhex stringにしてIVにする
  2. /dev/urandomから32 byte(256 bit)とってhex stringにして鍵にする
  3. /dev/urandomから1000バイトとってbase64エンコードしたものを表示しつつ、そのbase64エンコードしたものを
    • 作成した暗号化・復号化コマンドで暗号化してopensslコマンドで復号化して表示
    • opensslコマンドで暗号化して作成した暗号化・復号化コマンドで復号化して表示

GoコードをバイナリにしてYoctoに入れる(ARM)

GoはARMバイナリを直接吐ける。 なので、Goのコンパイラをバイナリで引っ張ってビルド時に走らせればARMバイナリを簡単にビルドしてYoctoへ組み込める。

やるにはレシピに次ようなパッケージを追加して

LICENSE = "CLOSED"

SRC_URI_append = " \
  https://dl.google.com/go/go1.13.3.linux-amd64.tar.gz;name=go \
  file://main.go \
"

# go 1.13.3(linux x64版)のチェックサム
SRC_URI[go.md5sum] = "e0b36adf4dbb7fa53b477df5d7b1dd8c"
SRC_URI[go.sha256sum] = "0804bf02020dceaa8a7d7275ee79f7a142f1996bfd0c39216ccb405f93f994c0"

# そのままバイナリをイメージに入れようとすると
# 出来上がったバイナリをstripしようとしてイメージの
# ビルドに失敗するので、stripしないように設定する
INHIBIT_PACKAGE_STRIP = "1"
INHIBIT_PACKAGE_DEBUG_SPLIT = "1"

export PROG_PATH="${WORKDIR}/${PN}"

do_compile() {
  # ターゲットOSはlinuxに
  export GOOS=linux
  # ターゲットのアーキテクチャをARMに
  export GOARCH=arm
  # ここでは命令セットはarmv7にしている
  # 他の命令セットは次を参照のこと
  #   ref. https://github.com/golang/go/wiki/GoArm
  export GOARM=7

  # goコンパイラは${WORKDIR}/go/binにある
  ${WORKDIR}/go/bin/go build -o ${PROG_PATH} ${WORKDIR}/main.go
}

do_install() {
  install -d ${D}/usr/local/bin
  install -m 755 ${PROG_PATH} ${D}/usr/local/bin/.
}

FILES_${PN} = " \
  /usr/local/bin/${PN} \
"

あとはIMAGE_INSTALLにパッケージを追加してlayerにレシピを追加すればGoで吐いたバイナリーをイメージに組込むことができる

I2C Toolsの簡単な使い方について

I2C Toolsには

  • i2cget
  • i2cset
  • i2cdetect
  • i2ctransfer
  • i2cdump

というコマンドが同梱されているが、この内

  • i2cget
  • i2cset
  • i2cdetect

についてコマンドの簡単な使い方を記しておく。

i2cdetect

i2cdetectの文法は次の通り:

i2cdetect -V
i2cdetect -l
i2cdetect -F i2cbus
i2cdetect [-y] [-a] [-q|-r] i2cbus [first last]

i2cdetect -Vはバージョン情報を表示して終了する。

i2cdetect -lはI2Cのバスを列挙してくれる。

$ i2cdetect -l 
i2c-1   i2c         21a4000.i2c                         I2C adapter
i2c-2   i2c         21a8000.i2c                         I2C adapter
i2c-0   i2c         21a0000.i2c                         I2C adapter

この表示されるi2c-1i2c-2などのi2c-xxの部分が接続されているI2Cバスの識別子となっている。 後のコマンドでi2cbusで指定すべきものはこのI2Cバスの識別子となっている。

i2cdetect -F i2cbusi2cbusに指定されたI2Cバスが行える機能についての情報を表示してくれる

$ i2cdetect -F 1
I2C                              yes
SMBus Quick Command              yes
SMBus Send Byte                  yes
SMBus Receive Byte               yes
SMBus Write Byte                 yes
SMBus Read Byte                  yes
SMBus Write Word                 yes
SMBus Read Word                  yes
SMBus Process Call               yes
SMBus Block Write                yes
SMBus Block Read                 yes
SMBus Block Process Call         no
SMBus PEC                        yes
I2C Block Write                  yes
I2C Block Read                   yes

i2cdetect [-y] [-a] [-q|-r] i2cbus [first last]は指定したi2cbusに対して(firstlastが0x18などの16進数アドレスで指定されていればfirstからlastまで)Write要求を発行して、反応があったスレーブアドレスを表示する。

-yは対話形式にしないようにするためのオプションで、-aは特別な理由がない限り使わないので無視してよい。

-qはSMBusのQuick Command機能を利用してWriteするように試みているが、必要でなければ使わないほうがよい。

-rはSMBusのRead Byte機能を利用してWrite要求ではなくRead要求でスキャンを試みるているが、Write onlyなデバイスに対してRead要求を行うとロックされて反応が返ってこなくなる可能性があるため、必要にならない限りは使わない方が良い。

したがって基本的な使い方は

$ i2cdetect -y 1

となる。これを実行すると次のような結果が返ってくる

     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- UU -- UU -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- 4a -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --

4bitを境として、一番左の列はアドレスの上位桁(0x18なら0x10)を表しており、一番上の行はアドレスの下位4桁(0xf8なら0x0x8)を表している。 表中に表示されている4aは反応のあったアドレスを表しており、上位桁と下位桁を併せた表記がなされている。 逆に--は反応がなかったことを表している。 また、UUは反応があったが、ドライバなどによって使用されていることを表している。

i2cget

i2cgetの文法は次の通り:

i2cget -V
i2cget [-f] [-y] i2cbus chip-address [data-address [mode]]

-Vはバージョン情報を表示して終了するだけなので、2つ目の文法についてここでは説明する。

i2cbus/sys/bus/i2c/devicesの下にあるi2c-x(xは数字)というディレクトリのx部分か、i2cdetect -lを実行して表示されるi2c-xxを指定する。 たとえば、自分の環境であれば、

$ ls /sys/bus/i2c/devices
i2c-0  i2c-1  i2c-2  i2c-3  i2c-4  i2c-5  i2c-6

があったので、i2cbus1から6までを指定することができる。

-fは強制的に実行する際に用いる。 例えばi2cdetectUUと表示されたアドレスに対して強制的にそのアドレスにあるスレーブデバイスから強制的に値を読みたい場合などに利用される。

-yi2cdetectと同じで対話モードを無視するためのオプションとなっている。

chip-addressにはi2cdetectで調べたときに反応があったスレーブアドレスを指定する。 例えば

$ i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- UU -- UU -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- 4a -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --

となっている場合には0x4aを指定したりする。

data-addressは利用できるスレーブアドレス下にあるデバイスに対してRead/Writeを行いたいアドレスを指定する。たとえば、WHO_AM_Iレジスタ(0x0f)を読みたい場合は次のようにする

$ i2cdetect -y 1 0x4a 0x0f

最後のmodeには

  • b: 1バイト読み込む
  • w: 1ワード(16bit)読み込む
  • c: 1ワード書き込んで読み込む

を指定でき、それぞれにプリフィクスとしてpをつけることができる(例: cp)。 pはPEC送信を有効にする場合につける。

i2cset

i2csetの文法は次の通り:

i2cset -V
i2cset [-f] [-y] [-m mask] [-r] i2cbus chip-address data-address [value] ... [mode]

i2cset -Vは他と同様バージョン情報を表示して終了し。-f-yオプションはi2cget-f-yと同じものとなっている。 また、i2cbuschip-addressdata-addressi2cgetと意味は同じため割愛する。

i2csetレジスターに値をセットするために使用されるコマンドだが、使用する値はvalueを指定することで実現する。 ただし、複数のバイトを指定したいときにはmodesiを指定する必要がある。

-m maskにはマスク値を指定することができる。 このオプションを指定すると、valueに指定されている値全てに指定したmask値がマスクされるようになっている。

最後のmode

  • b: 1バイト書き込む
  • w: 1ワード(16bit)書き込む
  • s: SMBusブロックモードによる書き込み
  • i: I2Cブロックモードによる書き込み

を指定することができ、iを除く3つにはpをプリフィクスとして付けることができる(例: cp)。 pはPEC送信を有効にする際に利用する。

i3で通知を出す

dunstという通知を行うためのアプリケーションがあるので、それを利用する。 Arch Linuxなら公式リポジトリから提供されているので、pacmanで入る

$ sudo pacman -S dunst

設定はArch Linuxならば/usr/share/dunst/dunstrcにあるので、これを$XDG_CONFIG_HOME/dunstにコピーすればOK(おそらく何もしていなければ$XDG_CONFIG_HOME = ~/.config)

f:id:moba1:20190808103707p:plain
dunstの動作デモ

もし、メールの通知を入れたいのであれば、mailnagもインストールするとできる。

$ sudo pacman -S mailnag

参考文献

wiki.archlinux.jp

meta-qtにおけるQtWebEngineで音を出す方法

概要

ALSAが既にレイヤーに追加されていても、 QtWebEngine側にWebRTCの設定を入れる必要があるため、そのままgetUserMediaなどでメディアを取得することはできない。 ここでは、その追加手順について説明する。

手順

まず、レシピファイルを追加する(ここでは、自分のレイヤー名をmeta-hogeとしておく)。 ディレクトリの構成はmeta-qt5ではqtwebengineのレシピはrecipies-qt/qt5の下にあるので、それに合わせる。

$ mkdir -p meta-hoge/recipies-qt/qt5
$ touch meta-hoge/recipies-qt/qt5/qtwebengine_git.bbappend

meta-hoge/recipies-qt/qt5/qtwebengine_git.bbappendの中には次のように書く。

PKGCONFIG += "webrtc"

参考資料

forum.qt.io