Files
cozystack/pkg/credentials/format/format_test.go
Timofei Larkin 0bcded822e feat(api): add Credential kinds and the credentials block
Cozystack generates managed application passwords at Helm render time
and keeps them in a Secret tenants can read again at any time, which a
deployment whose policy requires a credential shown once cannot accept.
The accepted design moves generation into the API: an
ApplicationDefinition declares its accounts and the stored form of a
password in a credentials block, and each account gets a
core.cozystack.io Credential whose mint and revoke subresources take a
TokenRequest-shaped CredentialRequest.

This adds the types, the built-in password formats, which the migration
CLI shares so a converted release and a mint store the same form, and
the account helpers that name the Secrets and read their record.

Design: https://github.com/cozystack/community/blob/main/design-proposals/user-secrets-api/README.md

Assisted-by: LLM
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2026-10-14 12:29:46 +02:00

123 lines
3.3 KiB
Go

// SPDX-License-Identifier: Apache-2.0
package format
import (
"crypto/hmac"
"crypto/pbkdf2"
"crypto/sha256"
"encoding/base64"
"regexp"
"strings"
"testing"
)
func TestGeneratePassword(t *testing.T) {
seen := map[string]bool{}
for range 64 {
p, err := GeneratePassword()
if err != nil {
t.Fatalf("generate: %v", err)
}
if len(p) != PasswordLength {
t.Fatalf("password %q has length %d, want %d", p, len(p), PasswordLength)
}
for _, c := range p {
if !strings.ContainsRune(passwordAlphabet, c) {
t.Fatalf("password %q carries %q outside the alphabet", p, c)
}
}
if seen[p] {
t.Fatalf("password %q generated twice", p)
}
seen[p] = true
}
}
// The verifier must be the canonical PostgreSQL form, since CNPG forwards it
// unchanged and PostgreSQL stores anything else as a plaintext password. The
// keys are recomputed here from the salt the verifier carries with the
// primitives RFC 5802 names, independently of the encoder's own helpers.
func TestEncodeScramSHA256(t *testing.T) {
const password = "pencil"
salt, err := base64.StdEncoding.DecodeString("W22ZaJ0SNY7soEsUEjb6gQ==")
if err != nil {
t.Fatal(err)
}
got, err := encodeScramSHA256(password, salt, 4096)
if err != nil {
t.Fatalf("encode: %v", err)
}
re := regexp.MustCompile(`^SCRAM-SHA-256\$(\d+):([A-Za-z0-9+/=]+)\$([A-Za-z0-9+/=]+):([A-Za-z0-9+/=]+)$`)
m := re.FindStringSubmatch(got)
if m == nil {
t.Fatalf("verifier %q is not in the canonical form", got)
}
if m[1] != "4096" {
t.Errorf("iterations %s, want 4096", m[1])
}
if m[2] != "W22ZaJ0SNY7soEsUEjb6gQ==" {
t.Errorf("salt %s, want the input salt", m[2])
}
salted, err := pbkdf2.Key(sha256.New, password, salt, 4096, 32)
if err != nil {
t.Fatal(err)
}
mac := hmac.New(sha256.New, salted)
mac.Write([]byte("Client Key"))
stored := sha256.Sum256(mac.Sum(nil))
mac = hmac.New(sha256.New, salted)
mac.Write([]byte("Server Key"))
server := mac.Sum(nil)
if want := base64.StdEncoding.EncodeToString(stored[:]); m[3] != want {
t.Errorf("StoredKey %s, want %s", m[3], want)
}
if want := base64.StdEncoding.EncodeToString(server); m[4] != want {
t.Errorf("ServerKey %s, want %s", m[4], want)
}
// A fresh salt every call, so two encodings of one password differ.
a, _ := Encode(ScramSHA256, password)
b, _ := Encode(ScramSHA256, password)
if a == b {
t.Errorf("two SCRAM encodings share a salt: %s", a)
}
}
func TestEncodeFixedVectors(t *testing.T) {
tests := []struct {
format, password, want string
}{
// MySQL's own PASSWORD('password').
{MySQLNative, "password", "*2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19"},
// SHA-256 of "abc", FIPS 180-2 example B.1.
{SHA256, "abc", "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"},
{Plaintext, "as-is", "as-is"},
}
for _, tt := range tests {
got, err := Encode(tt.format, tt.password)
if err != nil {
t.Fatalf("%s: %v", tt.format, err)
}
if got != tt.want {
t.Errorf("%s(%q) = %q, want %q", tt.format, tt.password, got, tt.want)
}
}
}
func TestEncodeUnknownFormat(t *testing.T) {
if _, err := Encode("apr1", "x"); err == nil {
t.Fatal("expected an error for an unknown format")
}
for _, name := range []string{ScramSHA256, MySQLNative, SHA256, Plaintext} {
if !Known(name) {
t.Errorf("%s not known", name)
}
}
if Known("apr1") {
t.Error("apr1 reported known")
}
}