Posts for: #Auth

rbac なロールでの動的な権限チェック

23時に寝て何度か起きて7時に起きた。夜に作業するつもりが、メイドインアビス を見ながら寝落ちした。

ldap で認証したユーザーの権限管理

rbac なロール管理 の仕組みを api サーバーに実装した。初期実装したときは静的にリソース (url) に対して acl から権限が許可されているかどうかのみをチェックするだけでよかった。ldap で認証したユーザーは自分の情報は更新できるが、他人の情報は更新できないといったリクエスト単位に権限が許可されているかどうかをミドルウェアで判別したい。いわば、リクエスト時に動的にリソースの情報をチェックしないといけない。

いくつか試行錯誤して、あーでもないこーどもないとやった結果、次のように落ち着いた。

  • リソースは動的な権限チェックのために AuthorizeFunc を取得するための AuthorizeKey をもつ
  • AuthorizeFunc は Context から取得する
  • AuthorizeFunc が true を返す場合は権限を付与する
func (r *Role) CanAccess(ctx context.Context, resourceName, target string) bool {
	for _, ac := range r.ACL {
		if ac.Resource.Match(resourceName) {
			for _, perm := range ac.Permissions {
				if perm.IsGranted(target) {
					return true
				}
			}
			if authorize, ok := ctx.Value(ac.Resource.Key).(AuthorizeFunc); ok {
				if authorize(ctx, resourceName, target) {
					return true
				}
			}
		}
	}
	return false
}

ロールに渡す Context にはあらかじめ、AuthorizeKey と AuthorizeFunc を登録しておく。

ctx = context.WithValue(ctx, role.LDAPResource, role.IsLDAPResourceOwner)

リソースを生成するときに AuthorizeKey をセットしておく。

ldapUserResource = rbac.NewResource(
	"ldap-user-specific-operation",
	"(/api/ldap/entry|/api/ldap/entry/.*)",
	LDAPResource,
)

ちょっと設定は煩雑になるが、ひとまずこれで要件を満たせた。

権限管理ができるようになった

1時に寝て何度か起きて7時半に起きた。なんかしんどい夢をみたが、もう覚えていない。

rbac なミドルウェアの実装

昨日の続き 。rbac (role-based access control) なライブラリが一通り実装できたのでそれを使って echo のミドルウェア を実装した。やりたいことは次のようなこと。たったこれだけだが、これを実装するために、ここ1週間ほど、あれやこれやの実装をしてきた。ようやくそれが動くようになったというところ。私にとってはミドルウェアの仕組みは過去に何度も実装してきたものだが、頭に描いたイメージのまま、実装できたのがよかったと思う。

func rbacWithConfig(cfg rbacConfig) echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			if cfg.Skipper(c) {
				return next(c)
			}

			req := c.Request()
			ctx := req.Context()
			sessionStore := c.Get(keySession).(session.Store)
			userName := c.Get("username").(string)
			s, err := session.Get(ctx, sessionStore, userName)
			if err != nil {
				return fmt.Errorf("failed to get session: %w", err)
			}
			if !s.Role.CanAccess(ctx, req.URL.Path, req.Method) {
				return echo.ErrForbidden
			}

			return next(c)
		}
	}
}

node.js のアップグレード

ちょうど管理画面も少し触ろうと思って環境を構築し始めた。たまたま node.js のバージョンをみると、ちょうど10月18日で 18 (LTS) の active support 期間が終了していた。security support は2025年4月まであるのでそんな急がなくてもよいが、それに気付いたついでなので 20 (LTS) にアップグレードすることにした。幸い、大半のライブラリは 20 (LTS) でもそのまま動いた。しかし、依存ライブラリのアップデートをしていて、一部 conflict してうまく動かないライブラリもあった。細かい原因調査をしていないが、フロントエンドの方がこまめにバージョンを上げていかないと何が原因でアプリケーションが正常に動かなくなるのか、分からなくなる気がする。

初めて rbac なライブラリを実装した

