生涯未熟

生涯未熟

プログラミングをちょこちょこと。

刺し身たんぽぽについて

刺し身たんぽぽについて調べてみました。

f:id:syossan:20190106180834p:plain

刺し身たんぽぽとは一体何なのか?

刺身の盛り合わせに添えられている食菊(食用菊)の通称。花の大きさからタンポポと誤認されやすい。菊には殺菌作用があり、かつ見た目の彩りや香りも添える。さらに刺身のツマとして食べられる。近年では形骸化しつつある。 - weblio辞書より引用 -

なるほど、実際にはたんぽぽでは無いのですね。勉強になります。
あと、食べれるものとは知りませんでした。むしろ、あんな花花しいものを食べる人はいるのだろうか?

いつから刺し身たんぽぽは誕生したのか?

奈良時代に、日本で現在でも食用菊として栽培されている「延命楽(もってのほか・カキノモト)」が中国から伝来した[2]。 - wikipediaより引用 -

そんな1000年以上前から刺し身たんぽぽはあったのですね・・・しゅごい。

食用としては、江戸時代から民間で食されるようになったとされており[4]、1695年に記された『本朝食鑑』に「甘菊」の記述が見られる[5]。 - wikipediaより引用 -

実際に食されるようになったのは結構最近なのですね。
刺し身のツマとして用いられるようになった経緯についても深掘りしたかったのですが、検索しても見つからず。インターネットの敗北です。

刺し身たんぽぽの効用とは?

刺し身にたんぽぽが入っている理由としては「殺菌作用・彩り」の2点のようですね。
このたんぽぽ自体には発ガン効果の抑制・コレステロールの低下・中性脂肪を低下させる効果があるそうです。めちゃくちゃ有能やんけ。

刺し身たんぽぽの危険性

ただ、こんな刺し身たんぽぽですがキク科のアレルギーを持つ方にとっては食すのに危険性が伴います。

togetter.com

食べる時には自分がアレルギーを持っていないか気を付けましょう。
また、刺し身たんぽぽという単語を真に受けて、その辺に生えてるたんぽぽを生で食べないようにしましょう。普通に汚いですからね。

刺し身たんぽぽについて完全に理解した

年始から刺し身たんぽぽの話題で溢れていたので、完全に理解するために色々と調べてみましたがどうだったでしょうか?
こういう単語はしっかり調べた上で、きちんと使える大人になりましょうね!

treeコマンド 〜Rオプションの謎〜

treeコマンド、皆さん知ってますか?
そうです、あのファイルやらディレクトリやらを良い感じに木構造で出力してくれるニクい奴です。

f:id:syossan:20190103044420p:plain

こんな感じ

さて、そんなtreeコマンドには多くのオプションがあります。
よく使われるのは -L だったり、 -a とかですかね?

そんな中、 -R オプションというものが存在するのをご存知でしょうか?

Rオプションとは何なのか?

まずは、 --help オプションに書かれている内容を見てみましょう。

-R            Rerun tree when max dir level reached.

直訳すると 最大ディレクトリレベルに達したらツリーを再実行します。 ってとこですかね。
あー、再帰的にツリーを辿るのねと試しに以下のようなコマンドを実行してみました。

$ tree -L 1 -R -o result

