keystore/file_keystore.go (263 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package keystore import ( "bytes" "crypto/rand" "encoding/base64" "encoding/json" "fmt" "io" "os" "path/filepath" "runtime" "sync" "github.com/elastic/elastic-agent-libs/config" "github.com/elastic/elastic-agent-libs/file" ) const ( filePermission = 0600 // Encryption Related constants iVLength = 12 saltLength = 64 iterationsCount = 10000 keyLength = 32 ) // Packager defines a keystore that we can read the raw bytes and be packaged in an artifact. type Packager interface { Package() ([]byte, error) ConfiguredPath() string } // FileKeystore Allows to store key / secrets pair securely into an encrypted local file. type FileKeystore struct { sync.RWMutex Path string secrets map[string]serializableSecureString dirty bool password *SecureString isStrictPerms bool } // Allow the original SecureString type to be correctly serialized to json. type serializableSecureString struct { *SecureString Value []byte `json:"value"` } // Factory Create the right keystore with the configured options. func Factory(c *config.C, defaultPath string, strictPerms bool) (Keystore, error) { cfg := defaultConfig() if c == nil { c = config.NewConfig() } err := c.Unpack(&cfg) if err != nil { return nil, fmt.Errorf("could not read keystore configuration, err: %w", err) } if cfg.Path == "" { cfg.Path = defaultPath } keystore, err := NewFileKeystoreWithStrictPerms(cfg.Path, strictPerms) return keystore, err } // NewFileKeystore returns an new File based keystore or an error, currently users cannot set their // own password on the keystore, the default password will be an empty string. When the keystore // is initialized the secrets are automatically loaded into memory. func NewFileKeystore(keystoreFile string) (Keystore, error) { return NewFileKeystoreWithStrictPerms(keystoreFile, false) } // NewFileKeystore returns an new File based keystore or an error, currently users cannot set their // own password on the keystore, the default password will be an empty string. When the keystore // is initialized the secrets are automatically loaded into memory. func NewFileKeystoreWithStrictPerms(keystoreFile string, strictPerms bool) (Keystore, error) { return NewFileKeystoreWithPasswordAndStrictPerms(keystoreFile, NewSecureString([]byte("")), strictPerms) } // NewFileKeystoreWithPassword return a new File based keystore or an error, allow to define what // password to use to create the keystore. func NewFileKeystoreWithPasswordAndStrictPerms(keystoreFile string, password *SecureString, strictPerms bool) (Keystore, error) { keystore := FileKeystore{ Path: keystoreFile, dirty: false, password: password, secrets: make(map[string]serializableSecureString), isStrictPerms: strictPerms, } err := keystore.load() if err != nil { return nil, err } return &keystore, nil } // NewFileKeystoreWithPassword return a new File based keystore or an error, allow to define what // password to use to create the keystore. func NewFileKeystoreWithPassword(keystoreFile string, password *SecureString) (Keystore, error) { return NewFileKeystoreWithPasswordAndStrictPerms(keystoreFile, password, false) } // Retrieve return a SecureString instance that will contains both the key and the secret. func (k *FileKeystore) Retrieve(key string) (*SecureString, error) { k.RLock() defer k.RUnlock() secret, ok := k.secrets[key] if !ok { return nil, ErrKeyDoesntExists } return NewSecureString(secret.Value), nil } // Store add the key pair to the secret store and mark the store as dirty. func (k *FileKeystore) Store(key string, value []byte) error { k.Lock() defer k.Unlock() k.secrets[key] = serializableSecureString{Value: value} k.dirty = true return nil } // Delete an existing key from the store and mark the store as dirty. func (k *FileKeystore) Delete(key string) error { k.Lock() defer k.Unlock() delete(k.secrets, key) k.dirty = true return nil } // Save persists the in memory data to disk if needed. func (k *FileKeystore) Save() error { k.Lock() err := k.doSave(true) k.Unlock() return err } // List return the availables keys. func (k *FileKeystore) List() ([]string, error) { k.RLock() defer k.RUnlock() keys := make([]string, 0, len(k.secrets)) for key := range k.secrets { keys = append(keys, key) } return keys, nil } // GetConfig returns config.C representation of the key / secret pair to be merged with other // loaded configuration. func (k *FileKeystore) GetConfig() (*config.C, error) { k.RLock() defer k.RUnlock() configHash := make(map[string]interface{}) for key, secret := range k.secrets { configHash[key] = string(secret.Value) } return config.NewConfigFrom(configHash) } // Create create an empty keystore, if the store already exist we will return an error. func (k *FileKeystore) Create(override bool) error { k.Lock() k.secrets = make(map[string]serializableSecureString) k.dirty = true err := k.doSave(override) k.Unlock() return err } // IsPersisted return if the keystore is physically persisted on disk. func (k *FileKeystore) IsPersisted() bool { k.Lock() defer k.Unlock() // We just check if the file is present on disk, we don't need to do any validation // for a file based keystore, since all the keys will be fetched when we initialize the object // if the file is invalid it will already fails. Creating a new FileKeystore will raise // any errors concerning the permissions f, err := os.OpenFile(k.Path, os.O_RDONLY, filePermission) if err != nil { return false } f.Close() return true } // doSave lock/unlocking of the resource need to be done by the caller. func (k *FileKeystore) doSave(override bool) error { if !k.dirty { return nil } temporaryPath := fmt.Sprintf("%s.tmp", k.Path) w := new(bytes.Buffer) jsonEncoder := json.NewEncoder(w) if err := jsonEncoder.Encode(k.secrets); err != nil { return fmt.Errorf("cannot serialize the keystore before saving it to disk: %w", err) } encrypted, err := k.encrypt(w) if err != nil { return fmt.Errorf("cannot encrypt the keystore: %w", err) } flags := os.O_RDWR | os.O_CREATE if override { flags |= os.O_TRUNC } else { flags |= os.O_EXCL } f, err := os.OpenFile(temporaryPath, flags, filePermission) if err != nil { return fmt.Errorf("cannot open file to save the keystore to '%s', error: %w", k.Path, err) } _, _ = f.Write(version) base64Encoder := base64.NewEncoder(base64.StdEncoding, f) _, _ = io.Copy(base64Encoder, encrypted) base64Encoder.Close() _ = f.Sync() f.Close() err = file.SafeFileRotate(k.Path, temporaryPath) if err != nil { os.Remove(temporaryPath) return fmt.Errorf("cannot replace the existing keystore, with the new keystore file at '%s', error: %w", k.Path, err) } os.Remove(temporaryPath) k.dirty = false return nil } func (k *FileKeystore) loadRaw() ([]byte, error) { raw, err := os.ReadFile(k.Path) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } if k.isStrictPerms { if err := k.checkPermissions(k.Path); err != nil { return nil, err } } v := raw[0:len(version)] if !bytes.Equal(v, version) { return nil, fmt.Errorf("keystore format doesn't match expected version: '%s' got '%s'", version, v) } if len(raw) <= len(version) { return nil, fmt.Errorf("corrupt or empty keystore") } return raw, nil } func (k *FileKeystore) load() error { k.Lock() defer k.Unlock() raw, err := k.loadRaw() if err != nil { return err } if len(raw) == 0 { return nil } base64Decoder := base64.NewDecoder(base64.StdEncoding, bytes.NewReader(raw[len(version):])) plaintext, err := k.decrypt(base64Decoder) if err != nil { return fmt.Errorf("could not decrypt the keystore: %w", err) } jsonDecoder := json.NewDecoder(plaintext) return jsonDecoder.Decode(&k.secrets) } // checkPermission enforces permission on the keystore file itself, the file should have strict // permission (0600) and the keystore should refuses to start if its not the case. func (k *FileKeystore) checkPermissions(f string) error { if runtime.GOOS == "windows" { //nolint: goconst // OS checking in cleaner this way return nil } info, err := file.Stat(f) if err != nil { return err } euid := os.Geteuid() fileUID, _ := info.UID() perm := info.Mode().Perm() if fileUID != 0 && euid != fileUID { return fmt.Errorf(`config file ("%v") must be owned by the user identifier `+ `(uid=%v) or root`, f, euid) } // Test if group or other have write permissions. if perm != filePermission { nameAbs, err := filepath.Abs(f) if err != nil { nameAbs = f } return fmt.Errorf(`file ("%v") can only be writable and readable by the `+ `owner but the permissions are "%v" (to fix the permissions use: `+ `'chmod go-wrx %v')`, f, perm, nameAbs) } return nil } // Package returns the bytes of the encrypted keystore. func (k *FileKeystore) Package() ([]byte, error) { k.Lock() defer k.Unlock() return k.loadRaw() } // ConfiguredPath returns the path to the keystore. func (k *FileKeystore) ConfiguredPath() string { return k.Path } // randomBytes return a slice of random bytes of the defined length func randomBytes(length int) ([]byte, error) { r := make([]byte, length) _, err := rand.Read(r) if err != nil { return nil, err } return r, nil }