test(credential): add test for env:// key passthrough in encryptPlaintextAPIKeys

This commit is contained in:
sushi30 2026-03-26 21:20:59 +01:00 committed by github-actions[bot]
parent c33970192d
commit c10ca240ca
2 changed files with 3 additions and 77 deletions

View file

@ -1171,80 +1171,6 @@ func toNameIndex(list []*ModelConfig) []string {
return nameList
}
// encryptPlaintextAPIKeys returns a copy of models with plaintext api_key values
// encrypted. Returns (nil, nil) when nothing changed (all keys already sealed or
// empty). Returns (nil, error) if any key fails to encrypt — callers must treat
// this as a hard failure to prevent a mixed plaintext/ciphertext state on disk.
// Symmetric counterpart of resolveAPIKeys: both operate purely on []ModelConfig
// and leave JSON marshaling to the caller.
func encryptPlaintextAPIKeys(
models map[string]ModelSecurityEntry,
passphrase string,
) (map[string]ModelSecurityEntry, error) {
sealed := make(map[string]ModelSecurityEntry, len(models))
changed := false
for k, m := range models {
sealedEntry := ModelSecurityEntry{APIKeys: make([]string, len(m.APIKeys))}
// Encrypt each key in APIKeys
for i, key := range m.APIKeys {
if key == "" || strings.HasPrefix(key, "enc://") || strings.HasPrefix(key, "file://") || strings.HasPrefix(key, "env://") {
sealedEntry.APIKeys[i] = key
continue
}
encrypted, err := credential.Encrypt(passphrase, "", key)
if err != nil {
return nil, fmt.Errorf("cannot seal api_key for model %q: %w", k, err)
}
sealedEntry.APIKeys[i] = encrypted
changed = true
}
sealed[k] = sealedEntry
}
if !changed {
return nil, nil
}
return sealed, nil
}
// resolveAPIKeys decrypts or dereferences each api_key in models in-place.
// Supports plaintext (no-op), file:// (read from configDir), and enc:// (AES-GCM decrypt).
func resolveAPIKeys(models []*ModelConfig, configDir string) error {
cr := credential.NewResolver(configDir)
for i := range models {
// Resolve APIKeys array
for j, key := range models[i].apiKeys {
resolved, err := cr.Resolve(key)
if err != nil {
return fmt.Errorf(
"model_list[%d] (%s): api_keys[%d]: %w",
i,
models[i].ModelName,
j,
err,
)
}
models[i].apiKeys[j] = resolved
}
}
return nil
}
func (c *Config) migrateChannelConfigs() {
// Discord: mention_only -> group_trigger.mention_only
if c.Channels.Discord.MentionOnly && !c.Channels.Discord.GroupTrigger.MentionOnly {
c.Channels.Discord.GroupTrigger.MentionOnly = true
}
// OneBot: group_trigger_prefix -> group_trigger.prefixes
if len(c.Channels.OneBot.GroupTriggerPrefix) > 0 &&
len(c.Channels.OneBot.GroupTrigger.Prefixes) == 0 {
c.Channels.OneBot.GroupTrigger.Prefixes = c.Channels.OneBot.GroupTriggerPrefix
}
}
func SaveConfig(path string, cfg *Config) error {
if cfg.Version < CurrentVersion {
cfg.Version = CurrentVersion

View file

@ -219,12 +219,12 @@ func (s *SecureString) UnmarshalJSON(value []byte) error {
}
func (s SecureString) MarshalYAML() (any, error) {
// Preserve raw value if it is already a reference (enc:// or file://)
if strings.HasPrefix(s.raw, credential.EncScheme) || strings.HasPrefix(s.raw, credential.FileScheme) {
// Preserve raw value if it is already a reference (enc://, file://, or env://)
if strings.HasPrefix(s.raw, credential.EncScheme) || strings.HasPrefix(s.raw, credential.FileScheme) || strings.HasPrefix(s.raw, "env://") {
return s.raw, nil
}
// If resolved is a reference format (e.g. set via Set), copy back to raw
if strings.HasPrefix(s.resolved, credential.EncScheme) || strings.HasPrefix(s.resolved, credential.FileScheme) {
if strings.HasPrefix(s.resolved, credential.EncScheme) || strings.HasPrefix(s.resolved, credential.FileScheme) || strings.HasPrefix(s.resolved, "env://") {
s.raw = s.resolved
return s.raw, nil
}