これで1階層ごとにtreeの結果が出力されるじゃね?と思ってみました。(あまりやっても意味ないですが・・・
で、やってみた結果、ワーキングディレクトリの下にしかresultが吐かれませんでした・・・

さてはて、予想が外れたのですが冷静に次はmanコマンドを参照してみます。

-R     Recursively cross down the tree each level directories (see -L option), and at each of them execute tree again adding `-o 00Tree.html' as a new option.

これを直訳すると

再帰的に各レベルのディレクトリにツリーをたどります(-Lを参照)
そして、それらのそれぞれで、新しいオプションとして '-o 00Tree.html'を追加して、それぞれtreeを実行します。

オッ、 --help の時と違って新たな -o 00Tree.html という概念が出てきましたね。
ただ、先の実行結果を見渡してみても 00Tree.html とやらが吐き出されている様子は無い模様。
ますます頭にはてなマークが生えてきました。

treeコマンドのソースコードを読む

このままでは埒が明かないので、treeコマンドのソースコードを読んじゃいましょう。
C言語で書かれているので、そこまで難しくはないはずです。

ソースコードは以下のリンク先からよしなに取得してきてください。
The Tree Command for Linux Homepage

さて、それでは読んでいきたいと思います。

まず、何はともあれ -R オプションを追っかけましょう。

case 'R':
  Rflag = TRUE;
  break;

-R オプションはこのようにコード内では Rflag という変数に置き換えられます。
これを追いかけようと、tree.cの中を見てみたのですが、まさかの以下のコードしか参照しているところがありませんでした。

if (Rflag && (Level == -1))
  Rflag = FALSE;

んん?意味自体は分かりますが、これが再帰処理に繋がる気配は無さそうですね・・・
一応他のコードも読んでみたのですが、これまたまさかの html.c というファイル内に以下の意味ありげなコードが書かれていました。

/* This is really hackish and should be done over. */
if (Rflag && (lev == Level) && (*dir)->isdir) {
  if (nolinks) fprintf(outfile,"%s",(*dir)->name);
  else {
    fprintf(outfile,"<a href=\"%s",host);
    url_encode(outfile,d+1);
    putc('/',outfile);
    url_encode(outfile,(*dir)->name);
    fprintf(outfile,"/00Tree.html\">");
    html_encode(outfile,(*dir)->name);
    fprintf(outfile,"</a><br>\n");
  }

  hdir = gnu_getcwd();
  if (sizeof(char) * (strlen(hdir)+strlen(d)+strlen((*dir)->name)+2) > pathsize)
    path = xrealloc(path, pathsize = sizeof(char) * (strlen(hdir)+strlen(d)+strlen((*dir)->name) + 1024));

  sprintf(path,"%s%s/%s",hdir,d+1,(*dir)->name);
  fprintf(stderr,"Entering directory %s\n",path);

  hcmd = xmalloc(sizeof(char) * (49 + strlen(host) + strlen(d) + strlen((*dir)->name)) + 10 + (2*strlen(path)));
  sprintf(hcmd,"tree -n -H \"%s%s/%s\" -L %d -R -o \"%s/00Tree.html\" \"%s\"\n", host,d+1,(*dir)->name,Level+1,path,path);
  system(hcmd);
  free(hdir);
  free(hcmd);
}

うーーーーーん、This is really hackish and should be done over.というコメントから察するに、なんか難易度高そうっすね・・・
一応この処理は -H オプションを付けて実行した際のHTMLを構築する処理の一部になります。

HTML構築のためにディレクトリを -L で指定した深さまで辿っている途中に、この処理が出てくるのですが、ざっくり読み解いてみると、どうやらRオプションがあり、かつLオプションで指定した深さのディレクトリの場合にHTMLを出力しつつ、再帰的に tree コマンドを実行していますね。
manの説明にあった、 00Tree.html の存在も伺えます。どうやら再帰的に実行する際に、各ディレクトリに 00Tree.html を生成しているようです。

というわけで、なんとなく分かったこととして -R オプションは -H オプションと共に使わないと意味が無さそうってことですね。
では実験してみましょう。

実験

それでは -H オプションを付加してやってみましょう。

$ tree -L 1 -HR .

<!DOCTYPE html>
<html>
<head>
 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
 <meta name="Author" content="Made by 'tree'">
 <meta name="GENERATOR" content="$Version: $ tree v1.8.0 (c) 1996 - 2018 by Steve Baker, Thomas Moore, Francesc Rocher, Florian Sesser, Kyosuke Tokoro $">
 <title>Directory Tree</title>
 <style type="text/css">
  <!--
  BODY { font-family : ariel, monospace, sans-serif; }
  P { font-weight: normal; font-family : ariel, monospace, sans-serif; color: black; background-color: transparent;}
  B { font-weight: normal; color: black; background-color: transparent;}
  A:visited { font-weight : normal; text-decoration : none; background-color : transparent; margin : 0px 0px 0px 0px; padding : 0px 0px 0px 0px; display: inline; }
  A:link    { font-weight : normal; text-decoration : none; margin : 0px 0px 0px 0px; padding : 0px 0px 0px 0px; display: inline; }
  A:hover   { color : #000000; font-weight : normal; text-decoration : underline; background-color : yellow; margin : 0px 0px 0px 0px; padding : 0px 0px 0px 0px; display: inline; }
  A:active  { color : #000000; font-weight: normal; background-color : transparent; margin : 0px 0px 0px 0px; padding : 0px 0px 0px 0px; display: inline; }
  .VERSION { font-size: small; font-family : arial, sans-serif; }
  .NORM  { color: black;  background-color: transparent;}
  .FIFO  { color: purple; background-color: transparent;}
  .CHAR  { color: yellow; background-color: transparent;}
  .DIR   { color: blue;   background-color: transparent;}
  .BLOCK { color: yellow; background-color: transparent;}
  .LINK  { color: aqua;   background-color: transparent;}
  .SOCK  { color: fuchsia;background-color: transparent;}
  .EXEC  { color: green;  background-color: transparent;}
  -->
 </style>
</head>
<body>
    <h1>Directory Tree</h1><p>
    <a href=".">.</a><br>
    ├── <a href="./LICENSE">LICENSE</a><br>
    ├── <a href="./Makefile">Makefile</a><br>
    ├── <a href="./README.md">README.md</a><br>
    ├── <a href="./cmd/00Tree.html">cmd</a><br>
Entering directory /Users/syossan27/go/src/go-tree/cmd
Entering directory /Users/syossan27/go/src/go-tree/cmd/go-tree
<br>
    ├── <a href="./dist/00Tree.html">dist</a><br>
Entering directory /Users/syossan27/go/src/go-tree/dist
<br>
    ├── <a href="./go.mod">go.mod</a><br>
    ├── <a href="./go.sum">go.sum</a><br>
    ├── <a href="./tree.go">tree.go</a><br>
    ├── <a href="./tree.html">tree.html</a><br>
    ├── <a href="./tree_windows.go">tree_windows.go</a><br>
    └── <a href="./validate.go">validate.go</a><br>
    <br><br>
    </p>
    <p>

2 directories, 9 files
    <br><br>
    </p>
    <hr>
    <p class="VERSION">
         tree v1.8.0 © 1996 - 2018 by Steve Baker and Thomas Moore <br>
         HTML output hacked and copyleft © 1998 by Francesc Rocher <br>
         JSON output hacked and copyleft © 2014 by Florian Sesser <br>
         Charsets / OS/2 support © 2001 by Kyosuke Tokoro
    </p>
</body>
</html>

おー、HTMLは無事出力されましたね。
この状態で、 00Tree.html とやらは各ディレクトリに出力されているのか確認します。

$ tree
.
├── LICENSE
├── Makefile
├── README.md
├── cmd
│   ├── 00Tree.html
│   └── go-tree
│       ├── 00Tree.html
│       └── main.go
├── dist
│   ├── 00Tree.html
│   ├── go_tree_darwin_386
│   ├── go_tree_darwin_amd64
│   ├── go_tree_freebsd_386
│   ├── go_tree_freebsd_amd64
│   ├── go_tree_freebsd_arm
│   ├── go_tree_linux_386
│   ├── go_tree_linux_amd64
│   ├── go_tree_linux_arm
│   ├── go_tree_netbsd_386
│   ├── go_tree_netbsd_amd64
│   ├── go_tree_netbsd_arm
│   ├── go_tree_openbsd_386
│   ├── go_tree_openbsd_amd64
│   ├── go_tree_windows_386.exe
│   └── go_tree_windows_amd64.exe
├── go.mod
├── go.sum
├── tree.go
├── tree.html
├── tree_windows.go
└── validate.go

オッ、たしかに出てますね。

で、出力されたHTMLを見てみると以下のような感じになります。

f:id:syossan:20190103053127p:plain

ほほー?一応cmdディレクトリのリンクを踏んでみましょうか。

f:id:syossan:20190103053158p:plain

なるほど、リンク先もtree構造のHTMLになってますね。
これが -R オプション無しだと、リンク先を踏んだ場合にはこんな感じになります。

f:id:syossan:20190103053251p:plain

普通にブラウザでローカルディレクトリを覗いた時の画面ですね。
という形の結果になりました。

まとめ

結局のところ -R オプションってなんぞ?というところなのですが、これはtreeコマンドの開発者が単に内部的に00Tree.htmlを吐く機構が欲しかったから作ったのでは・・・?という感想があります。
CHANGELOGとかも遡って見たり、インターネット上を彷徨ったりしたのですが特に有力な情報は見つからず・・・

あと、ちょっと気になるところとして 00Tree.html の親ディレクトリへのリンク先を押しても戻ることが出来ず、リンク先の出力ロジックがなんか間違えてるんじゃないかと。
こういった時のバグの報告とかどうやればいいのかちょっと謎なので、treeコマンドに詳しい方、情報お待ちしております。

2018年の棚卸し

2018年もそろそろ終わりですね。

恒例の本年の棚卸しをやっていきたいと思います。

1月

syossan.hateblo.jp

今年の抱負は「つよく いきる」でした。
つよく いきました、よかったですね。

syossan.hateblo.jp

homebrewに自作ツール登録しようとして失敗したりしました。
来年は登録基準を満たすレベルのリポジトリを生成したいですね。

2月

syossan.hateblo.jp

TebataというLinuxシグナルをハンドリングするGoライブラリ作ったりしました。
ちょこちょこと使って頂いてるみたいで、

github.com

そこそこ大きいライブラリでも使って頂いたり、

github.com

何故かワームプログラムに使われたりしてました。
悪いことはやめようね!

3月

syossan.hateblo.jp

なんかバズりました。
けど結局Twitterをまだやってます。付き合い方は変わったと思うので、この時の経験は活きたのかな?と思います。
一旦距離を置くというのは大事ですね。

4月

syossan.hateblo.jp

Goの勉強会を開催し始めました。
僕の中でgolang.tokyoや、GoConでの登壇って皆様発表内容が高レベルでハードル高いなーと思っていたので、なんかゆるっと登壇できる勉強会を作りたかったのです。
来年も細々と続けていきたいです。

5月

syossan.hateblo.jp

クソ雑魚ナメクジエンジニアはこんなくらいのスカウトが来るよって事例を書いてみたりしました。
直近の転職ドラフト結果もまた書きたいですね。

syossan.hateblo.jp

主催の勉強会でGoの並行処理についての発表しました。
英語苦手なりに原書を頑張って読んだら、この5か月後に和訳版が発売されました。泣きたい。
というのは冗談で、めちゃくちゃ良い本なのでGopherは是非読みましょう。

6月

syossan.hateblo.jp

Cassandra周りをガリガリやってた時に、あまり日本語情報が無い現象にぶち当たったりしました。

syossan.hateblo.jp

Kafkaもガリガリとやってたので、ライブラリの挙動にハマったりしましたねぇ。
Kafkaはもっと極めていきたい。

7月

syossan.hateblo.jp

ランサーズさんの開発ランチにお邪魔したりもしました。

syossan.hateblo.jp

Go猿本読んだりもしました。
来年もGoの良本出ないかな〜。

gounconference.connpass.com

勉強会3回目をやったり。

8月

syossan.hateblo.jp

書籍のレビューをやらせて頂いたりもしました。
貴重な経験ができました。

syossan.hateblo.jp

あと、Sparkが分からなすぎてキレたりしてました。

9月

syossan.hateblo.jp

この頃からElmの教祖に熱心な勧誘を受け、Elm教に入信致しました。
今ではフロントはElm以外で書く気がしません。

syossan.hateblo.jp

buildersconに初参加もしました。
jollyjoester・・・一体何者だったんだ・・・

10月

gounconference.connpass.com

4回目やりました。

そして30になりました。
30代が不安過ぎてつらい。

11月

syossan.hateblo.jp

これ、今年めちゃくちゃ嬉しかったです。
ありがとうJetBrains、来年もいっぱいお布施します。

12月

アドカレ5本仕込みました。
あまりウケがよろしくなかったので、来年はもっとウケるようなネタを書きたいですね。

まとめ

というわけで、30になってしまいました。
こんなクソ雑魚ナメクジエンジニアなのになんの成果もなく30になってしまい、将来が不安です。
とりあえず来年もがむしゃらにエンジニアリングをがんばっていきたいと思います。

GoでSSH Managerを作成した際の知見

この記事は Go Advent Calendar 2018 の23日目の記事です。

今回は過去に作成したSSH Managerのお話をさせて頂きます。

github.com

どんなツールなのか?

ざっくり言うとSSH接続情報を管理するツールになります。
今の所パスワード認証のみ対応しておりますが、ゆくゆくは公開鍵認証にも対応していく予定です。

簡単に使い方ですが、まずはSSH接続先設定を追加し、

f:id:syossan:20181222025351p:plain

以下のように設定したSSH設定の名前を指定して、実行することでSSH接続が可能になります。

f:id:syossan:20181222024657p:plain

何故作ったか?

所属している会社で運用しているサーバが、パスワード認証で管理されており、毎回user/passwordを入力するのが面倒だなと思ったのがキッカケでした。
パスワード認証の場合、 .ssh/config での接続管理も出来ないので、だったらサクッとGoでSSH Managerを作ってしまおうと思い立ったのです。

類似のツールがあれば良かったのですが、パスワード認証に対応したツールは見つからず・・・

実装

まずは、実装したいものを大きく3つのステップに分けてみました。

  • 接続名、接続先、ユーザー、パスワードの追加・更新・削除、及びAES暗号化
  • パスワード認証によるSSH接続
  • bash-completionの対応

それではひとつずつ実装を追って行きたいと思います。

接続名、接続先、ユーザー、パスワードの追加・更新・削除、及びAES暗号化

実装のイメージとして、下図のようなフローを考えつつ実装を始めました。

f:id:syossan:20181222115732p:plain

順に流れを追って解説致します。

接続情報管理ファイルの操作

接続情報を保持しておくために、接続情報管理ファイルを置き、そこから接続情報の読み込み等を行うようにしました。
そのため、まずは接続情報管理ファイルの存在を確認し、存在しない場合は生成する処理を実装します。

次に、ファイルの存在を確認できたら読み込み処理に入ります。

読み込み処理は少し複雑で、暗号化された接続情報管理ファイルの復号処理を行っております。
今回はAES暗号を採用しており、ハッシュ化にはSHA-256を用いています。

主に復号は次のステップで行っています。

  1. 暗号・復号に用いる鍵をSHA-256で変換
  2. 暗号化された接続情報管理ファイルの内容をbase64デコードする
  3. 変換された鍵を基にブロック・サイファーでのブロックを生成
  4. CTRモードでの鍵ストリーム生成のために初期化ベクトル、また初期化ベクトルを除いた暗号データを取得
  5. CTRモードで鍵ストリームを生成し、XORすることで平文を取得

あまり暗号には詳しくないのですが、多分この流れでキチンと暗号化されている・・・はずです。(※有識者の方、ご指摘あればお願いします)
この暗号・復号に際して、deeeetさんのこちらの記事がとても参考になりました。この場をお借りして感謝🙇

Go言語と暗号技術(AESからTLS) | SOTA

さて、無事に復号出来たところで、このデータを用いて接続情報の追加・更新・削除を行っていきます。

まずは追加処理から。

こちらも復号と同じプロセスで接続情報を暗号化し、接続情報管理ファイルに書き込みを行っております。
接続情報入力に際してはサクッと実装したかったのでprompterを使っています。便利。

github.com

更新処理はあまり追加処理と変わらないですが、このようになります。

どんどんいきましょう、次は削除処理。

これも指定した接続情報を除外した接続情報配列をファイルに保存し直すという簡単な処理になっています。

この他に、接続情報の一覧表示もあるのですが、説明を省略させていただきます。

パスワード認証によるSSH接続

さて、ここからは保存した接続情報を用いた、パスワード認証によるSSH接続の実装に入ります。
何はともあれ、まずはコードを見ていきましょう。

ガガガっと書かれていますが、こちらも大きく分けて2つの要素で構築されています。

  • SSHセッションの確立と設定
  • SSH擬似端末の設定

SSHセッションの確立と設定

connect 関数でセッションの確立を行っています。
パスワード認証の場合、SSHクライアント設定にてパスワード認証を渡す必要があります。

セッション確立の中でアレ?となるのは HostKeyCallback: ssh.InsecureIgnoreHostKey() のところでしょうかね。
これはSSHセッション確立の際に、finger print verificationをskipしている処理になります。
コメントを読むと It should not be used for production code. と書いてあるんですが、正直使ってるの自分くらいなので怒られたら直します。(あかん

セッションの設定については、入出力の設定とSSH擬似端末の関連付けているところくらいですかね。
あまり特筆すべきことはないですが、入出力設定とかはマストだと思うので書くのを忘れないようにしましょう。

SSH擬似端末の設定

次にSSH擬似端末の設定ですが、ここでいちばん大事なのは擬似端末のモード設定になります。
擬似端末にはRAWモードとCOOKEDモードの2種類があり、何が違うかというと「入力文字データをすぐに送信するか、バッファに溜めてから送信するか」の違いになります。
通常、今回のようなSSHに関するツールを作る際にはRAWモードが使われると思いますが、うっかりCOOKEDモードで動かしてしまうとこんな感じになってしまいます。

f:id:syossan:20181222182630p:plain

気を付けましょう。以下のリンク先を読むとより理解できるかと思います。

d.hatena.ne.jp

また、コード内にコメントとして詳しく書いてありますが、RFC4254を読むことでどのようなパラメータを設定すればよいか?などが分かるので、困った時は読むのをオススメします。

bash-completionの対応

最後にbash-completionの対応に入ります。 bash-completionとはよくあるコマンドの入力補完ですね。今回はサブコマンドや接続名の補完に使っていきたいと思います。
このSSH Managerでは urfave/cli を使っており、それを前提としたお話になりますのであしからず。

github.com

さて、bash-completionの実現には次の2つのステップを踏む必要があります。

  • bash-completion機能の有効化
  • 接続名補完の実装

bash-completion機能の有効化

urfave/cli のREADMEにも丁寧にbash-completion機能についての説明がありますが、やることとしては

になります。

EnableBashCompletionをtrueに設定するのはとても簡単で、以下のように行います。

次に、bash_autocompleteファイルの設置ですが、READMEに詳しい方法が書かれています。

https://github.com/urfave/cli#distribution

要するに、「/etc/bash_completion.d/にbash_autocompleteファイルを置く」という手順になります。
簡単は簡単なのですが、ユーザーにとっては一回限りの行動であったとしてもハードルがあるのは否めません。
そこで、この手順をサブコマンドの一つとして組み込むことにしました。

やっていることは単純で、sudo権限でコマンドを実行してもらい、手順通りに/etc/bash_completion.dディレクトにファイルを設置している形になります。
中でも、sudoのチェックの方法が「uidが0か否か」というのを知らなかったので、実装しながらへぇ〜って唸ってました。

さて、ここからは接続名補完の実装のお話です。
幸いなことに、 urfave/cli ではbash-completion機能を有効化にするだけで、サブコマンド等の補完は実現できます。
ただし、接続名などCLIツールとしての範疇外のところは自分で実装する必要があります。
該当コードとしては以下の部分です。

見て分かるように、やっていることは単純に接続名を出力しているだけですね。
このように補完したい項目をfmt.Printlnで出力してあげるだけで実装できます。よかったですね。

まとめ

このように、様々なハマりどころがあったりしますが、比較的簡単にSSH Managerを作成することが出来ました。
実際に、業務の中で試したりしていますが、今の所なんとか使えるかなーというレベルにはなっています。
ただし、動的な擬似端末サイズの変更などまだまだ実装しきれていないところがありますので、その辺もゆくゆくは実装していきたいと考えています。

以上、長文でしたがお読み頂きありがとうございました。

Goのあるコードを発端とした衝撃の結末

はい、というわけで今回はGoのあるコードから紡がれるストーリーに纏わるお話です。

一体何が起こったのか?

発端はこのツイートでした。

むむー、一体なんじゃらほい?とGoの当該ソースコードを見たら一目で分かりました。

// Bug compatibility with C bcrypt implementations. We only encode 23 of
// the 24 bytes encrypted.
hsh := base64Encode(cipherData[:maxCryptedHashSize])
return hsh, nil

github.com

うおー!たしかに Bug compatibility with C bcrypt implementations. って書いてあるー!
"24バイトから23バイト分を暗号化するよ"ってな感じですね。

ここから旅は始まる

まずはこのコードが生まれたところから遡ってみました。
どうやらGo1以前に実装されたようで、ここらへんで実装された様子が伺えます。

Issue 4964078: code review 4964078: crypto/bcrypt: new package - Code Review

うーーーーーん、どうやら「bcrypt実装を書いてみたぜ!」って感じでガバっとPR投げられてますね・・・
ここのレビュー等で件の24バイトから23バイトへ切り詰めている話は特にされてない雰囲気を感じ取ったので、ソースコードから意図を読み取るのを早々に諦めました。

bcryptについて調べる

次に「bcryptの実装上そうなってるのだろうか?」と思い、bcrypt自体を調べることに。

bcrypt - Wikipedia

安心と信頼のwikipediaですね。
幸いにもアルゴリズムに基づいた実装例があったので読んでみると、

Function bcrypt
   Input:
      cost:     Number (4..31)                      log2(Iterations). e.g. 12 ==> 212 = 4,096 iterations
      salt:     array of Bytes (16 bytes)           random salt
      password: array of Bytes (1..72 bytes)        UTF-8 encoded password
   Output: 
      hash:     array of Bytes (24 bytes)

   //Initialize Blowfish state with expensive key setup algorithm
   state 
  
    
    {\displaystyle \gets }
  
\gets EksBlowfishSetup(cost, salt, password)   

   //Repeatedly encrypt the text "OrpheanBeholderScryDoubt" 64 times
   ctext 
  
    
    {\displaystyle \gets }
  
\gets "OrpheanBeholderScryDoubt"  //24 bytes ==> three 64-bit blocks
   repeat (64)
      ctext 
  
    
    {\displaystyle \gets }
  
\gets EncryptECB(state, ctext) //encrypt using standard Blowfish in ECB mode

   //24-byte ctext is resulting password hash
   return Concatenate(cost, salt, ctext)

うーん、どうやら24bytesを特に切り詰めずにそのまま使ってるな・・・🤔
wikiが段々信用できなくなってきたので、ここは愚直にbcryptの提唱者であるNiels Provos & David Mazieresによる文献を読んでみましょう。

www.usenix.org

1999年の資料らしいのですが、こういうのがネット上に残ってるというのは改めてありがたいことですね。
で、アルゴリズムのところとか読んでみたのですが、ここにも特に言及しているところは無く・・・残念。

「こりゃもうダメかもしれんね」って感じで、半ば諦めつつ広大なネットの海を漂うことにしました。

驚きの記事が

「bcrypt 23bytes 24bytes」とか思考停止状態で検索していると、以下のような記事を見つけました。

hackernoon.com

ここに来るまでにかなりの数の記事を読んでいて、これもきっと書いてないんだろうなと白目になりながら見てたら衝撃の記載が。

Issue 2: Using 23 byte instead of the full 24 byte hash

オッ

As stated before, nearly all bcrypt implementations output a 23 byte long hash. The bcrypt algorithm however generates a 24 byte password hash by encrypting three 8 byte blocks using a password-derived blowfish key. The original reference implementation however choose truncate the hash output, it is rumored the reason is to cap it to a more manageable length of 60 character limit (a strange reason if you ask me). The consensus seems to be that the issue of cutting a hash byte off is not a meaningful degradation in security, so it stays an oddity inherited from the reference implementation.

米語なのでGoogle翻訳様に頼ってみましょう。

前述のように、ほぼすべてのbcrypt実装では、23バイトの長さのハッシュが出力されます。 しかし、bcryptアルゴリズムは、パスワード由来のブローキーを使用して3つの8バイトブロックを暗号化することによって、24バイトのパスワードハッシュを生成します。 しかし、元のリファレンス実装では、ハッシュ出力を切り捨てることを選択します。理由は、それが60文字の制限(あなたが私に尋ねると奇妙な理由)の管理しやすい長さにキャップすることです。 合意は、ハッシュバイトを切り捨てることの問題はセキュリティの意味のある劣化ではないので、参照実装から継承された奇妙なままであると考えられます。

ん????

24バイトのパスワードハッシュを生成します。 しかし、元のリファレンス実装では、ハッシュ出力を切り捨てることを選択します。

え?????

理由は、それが60文字の制限(あなたが私に尋ねると奇妙な理由)の管理しやすい長さにキャップすることです。

マジで??????

ありがたいことにHacker Newsのリンクが貼ってあったので、そちらも一応見てみましょう。

A possible flaw in open-source bcrypt implementations | Hacker News

私はpy-bcryptの作者です。py-bcryptは、それを記述している論文の著者(ProvosとMazieres)のbcryptの元の参照実装の周りの薄いラッパーです。ハッシュの長さの差異は完全に無害です(衝突の可能性は2 ^ -186となります)、リファレンス実装に含まれていました。私はハッシュが少し切り捨てられている理由を正確にはわかりませんが、DavidやNielsは60文字のハッシュが扱いやすい長さだと思っていたと思います。

ほげ〜〜〜〜〜〜〜〜〜〜〜

そして、リファレンス実装を見てみると・・・

cvsweb.openbsd.org

encode_base64((u_int8_t *) encrypted + strlen(encrypted), ciphertext,
    4 * BCRYPT_BLOCKS - 1);

( д) ゚ ゚

なんで・・・なんで-1しとるんや・・・
ということで無事、衝撃の結末に辿り着きました。

まとめ

どうやらGoだけではなく、PythonJavaで実装されているbcryptライブラリも、殆どがこのリファレンス実装に従っているみたいですね。
一体どうしてこうなったのかは完全に謎ですが、なんとかハラオチできる結論に辿り着いたのでヨシとしましょう。

現場からは以上です。

[追記]