C++ コンパイル時パスワード認証　〜コードを不正コンパイルから守ろう！〜

---

ラクラムシ「今回は C++ に関する内容ですね！」

謎の女性「どういった内容ですか？」

ラクラムシ「 C++ でコンパイル時にパスワードを要求する方法について説明していきます！」

謎の女性「よろしくお願いします！」

---

この記事では、C++ でコンパイル時にパスワードを要求する方法について説明します（ UNIX 系の環境に限ります）。


# コンパイル時パスワード認証

みなさんは、プログラミングをするときにソースコードを公開していますか？
ソースコードを公開することで、ほかの人にバグを指摘してもらったり、新機能を提案してもらったりすることができます。

ソースコードを共有できるサイトはいろいろありますが、代表的なものに GitHub があります。
GitHub は無料で利用できるので、ソースコードを共有してみたいという人は使ってみましょう！

[<img width="300" alt="GitHub" title="GitHub" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643544/b8a95f7e-3a56-1b34-ac71-075aa2d2c98a.png">](https://github.com/)

しかし、「コードを見られるのはいいけど、勝手にコンパイルされたくない！[^omae]」というときはどうすればいいのでしょうか？

[^omae]: *おまえは何を言っているんだ*

そんなときは、**コンパイル時パスワード認証**です！

ソースコードに以下のようなコードを挿入すると、正しいパスワードが入力されないとコンパイルできない（エラーが出る）ようになります。

```c++:main.cpp
// ====================
// ソースコードを不正コンパイルから守る！
// ====================

#include <string_view>

// プロンプト
#warning パスワードを入力してください。

// パスワード入力
constexpr std::string_view password =
#include "/dev/stdin"
;

// パスワードチェック
static_assert(password == "KW!yaQ)5j_esE$CH6$Q+RvVu*(XQmWIXh8D.", "パスワードが違います！！");



// ====================
// ソースコード
// ====================
#include <iostream>
int main() {
    std::cout << "Hello, World!\n";
}
```

このソースコードのパスワードは `"KW!yaQ)5j_esE$CH6$Q+RvVu*(XQmWIXh8D."` に設定されています。
これをコンパイルすると、次のようになります。

```shell-session
$ g++ -std=c++17 main.cpp
main.cpp:8:2: warning: #warning パスワードを入力してください。 [-Wcpp]
    8 | #warning パスワードを入力してください。
      |  ^~~~~~~
"KW!yaQ)5j_esE$CH6$Q+RvVu*(XQmWIXh8D."
(Ctrl+D)
$ ./a.out
Hello, World!
```

コンパイルすると「パスワードを入力してください。」とメッセージが表示され、入力待ちの状態になります。
ここでパスワードを入力して <kbd>`Ctrl`</kbd> + <kbd>`D`</kbd> で閉じると、実行可能ファイルが出力されました。

ではここで、間違ったパスワードを入力するとどうなるでしょうか？

```shell-session
$ g++ -std=c++17 main.cpp
main.cpp:8:2: warning: #warning パスワードを入力してください。 [-Wcpp]
    8 | #warning パスワードを入力してください。
      |  ^~~~~~~
"password"
(Ctrl+D)
main.cpp:16:24: error: static assertion failed: パスワードが違います！！
   16 | static_assert(password == "KW!yaQ)5j_esE$CH6$Q+RvVu*(XQmWIXh8D.", "パスワードが違います！！");
      |               ~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
```

「パスワードが違います！！」とエラーが出てコンパイルに失敗しました。
しっかりとソースコードが保護されていますね！


# コンパイル時パスワード認証の仕組み

まずはコンパイル時にどうやって入力を受け取るかですが、これにはよく知られた<sup>[要出展]</sup>方法があって、 `#include "/dev/stdin"` で標準入力の内容がソースコードに埋め込まれることを利用します[^includestdin]。
標準入力に入力された文字列リテラルを constexpr 変数で受け取ることで、パスワードを確認します。
`std::string_view` の比較演算子は constexpr であるので、 `static_assert` によってパスワードが正しくないときにエラーを出すことができます。

[^includestdin]: 正確には入力はコンパイルより前のプリプロセス時に読み込まれます。


# ハッシュ関数

これでパスワードを知らないとコンパイルできないようになったと思いましたが、なんと**ソースコードにパスワードが含まれています**。
これではコードを読めば誰でもパスワードがわかってコンパイルできますね！

そこで、パスワードそのものを使わずにパスワードをチェックするために[**ハッシュ関数**](https://ja.wikipedia.org/wiki/%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E9%96%A2%E6%95%B0)を使います！

ハッシュ関数は任意のデータから別の値（ハッシュ値）を得る操作で、非可逆性変換であるためハッシュ値から元のデータを得ることが難しく、安全なパスワードチェックに使えます。

ハッシュ関数のアルゴリズムには様々なものがありますが、ここでは軽量な [FNV-1a](https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function) という方法を使って実装してみます。

```c++:main.cpp
// ====================
// ソースコードを不正コンパイルから守る！
// ====================

#include <string_view>
#include <cstdint>

// ハッシュ関数 (FNV-1a)
constexpr std::uint64_t constexpr_hash(std::string_view s) {
    std::uint64_t value = UINT64_C(14695981039346656037);
    for (auto&& c : s) {
        value ^= static_cast<std::uint64_t>(c);
        value *= UINT64_C(1099511628211);
    }
    return value;
}

// プロンプト
#warning パスワードを入力してください。

// パスワード入力
constexpr std::string_view password =
#include "/dev/stdin"
;

// パスワードチェック
static_assert(constexpr_hash(password) == UINT64_C(5032771906826054373), "パスワードが違います！！");



// ====================
// ソースコード
// ====================
#include <iostream>
int main() {
    std::cout << "Hello, World!\n";
}
```

これで同様にパスワードを要求することができますが、ソースコードからパスワードがバレません。


# ソースコードを変更不可に

これで完璧になったかと思いましたが、まだ問題が残されています。
今の状態では、ソースコードを書き換えて**パスワードチェックの部分を削除する**だけで**簡単に突破**されてしまいます。

これではパスワードの意味がありません。
どうにかしてソースコードを変更できないようにする必要があります。

そこで役に立つのが**ライセンス**です。

ライセンス（ソフトウェアライセンス）とは、ソフトウェアの使用、改変、再配布、商用利用などの可否や条件を定めたもので、利用者はライセンスに違反しない範囲で使用しなければなりません。

つまり、**ソースコードに改変禁止のライセンスをつけてしまえばパスワード認証のコードが削除されることはありません**！

ライセンスは自分で作ることもできますが、ここでは広く使われている[クリエイティブ・コモンズ・ライセンス](https://creativecommons.jp/licenses/)によって改変禁止を示してみましょう。
**CC BY-ND** (Creative Commons Attribution-NoDerivs) を表示すると、利用者に**クレジットの表示**および**改変不可**を要求することができます。

```c++:main.cpp
// Copyright (c) 2023 Raclamusi
// This source code is licensed under CC BY-ND, see https://creativecommons.org/licenses/by-nd/4.0/ .

// ====================
// ソースコードを不正コンパイルから守る！
// ====================

#include <string_view>
#include <cstdint>

// ハッシュ関数 (FNV-1a)
constexpr std::uint64_t constexpr_hash(std::string_view s) {
    std::uint64_t value = UINT64_C(14695981039346656037);
    for (auto&& c : s) {
        value ^= static_cast<std::uint64_t>(c);
        value *= UINT64_C(1099511628211);
    }
    return value;
}

// プロンプト
#warning パスワードを入力してください。

// パスワード入力
constexpr std::string_view password =
#include "/dev/stdin"
;

// パスワードチェック
static_assert(constexpr_hash(password) == UINT64_C(5032771906826054373), "パスワードが違います！！");



// ====================
// ソースコード
// ====================
#include <iostream>
int main() {
    std::cout << "Hello, World!\n";
}
```

これでこのソースコードは自分以外変更することができません。


# 脆弱性

実はこのパスワード認証には致命的な脆弱性があります。

それは、 **C++ のコードを入力でき、それがそのままコンパイルされること**です。

`#include` を使って入力を得ているため、文字列リテラルに限らずあらゆる C++ のコードが入力可能です[^embed]。
これを利用すると、パスワードを知らずとも、ソースコードを改変することなくパスワードチェックを突破して、コンパイルすることができてしまいます。

[^embed]: C23 で追加された `#embed` を使うと安全な入力が可能です。 C++ にも早く来い。

```shell-session
$ g++ -std=c++17 main.cpp
main.cpp:22:2: warning: #warning パスワードを入力してください。 [-Wcpp]
   22 | #warning パスワードを入力してください。
      |  ^~~~~~~
""
#define static_assert(...)
(Ctrl+D)
$ ./a.out
Hello, World!
```


# おわりに

***いかがでしたか？***



# この記事にあるソースコードについて

この記事に記載したソースコードには CC BY-ND が表示されているものがありますが、これは改変禁止にする方法を紹介するためのものであり、この記事のソースコードは改変も含めて自由に使用していただいて構いません。