Caching credentials using the Linux keyring in Go
version 0.1, 2019-12-23
This post describes how to cache credentials for your Go (and by proxy bash) scripts.
Definitions
- Keyring
-
In cryptography, a keyring stores known encryption keys (and, in some cases, passwords).
- Difference between the Gnome Keyring and the Linux Kernel Keyring
-
Basically, The Gnome Keyring actually stores creds, while Linux Caches them.
- keyutils - keyctl
-
key management facility control.
keyctl is very useful while playing with this. For example:
$ keyctl show @s Keyring 823180767 --alswrv 1000 65534 keyring: _uid_ses.1000 109050238 --alswrv 1000 65534 \_ keyring: _uid.1000
$ keyctl show @u Keyring 109050238 --alswrv 1000 65534 keyring: _uid.1000
A Go library
There seems to be lots of alternative Go libraries to work with the different keyrings. I landed on Jesse Sipprell's keyctl since it focuses on the Linux keyring only, it has no external dependencies and the code is easy to parse.
import (
...
"github.com/jsipprell/keyctl"
)
The library requires creating a keyring session first:
// Create session
keyring, err := keyctl.UserSessionKeyring()
if err != nil {
return nil, fmt.Errorf("couldn't create keyring session: %w", err)
}
Then you can save a key with a given timeout in seconds:
// Store key
keyring.SetDefaultTimeout(timeoutSeconds)
key, err := keyring.Add(name, []byte(password))
if err != nil {
return fmt.Errorf("couldn't store '%s': %s", name, err)
}
info, _ := key.Info()
logger.Printf("key: %+v", info)
To refresh the timer, simply save again. To invalidate a key, you can save it with a 1 second timeout and it will expire.
Finally, to retrieve it:
// Retrieve
key, err := keyring.Search(name)
if err == nil {
data, err := key.Get()
if err != nil {
return nil, fmt.Errorf("couldn't retrieve key data: %w", err)
}
info, _ := key.Info()
logger.Printf("key: %+v", info)
return data, nil
}
Piecing it together
package main
import (
"fmt"
"io/ioutil"
"log"
"os"
"syscall"
"github.com/DavidGamba/go-getoptions"
"github.com/jsipprell/keyctl"
"golang.org/x/crypto/ssh/terminal"
)
var logger = log.New(ioutil.Discard, "", log.LstdFlags)
func main() {
var timeout int
opt := getoptions.New()
opt.Self("", `Saves/Retrieves passwords from/to the Linux keyring.
It will cache the password for a given timeout.
If a password doesn't exist in the keyring it will prompt the user for one.`)
opt.Bool("help", false, opt.Alias("?"))
opt.Bool("debug", false)
opt.Bool("print", false, opt.Description("Print password to STDOUT"))
opt.IntVar(&timeout, "timeout", 900, opt.ArgName("seconds"),
opt.Description("Timeout in seconds, default 15 minutes"))
opt.HelpSynopsisArgs("<key-name>")
remaining, err := opt.Parse(os.Args[1:])
if opt.Called("help") {
fmt.Println(opt.Help())
os.Exit(1)
}
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: %s\n", err)
os.Exit(1)
}
if opt.Called("debug") {
logger.SetOutput(os.Stderr)
}
logger.Println(remaining)
if len(remaining) < 1 {
fmt.Fprintf(os.Stderr, "ERROR: Missing key\n")
os.Exit(1)
}
keyName := remaining[0]
// Retrieve existing password or ask user to add one
data, err := GetPassword(keyName)
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: %s\n", err)
os.Exit(1)
}
err = CachePassword(keyName, string(data), uint(timeout))
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: %s\n", err)
}
if opt.Called("print") {
fmt.Printf("%s", data)
}
}
// GetPassword - Gets a secret from the User Session Keyring.
// If the key doesn't exist, it asks the user to enter the password value.
// It will cache the secret for a given number of seconds.
func GetPassword(name string) ([]byte, error) {
// Create session
keyring, err := keyctl.UserSessionKeyring()
if err != nil {
return nil, fmt.Errorf("couldn't create keyring session: %w", err)
}
// Retrieve
key, err := keyring.Search(name)
if err == nil {
data, err := key.Get()
if err != nil {
return nil, fmt.Errorf("couldn't retrieve key data: %w", err)
}
info, _ := key.Info()
logger.Printf("key: %+v", info)
return data, nil
}
// If not found promt user
fmt.Printf("Enter password for '%s': ", name)
password, err := terminal.ReadPassword(int(syscall.Stdin))
fmt.Println()
if err != nil {
return nil, fmt.Errorf("failed to read password: %w", err)
}
return password, nil
}
// CachePassword - Saves a secret to the User Session Keyring.
// It will cache the secret for a given number of seconds.
//
// To invalidate a password, save it with a 1 second timeout.
func CachePassword(name, password string, timeoutSeconds uint) error {
// Create session
keyring, err := keyctl.UserSessionKeyring()
if err != nil {
return fmt.Errorf("couldn't create keyring session: %w", err)
}
// Store key
keyring.SetDefaultTimeout(timeoutSeconds)
key, err := keyring.Add(name, []byte(password))
if err != nil {
return fmt.Errorf("couldn't store '%s': %s", name, err)
}
info, _ := key.Info()
logger.Printf("key: %+v", info)
return nil
}
Bash
The above Go script can be called from bash, it will handle the user prompt.
#!/bin/bash
# Allow interactive operation
./password-cache mykey -t 60
if [[ $? == 0 ]]; then
# Read from store
password=$(./password-cache mykey -t 60 --print)
# Use
echo "|$password|"
fi
Then we call it the first time:
$ bash bash-script.sh Enter 'mykey' password: |password|
The second time around it just retrieves the key as expected:
$ bash bash-script.sh |password|
$ keyctl list @us 2 keys in keyring: 109050238: --alswrv 1000 65534 keyring: _uid.1000 147013626: --alswrv 1000 1000 user: mykey
$ keyctl print 147013626 password