22時頃から休んでいて寝たり起きたりで7時に起きた。起きたらネットの記事とかだらだら読んでた。

rbac なライブラリの実装

昨日から認可のための仕組みを調査している。私の中ではもっとも一般的な rbac (role-based access control) でまずは作ってみようと思う。次の2つのライブラリの利用を検討したが、自分たちのやりたいことにあわない気がして今回は見送ることにした。

一通り、ライブラリとして使えるように参照実装した。これから実際のアプリケーション要件にあわせてミドルウェアとして rbac な認可処理を作っていく。

変わりゆく世界秩序

サンフランシスコが陥った負の“スパイラル” の記事にあるような、米国で950ドル以下の窃盗は軽犯罪とするという法律の変更によって、万引きを逮捕しなくなってモラル崩壊が起きて、小売店の商品を普通に盗むという事件が多発しているらしい。fin-py でおがわさんとそんな話しをしていたら次の動画を教えてもらった。私は歴史が好きなので、こういった「歴史は繰り返す」といったものはだいたいみてしまう。厳密な裏付けはわからないが、盛者必衰という言葉もあるように、どんな国でも栄枯盛衰のサイクルはあるだろうというのは大局の視点として同意できる。過去の歴史と国の栄枯盛衰をいくつかの指標とお金の視点から調査したものでおもしろかった。

日本は80年サイクルで戦争の周期がくるといった説もあるが、この動画でもサイクルの切り替わりのタイミングで平和的にしろ暴力的にしろ、かならず戦争は起きると説明している。もうすでに戦争は始まっている感もあるが、戦争は避けようがないという点も同意するところだ。本も読んでみようと思う。

パスワードリセットのテストのためのガイドライン

0時に寝て何度か起きて4時に起きて仮眠して6時に起きた。あまり寝た気がしない。

OWASP のパスワードリセットのガイドライン

パスワードリセットの仕組みをメンバーに開発してもらっている。そのコードレビューが先週から白熱している。セキュリティが関連するので堅牢に作る必要があるのでここはあまり妥協せきない。お手伝い先のシニアエンジニアから独自設計で作るのではなく、最低限、世の中の一般的なガイドラインに従っているかを確認するために OWASP のガイドラインを紹介してくれた。これはパスワードリセットの仕組みをテストするための要項をあげている。

これをメンバーに読んでもらって理解して実装しろと言いたいところでもあるが、私自身、読んだことがないとレビューできないことに気付いて、これは私が読んだ上で既存の設計や実装を見直すべきだと判断して deepl を駆使しながらほとんどを読んでみた。たしかに読んでみていくつか抜け・漏れに気付いたり、うちのセキュリティポリシーとして意図的に緩和しているところも認識できたりして、結論から言って読んでよかったと思う。当初はパスワードリセットのために一時トークンを1つだけ使っていたのだけど、それも2つを別々の経路に送って、割符のように組み合わせて認証する仕組みに変更した。よりセキュアにするという意図では一時トークンも1つよりも2つの方がよいというのは概ね正しいと思う。

標準ライブラリに XOAUTH2 の実装がない

0時に寝て3時に起きて5時ぐらいまでネットで遊んでて6時半に起きた。昨日の夜に洗濯しようと思って忘れていたので朝から洗濯した。

隔週の雑談

顧問のはらさんと隔週の打ち合わせ。今日の議題はこれら。

  • 税理士さんとの打ち合わせのふりかえり
  • 昔お手伝いした会社の開発体制の話
  • 新しいチーム勉強会 の導入

3人の税理士さんと打ち合わせしてみて最終的に顧問契約をお願いする方を決めた。話してみてやり取りした雰囲気だと、その税理士さんもスキルやこちらの要件対応については全く問題なさそうに思えた。あとは報酬とうちの会社の規模などを考慮して選択した。

