Files
coder/cli/sessionstore/sessionstore_windows.go
T
Zach 139dab7cfe feat(cli): optionally store session token in OS keyring (#20256)
This change implements optional secure storage of the CLI token using the operating system
 keyring for Windows, with groundwork laid for macOS in a future change. Previously, the
 Coder CLI stored authentication tokens in plaintext configuration files, which posed a
 security risk because users' tokens are stored unencrypted and can be easily accessed by
 other processes or users with file system access.

The keyring is opt-in to preserve compatibility with applications (like the JetBrains
Toolbox plugin, VS code plugin, etc). Users can opt into keyring use with a new
`--use-keyring` flag.

The secure storage is platform dependent. Windows Credential Manager API is used on Windows.
The session token continues to be stored in plain text on macOS and Linux. macOS is omitted
for now while we figure out the best path forward for compatibility with apps like Coder Desktop.

https://www.notion.so/coderhq/CLI-Session-Token-in-OS-Keyring-293d579be592808b8b7fd235304e50d5

https://github.com/coder/coder/issues/19403
2025-10-30 17:41:08 -06:00

67 lines
1.6 KiB
Go

//go:build windows
package sessionstore
import (
"errors"
"os"
"syscall"
"github.com/danieljoos/wincred"
)
const (
// defaultServiceName is the service name used in the Windows Credential Manager
// for storing Coder CLI session tokens.
defaultServiceName = "coder-v2-credentials"
)
// operatingSystemKeyring implements keyringProvider and uses Windows Credential Manager.
// It is largely adapted from the zalando/go-keyring package.
type operatingSystemKeyring struct{}
func (operatingSystemKeyring) Set(service, credential string) error {
// password may not exceed 2560 bytes (https://github.com/jaraco/keyring/issues/540#issuecomment-968329967)
if len(credential) > 2560 {
return ErrSetDataTooBig
}
// service may not exceed 512 bytes (might need more testing)
if len(service) >= 512 {
return ErrSetDataTooBig
}
// service may not exceed 32k but problems occur before that
// so we limit it to 30k
if len(service) > 1024*30 {
return ErrSetDataTooBig
}
cred := wincred.NewGenericCredential(service)
cred.CredentialBlob = []byte(credential)
cred.Persist = wincred.PersistLocalMachine
return cred.Write()
}
func (operatingSystemKeyring) Get(service string) ([]byte, error) {
cred, err := wincred.GetGenericCredential(service)
if err != nil {
if errors.Is(err, syscall.ERROR_NOT_FOUND) {
return nil, os.ErrNotExist
}
return nil, err
}
return cred.CredentialBlob, nil
}
func (operatingSystemKeyring) Delete(service string) error {
cred, err := wincred.GetGenericCredential(service)
if err != nil {
if errors.Is(err, syscall.ERROR_NOT_FOUND) {
return os.ErrNotExist
}
return err
}
return cred.Delete()
}