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 にログインしてみないと判別できないのでわりとデバッグや検証に時間がかかった。

リファレンス