昔お手伝いした会社で2年経ってちょっと相談にのってほしいという打ち合わせをした。私がいた2年前と開発体制はまったく変わってなくて、未だにテックリードがほぼ1人で開発している状況らしい。私が辞めてから以降も何人かは開発者が入っては辞めを繰り返しているのだと推測する。私も2度とその開発者と一緒に働きたくないと思うぐらいには信頼してなくて、開発者が引く手あまたな世の中の状況において、人間として信頼されないリーダーって致命的なんだなということを改めて実感した。おそらくテックリードを追放しない限り、あの開発体制 (と言ってもほぼ独り開発) は何も変わらないのだろうと思う。

oauth 2.0 で認証して google の smtp サーバーを使う

昨日の続き

リフレッシュトークンを使って取得したアクセストークンで smtp の AUTH コマンドで XOAUTH2 で認証すればよい。仕様は次のドキュメントに書いてある。

なぜか go の標準ライブラリの net/smtp には Plain と CRAM-MD5 の2つしか実装されていない。AUTH コマンドの実装は smtp.Auth インターフェースで定義されている。

type Auth interface {
	Start(server *ServerInfo) (proto string, toServer []byte, err error)
	Next(fromServer []byte, more bool) (toServer []byte, err error)
}

正常系の雑な実装だとこんな感じ。

type oauth2 struct {
	user        string
	tokenType   string
	accessToken string
}

func (o *oauth2) Start(server *smtp.ServerInfo) (string, []byte, error) {
	if !server.TLS {
		return "", nil, fmt.Errorf("need tls")
	}
	resp := []byte("user=" + o.user "\001auth=" + o.tokenType  + " " + o.accessToken + "\001\001")
	return "XOAUTH2", resp, nil
}

func (o *oauth2) Next(fromServer []byte, more bool) ([]byte, error) {
	if more {
		return nil, errors.New("unexpected server challenge")
	}
	return nil, nil
}

ググるとサンプルコードを実装している人たちがちらほらいるので、そのうち標準ライブラリに誰か実装してくれると思う。

go 本体に pr を送るチャンスでもあるけど、Contribution Guide を少し眺めて大変そうと思って、いまそこまでのモチベーションないなって感じ。

xoauth2 という smtp の認証

0時に寝て何度か起きて6時に起きた。昨日、凡人が天才に挑むという状況で、キングダムの 蒙驁 将軍が廉頗に挑むみたいな状況を思い出して見返していた。史実では蒙驁が魏を攻めて東郡を置いたというのは事実だが、廉頗と戦ったという記録はなく、おそらくは蒙驁と廉頗に因縁があって雪辱戦としたというのはキングダムの創作だろうと推測される。

oauth 2.0 で認証して google の smtp サーバーを使う

google さんの smtp.gmail.com の smtp サーバーを使ってメールを送信したい。

2019年にパスワード認証は廃止するので oauth 2.0 へ移行してくださいといった、最初のアナウンスが行われて、もうできないかと思ったら2024年9月30日に完全廃止するのかな?まだパスワード認証は動くかもしれない。一方で oauth 2.0 へ移行しないといけないのでその調査をメンバーにしてもらっていた。結局、途中からは私も本気になって調べていた。

oauth 2.0 で認証してアクセストークンとリフレッシュトークンを取得するためのサンプルコードとして OAuth2DotPyRunThrough が用意されている。このトークンを取得するときに callback の url にユーザーが明示的にアクセスして同意する必要がある。ここで得たアクセストークンは1時間で有効期限がきれる。しかし、リフレッシュトークンはユーザーが revoke しない限りは永続的に使えるそうで、このリフレッシュトークンを使って必要なときにアクセストークンを取得するというのが google さんの oauth 2.0 のアプリケーションの運用になるみたい。つまりリフレッシュトークンをアプリケーション側で管理することでアクセストークンは何度でも取得できる。

OAuth 2.0 Mechanism によると、取得したアクセストークンを使って XOAUTH2 という smtp の認証方式で認証すれば smtp サーバーに対して smtp でメールを送信できる。gmail 以外でメールをやり取りする機会がなくなって数年たつ。smtp の仕組みとか、まったく忘れてしまって関心もない。たったこれだけなんだけど、右往左往してあちこち調べることになった。ややこしいのは google のクラウド api 経由でメールを送ることもできて、そのやり方と混同するとまったく違う方向に行ってしまう。そこだけ注意。

