package bots import ( "context" "crypto/rand" "crypto/sha256" "encoding/hex" "errors" "strings" "time" "atlas9.dev/c/core" "atlas9.dev/c/core/iam" ) type Key struct { ID core.ID Tenant core.ID Bot core.ID SecretHash string CreatedAt time.Time ExpiresAt time.Time } func NewKey(tenant, bot core.ID) (*Key, string, error) { // Generate a random secret. Encode as a hex string. data := make([]byte, 32) rand.Read(data) sec := hex.EncodeToString(data) key := &Key{ ID: core.NewID("key"), Tenant: tenant, Bot: bot, SecretHash: HashSecret(sec), CreatedAt: time.Now(), ExpiresAt: time.Now().Add(time.Hour * 24), } return key, sec, nil } // HashSecret returns the hex-encoded SHA-256 of a key secret, the only form // stored at rest. Secrets are 32 bytes of crypto/rand output, so an unsalted // hash is enough to make a stolen database useless. func HashSecret(secret string) string { sum := sha256.Sum256([]byte(secret)) return hex.EncodeToString(sum[:]) } // FormatToken builds the bearer credential handed to the client once at key // creation: ".". Key IDs never contain a dot, so the first // dot separates the parts. func FormatToken(keyID core.ID, secret string) string { return keyID.String() + "." + secret } // ParseToken splits a bearer credential into its key ID and secret. func ParseToken(token string) (keyID, secret string, err error) { keyID, secret, ok := strings.Cut(token, ".") if !ok || keyID == "" || secret == "" { return "", "", errors.New("malformed api key token") } return keyID, secret, nil } var Cap_BotKeys_Read = iam.NewCap("BotKeys_Read") var Cap_BotKeys_Write = iam.NewCap("BotKeys_Write") // KeyStore defines the interface for persisting bot keys. type KeyStore interface { Create(ctx context.Context, key *Key) error SetExpiration(ctx context.Context, tenant, keyID core.ID, expiration time.Time) error Get(ctx context.Context, tenant, keyID core.ID, out *Key) error // GetByID loads a key by ID alone, for the IAM middleware, which learns // the tenant from the key row itself. GetByID(ctx context.Context, keyID core.ID, out *Key) error List(ctx context.Context, tenant core.ID, out *core.Page[Key]) error }