ステートレスな認証という概念

0時に寝て4時ぐらいに起きてだらだらして7時半に起きた。やっぱりあまり眠れない。

ステートレスな認証という概念

次の開発フェーズが始まっていて、ちょっと時間が経ってしまったが、前開発フェーズのお披露目的な製品紹介をお手伝い先の全社向けに行った。主には直近の開発フェーズで追加した機能などを紹介した。その過程で新たに認証の仕組みを追加して jwt で認証するといった話しをしたところ、それはステートレスなのかどうかといった質問が出た。セキュリティを考慮して、アーキテクチャ的にフロントエンドの認証と api サーバーの認証は分けて実装しているのと、そのために仕組みも複雑になっているのだけど、ステートレスという言葉が指す意図を私がよくわかっていなくて、うまく説明できなかった。説明を終えた後にアーキテクチャのイメージ図と一緒に補足をしながらやり取りして次の記事を教えてもらった。

jwt は暗号化の技術で認証する仕組みなので有効期限が切れるまでは有効なアクセストークンとなる。そのため、jwt のみだとログアウトという概念はないため、そこをどうしているのか?という質問だった。フロントエンド/api サーバーともに session をオンメモリで保持して、ログインしたユーザーを管理しているため、ログアウトしたら session からレコードを削除することで有効な jwt のアクセストークンが来ても認証エラーにしてしまうことでステートをもった認証方式を実現している。とくまる先生が次のように説明しているところ。

「セッションIDをJWTに内包する」 という考え方です。

うちはこれをセッション ID ではなくユーザー名でやっている。とくに難しいことをやっているわけではなく、普通に実装したらそんな感じかな?と考えていたが、jwt = ステートレス認証だと思い込んでいる人たちがいるから ストートレスな認証 というキーワードが出てきたんだなと理解できた。最近のトレンドとしてはログアウトで jwt のアクセストークンを無効にできないと脆弱性と指摘される可能性がありそうとも書いてある。

ログアウト時にJWTを無効化できない実装は今後脆弱性診断で「OWASP Top 10 2021違反」と指摘されるようになりそう(今も個別にされてるかもしれないけど)

私はアーキテクチャ的にブラウザに api サーバーのアクセストークンをみせないというところに注力して認証機能の開発をサポートしていた。それ自体も間違っていないとは思うけど、今回の質問はその工夫とは異なるところの質問だった。認証は難しい。

トークン認証のプロバイダ実装

22時から寝始めて何度か起きて6時に起きた。最近は早寝早起きにしている。

client ライブラリのトークン認証対応

いまの開発の新機能の1つにローカルアカウントの管理機能がある。普通のパスワード認証により、JWT によるアクセストークンを発行し、リフレッシュトークンを使ってアクセストークンの再取得を行う。api サーバーで実装してもらったこれらの web api を使って、api client 側でもログインしてアクセストークンを取得して web api のリクエストができるようにする。一般的にアクセストークンは有効期限が短いため、有効期限が切れたときは透過的にリフレッシュトークンを使ってアクセストークンを再取得する。またリフレッシュトークンの有効期限が切れたときは再ログインして、アクセストークンとリフレッシュトークンを再取得する。

文章で書けばこれだけの機能だけど、このための AuthProvider を実装した。最終的には次のインターフェースになった。

type AuthProvider interface {
	CanRefresh() bool
	GetAuthorization() (string, error)
	GetType() AuthType
	Refresh() error
}

先週たまたま Azure/azure-sdk-for-goAuthProvider のソースコードを読んだ。やりたいことに対して、かなり複雑なことをしているようにみえたが、アクセストークンのキャッシュ、有効期限が切れたときのリフレッシュを透過的に行うコードだった。このライブラリの実装が読みにくいコードで、私だったらもっとシンプルに実装するというイメージが先週からあったのでそのイメージ通りに実装して1日で対応を終えた。本当はこの機能をいまの開発フェーズで提供する予定はなかったんだけど、うまく簡潔に実装できたので一部 agent で導入してテストで検証することにした。

yubikey bio を完全にマスターした

2時に寝て遅くまで飲んでいたせいか、やや吐き気もしながら朝起きてだらだらして11時ぐらいに起きた。

1password のロック解除を yubikey で行う

先日購入して触っていた yubikey bio の設定 の続き。1password のロック解除を指紋認証で行いたかったが、1password の 8.10.4 のアプリではロック解除をシステム認証で行おうとするとエラーが発生していた (バグってた) 。5月の頭に 8.10.6 がリリースされていてシステム認証のバグが直っていることに気付いた。1password のアプリケーションがどうやって linux のシステム認証を使っているかは次の1文に書いてある。

システム認証は、Linux のユーザーアカウントに組み込まれたアクセス制御機構を使用します。これは polkit と PAM (Pluggable Authentication Modules) という2つの Linux 標準に依存しています。この2つを組み合わせることで、安全な認証サービスを提供します

https://support.1password.com/system-authentication-linux/#about-system-authentication-security

私は polkit を使ったことがなくて初見でよく分かっていないが、どうやら polkit から pam を介して認証しているようにみえる。pam.d 配下の設定を調べてみると /etc/pam.d/polkit-1 がある。前回の設定で pam.d の設定とテストの方法を理解していた。ここまでくれば Module Arguments のドキュメントをみながらオプションを設定するだけ。1Password のロック解除をYubiKeyでやる の記事で origin/appid にホスト名を指定しているが、最新のモジュールだと指定しないときはデフォルトでホスト名が使われるようにみえる。

  • origin: FIDO 認証手順の party ID を設定する、値が指定されない場合は識別子 pam://$HOSTNAME が使用される
  • appid: FIDO 認証手順の application ID を設定する、値を指定しない場合は origin で使用されたものと同じ値が使用される (origin も指定しない場合は pam://$HOSTNAME)
  • nouserok: 認証しようとするユーザーが authfile 内に存在しない場合や authfile が欠落/不正確な場合でも、認証の試行を成功させるように設定する
  • cue: タッチすることを促すメッセージを表示するように設定する

これらを踏まえて u2f で認証が成功したらそれ以降の処理をスキップするように次の設定を先頭に追加する。

$ sudo vi /etc/pam.d/polkit-1
auth   sufficient   pam_u2f.so nouserok cue
...

cue を指定したことでパスワードプロンプトを表示せず、デバイスをタッチするように促される。よい感じ。

$ pamtester polkit-1 t2y authenticate
Please touch the device.
pamtester: successfully authenticated

これで pam の polkit の設定が正しいことを検証できる。この後に 1password のアプリケーションでロック解除するときに u2f を使って指紋認証できた。めっちゃ便利。

yubikey bio を触ってみた

3時半に寝て7時に起きた。リリース終えたので晩ご飯を食べてから自分の会社のお仕事をしていたら遅くなった。

ストレッチ

今週もリリース明けでそれほど座っている時間が長かったわけでもないため、腰の張りはあまりなく、負荷は低くなっているのではないかと推測する。今日の開脚幅は開始前156cmで、ストレッチ後158cmだった。右股関節周りの硬さは相変わらず大きく変化はないが、他がよくなった分、右ふともも前の筋を伸ばすと張りがきつくてしんどいように感じた。しばらく余裕のある生活ができるので歩いたりストレッチしたりする余裕が出てくるかもしれない。

yubikey bio を触ってみた

先日 yubikey bio をオンラインストアで購入 した。会社の経費で業務で使うことを目的に購入したのでこの場合は商用利用の輸入にあたる。また別途、輸入についての会計処理を書く。yubico はスウェーデンの会社でどうやら日本向けはスウェーデンの首都ストックホルムから発送されてきたらしい。安いエコノミープランで注文していた。発送メールを受け取ったのが 4/17 でオフィスの郵便受けに入っていることに気付いたのが 4/29 になる。12日で届いたことになる。船便にしては早いからなにかしら航空便の安いプランで届いたのだと推測する。

Economy - 10-20 Working Days - No tracking available

早速、接続していろいろ触ってみた。結論から言って ubuntu 22.04 で fido2 pin を設定して u2f を使って2要素認証のメソッドとして pam や web アプリケーションから問題なく使えた。ubuntu 向けのアプリケーションも ppa のリポジトリを追加して apt からインストールできる。

このドキュメントには次の4つのアプリケーションのインストールが書いてある。

  • YubiKey Manager (CLI): sudo apt install yubikey-manager
  • YubiKey Personalization Tool: sudo apt install yubikey-personalization-gui
  • libpam-yubico: sudo apt install libpam-yubico
  • libpam-u2f: sudo apt install libpam-u2f

yubikey は他にもいろいろな認証の用途に使えるらしい。今回は fido2 の設定のみを紹介する。fido2 pin を登録するには YubiKey Manager の GUI を使った方が簡単なので次のアプリケーションも一緒にインストールするとよい。

$ sudo apt install -y yubikey-manager-qt
$ ykman-gui

おそらく ykman の cli でも登録できると思うけど、yubikey や認証設定に慣れていない初心者は gui のナビゲーションに従って作業して結果を確認した方が安心だと思う。fido2 pin は ssh でいうところのパスフレーズのようなものなのかな?デバイスが盗まれるのを防ぐために pin があるという。一方で web アプリケーションが fido2 で認証するときに pin を要求するかどうかは任意になるらしい。よくあるパターンは初めて使うときは pin を要求して2回目以降はスキップするといった用途が多いのではないかと推測する。

この fido2 pin を使って Universal 2nd Factor (u2f) の設定を行う。

$ mkdir -p ~/.config/Yubico
$ pamu2fcfg > ~/.config/Yubico/u2f_keys
Enter PIN for /dev/hidraw3: ***  <= このときに yubikey manager で設定した fido2 pin を入力

この後 yubikey デバイスが点滅するので丸いセンサー部分を指でタッチすると完了する。このときに指紋登録しているのか、物理的にタッチすることを要求しているだけなのか、よくわかっていない。この前に chrome で指紋登録 をしていたのでそれが使われているのかもしれない。いま ykman で設定をみたら Fingerprints registered とあるのでどこかしらに指紋登録されているらしい。おそらく chrome じゃないかと推測する。

$ ykman fido info
WARNING: PC/SC not available. Smart card (CCID) protocols will not function.
PIN is set, with 8 attempt(s) remaining.
Fingerprints registered, with 3 attempt(s) remaining.
Always Require User Verification is turned on.

linux で認証時に u2f を使いたいときは pam_u2f.so を使って pam の設定をするとよい。ubuntu だと /etc/pam.d/ 配下にいろんな認証設定がある。例えば login 時に2要素認証をしたい場合は次のようにフックする。パスワード入力した後に yubikey のセンサーを物理的にタッチして指紋認証を行える。セキュリティに厳しい会社はこういった運用をしているのかもしれない。

$ sudo vi /etc/pam.d/login
...
# Standard Un*x authentication.
@include common-auth
auth       required    pam_u2f.so

パスワード認証の代替として使いたい場合は通常の認証の前に sufficient として呼び出せば指紋認証が成功したときにそれ以降の処理がスキップされる。

auth       sufficient    pam_u2f.so
@include common-auth

これらの設定を確認にするには pamtester というツールを使うと簡単にできる。認証の設定を誤るとログインできなくなってしまうので慎重にテストして振る舞いを確認した上で実際の運用を行うとよい。

$ pamtester login t2y authenticate  <= このときに yubikey デバイスが点滅するので指紋認証する
pamtester: successfully authenticated

せっかく購入したので 1password アカウントや github の2要素認証に設定してみた。ワンタイムパスワードの otp の代替として使える。ワンタイムパスワードを入力するより yubikey のセンサーをタッチする方が日々の運用としては少しお手軽と言える。設定していて otp と yubikey でお互いに2要素認証のバックアップを兼ねることに気付いた。認証方法が増えるのでセキュリティ的には脆弱になるけれど、サービスとセキュリティのトレードオフの考え方から、yubikey の分だけ脆弱になっても物理的なセキュリティを担保できるのであれば利便性を考慮してよいように私には思えた。

unix crypt(3) をよくわかってなかった

0時に寝て2回ほど起きて7時に起きた。わりと気分がよい方。

unix の crypt(3) というライブラリ実装

google の Admin console の api の REST Resource: usershashFunction として crypt を選択してハッシュ化したパスワードを連携できる。

crypt - C crypt ライブラリに準拠しています。DES、MD5(ハッシュ プレフィックス $1$)、SHA-256(ハッシュ プレフィックス $5$)、SHA-512(ハッシュ プレフィックス $6$)ハッシュ アルゴリズムをサポートします。

この crypt というのは単純に sha256 や sha512 でハッシュ化すればよいわけではなく、歴史的経緯でそれぞれの os ごとにある crypt ライブラリの実装に依存しているらしい。

$ man 3 crypt

おそらく google のドキュメントがいう C crypt ライブラリというのは glibc のことを指していると考えてよいと思うが、go の準標準パッケージである golang.org/x/crypto を探してもその実装は存在しない。これも推測だが、仕様が曖昧なものを go の開発者は実装しようとしないのだと思う。とはいえ、c の crypt ライブラリをラップして go から使うのも面倒と言えば面倒なので誰かが crypt ライブラリを真似て野良実装して、それが一部で使われていたりするようにみえる。しかし、なぜかそのオリジナルを作った開発者はそのコードのリポジトリを削除していて、ソースコードのコピーがまわりまわって、いま github.com/GehirnInc/crypt で保守されているらしい。このライブラリを使ってエンコードすると c の crypt ライブラリの出力と一致することは確認できた。この実装をみれば、単純にエンコードすればよいといったものではないことが伺えるので pure go のライブラリとして共有されているのは有り難い。

このライブラリを使ってハッシュ化した文字列と c 言語のコードも chatgpt に書いてもらっていくつか一致することは検証できた。デバッグしていて、もう1つ salt を生成も特定の文字しか使えないのでうっかり乱数を使って文字列生成していると間違ってしまう。

var saltChars = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./")

func GenerateSalt(method Method) []byte {
	var b = make([]byte, 16)
	charsLength := len(saltChars)
	for i := range b {
		b[i] = saltChars[rand.Intn(charsLength)]
	}
	var salt []byte
	switch method {
	case SHA256:
		salt = append([]byte("$5$"), b...)
	case SHA512:
		salt = append([]byte("$6$"), b...)
	default:
		panic(fmt.Sprintf("unsupported salt method: %s", method))
	}
	return salt
}

ここで生成した salt を使って github.com/GehirnInc/crypt を使うとこんな感じで crypt を使って google のユーザーアカウント連携ができる。

func Crypt(password, salt []byte) (string, error) {                              
    if len(salt) < 3 {                                                           
        return "", fmt.Errorf("invalid salt: %s", string(salt))                  
    }                                                                            
                                                                                 
    var crypter crypt.Crypter                                                    
    switch string(salt[0:3]) {                                                   
    case "$5$":                                                                  
        crypter = crypt.SHA256.New()                                             
    case "$6$":                                                                  
        crypter = crypt.SHA512.New()                                             
    default:                                                                     
        return "", fmt.Errorf("unsupported salt prefix: %s", string(salt[0:3]))  
    }                                                                            
    hashed, _ := crypter.Generate(password, salt)                                
    err := crypter.Verify(hashed, password)                                      
    return hashed, err                                                           
}

ハッシュ化した文字列が正しいかどうかは実際に google にログインしてみないと判別できないのでわりとデバッグや検証に時間がかかった。

リファレンス