airshipctl/pkg/secret/sops/sops.go
uday.ruddarraju 9a608de653 Encrypt and decrypt using sops
Design document: https://docs.google.com/document/d/1EjiCuXoiy8DEEXe15KxVJ4iWrwogCyG113_0LdzcWzQ/edit?usp=drive_web&ouid=102644738301620637153

Demo readme: https://hackmd.io/@WE7PUWXBRVeQJzCZkXkOLw/ryoW-aOLv

This patchset comprises of:
- package library to interact with sops
- integrate airshipctl encrypt/decrypt with sops

Change-Id: I2ca3ff3c8661d146708084728cb3f87365a4f39e
2020-10-23 02:27:45 -07:00

333 lines
8.5 KiB
Go

/*
Licensed 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
https://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 sops
import (
"bufio"
"bytes"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"time"
"go.mozilla.org/sops/v3"
"go.mozilla.org/sops/v3/aes"
"go.mozilla.org/sops/v3/cmd/sops/common"
"go.mozilla.org/sops/v3/keys"
"go.mozilla.org/sops/v3/keyservice"
"go.mozilla.org/sops/v3/pgp"
"golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/packet"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/yaml"
"opendev.org/airship/airshipctl/pkg/k8s/client"
)
const (
tempEncryptionKeyFile = "/tmp/encryption-key.pri"
encryptionRegexAnnotationKey = "airshipit.org/encryption-regex"
encryptionFilterAnnotationKey = "airshipit.org/encrypt"
)
// Options holds the key information used to encrypt and decrypt secrets using Sops
type Options struct {
KeySecretName string
KeySecretNamespace string
EncryptionKeyPath string
DecryptionKeyPath string
}
// Client is an interface that is used to encrypt and decrypt secrets
type Client interface {
// Encrypt reads plain text secrets from srcPath and writes encrypted secrets to dstPath
Encrypt(srcPath string, dstPath string) ([]byte, error)
// Decrypt reads encrypted secrets from srcPath and writes plain text secrets to dstPath
Decrypt(srcPath string, dstPath string) ([]byte, error)
}
// localGpg implements Client with local gpg keys for encryption and decryption
type localGpg struct {
*Options
kclient client.Interface
publicKey []byte
privateKey []byte
}
// NewClient returns a localGpg Client implementation
func NewClient(kclient client.Interface, options *Options) (Client, error) {
client := &localGpg{
kclient: kclient,
Options: options,
}
err := client.initializeKeys()
return client, err
}
func (lg *localGpg) initializeKeys() error {
var publicKey, privateKey []byte
var err error
if lg.DecryptionKeyPath == "" && lg.EncryptionKeyPath == "" {
// retrieve sops keys from the apiserver
if lg.kclient == nil {
return fmt.Errorf("kube client not initialized")
}
secret, apiErr := lg.getSecretFromAPI(lg.KeySecretName, lg.KeySecretNamespace)
if apiErr != nil {
return err
}
privateKey = secret.Data["pri_key"]
publicKey = secret.Data["pub_key"]
} else {
// load the keys from disk
if lg.DecryptionKeyPath != "" {
privateKey, err = ioutil.ReadFile(lg.DecryptionKeyPath)
if err != nil {
return err
}
}
if lg.EncryptionKeyPath != "" {
publicKey, err = ioutil.ReadFile(lg.EncryptionKeyPath)
if err != nil {
return err
}
}
}
lg.publicKey = publicKey
lg.privateKey = privateKey
if len(lg.privateKey) > 0 {
// import the key locally
if err := lg.importGpgKeyPairLocally(); err != nil {
return err
}
}
return nil
}
func (lg *localGpg) importGpgKeyPairLocally() error {
tmpPriKeyFileName := fmt.Sprintf(tempEncryptionKeyFile)
if err := writeFile(tmpPriKeyFileName, lg.privateKey); err != nil {
return err
}
defer func() {
os.Remove(tmpPriKeyFileName)
}()
gpgCmd := exec.Command("gpg", "--import", tmpPriKeyFileName)
err := gpgCmd.Run()
if err != nil {
return err
}
// gpg --export-secret-keys >~/.gnupg/secring.gpg
// make this work with gpg1 as well for linux
homeDir, err := os.UserHomeDir()
if err != nil {
return err
}
gpgSecretImportCmd := exec.Command("gpg", "--export-secret-keys")
secringBytes, err := gpgSecretImportCmd.Output()
if err != nil {
return err
}
err = ioutil.WriteFile(filepath.Join(homeDir, ".gnupg", "secring.gpg"), secringBytes, 0600)
return err
}
func (lg *localGpg) Encrypt(fromFile string, toFile string) ([]byte, error) {
groups, err := lg.getKeyGroup(lg.publicKey)
if err != nil {
return nil, err
}
store := common.DefaultStoreForPath(fromFile)
fileBytes, err := ioutil.ReadFile(fromFile)
if err != nil {
return nil, fmt.Errorf("error reading file: %s", err)
}
branches, err := store.LoadPlainFile(fileBytes)
if err != nil {
return nil, err
}
if err = lg.ensureNoMetadata(branches[0]); err != nil {
// do not return error to keep this function idempotent
// ensureNoMetadata will return an error if the file is already encrypted
return nil, nil
}
// get encryption regex
encryptionRegex, err := getEncryptionRegex(fileBytes)
if err != nil || encryptionRegex == "" {
encryptionRegex = "^data"
} else if encryptionRegex != "" {
encryptionRegex = "^data|" + encryptionRegex
}
tree := sops.Tree{
Branches: branches,
Metadata: sops.Metadata{
KeyGroups: groups,
Version: "3.6.0",
EncryptedRegex: encryptionRegex,
},
FilePath: fromFile,
}
keySvc := keyservice.NewLocalClient()
dataKey, errors := tree.GenerateDataKeyWithKeyServices([]keyservice.KeyServiceClient{keySvc})
if len(errors) > 0 {
return nil, fmt.Errorf("%s", errors)
}
if err = common.EncryptTree(common.EncryptTreeOpts{
Tree: &tree,
Cipher: aes.NewCipher(),
DataKey: dataKey,
}); err != nil {
return nil, err
}
dstStore := common.DefaultStoreForPath(toFile)
output, err := dstStore.EmitEncryptedFile(tree)
if err != nil {
return nil, err
}
if toFile != "" {
err = ioutil.WriteFile(toFile, output, 0600)
if err != nil {
return nil, err
}
}
return output, nil
}
func (lg *localGpg) Decrypt(fromFile string, toFile string) ([]byte, error) {
keySvc := keyservice.NewLocalClient()
tree, err := common.LoadEncryptedFileWithBugFixes(common.GenericDecryptOpts{
Cipher: aes.NewCipher(),
InputStore: common.DefaultStoreForPath(fromFile),
InputPath: fromFile,
KeyServices: []keyservice.KeyServiceClient{keySvc},
})
if err != nil && err.Error() == sops.MetadataNotFound.Error() {
return nil, nil
} else if err != nil {
return nil, err
}
if _, err = common.DecryptTree(common.DecryptTreeOpts{
Tree: tree,
KeyServices: []keyservice.KeyServiceClient{keySvc},
Cipher: aes.NewCipher(),
}); err != nil {
return nil, err
}
dstStore := common.DefaultStoreForPath(toFile)
output, err := dstStore.EmitPlainFile(tree.Branches)
if err != nil {
return nil, err
}
if toFile != "" {
if err = writeFile(toFile, output); err != nil {
return nil, err
}
}
return output, nil
}
// Config for generating keys.
type Config struct {
packet.Config
// Expiry is the duration that the generated key will be valid for.
Expiry time.Duration
}
// Key represents an OpenPGP key.
type Key struct {
openpgp.Entity
}
func (lg *localGpg) getSecretFromAPI(name string, namespace string) (*corev1.Secret, error) {
return lg.kclient.ClientSet().CoreV1().Secrets(namespace).Get(name, metav1.GetOptions{})
}
func (lg *localGpg) getKeyGroup(publicKeyBytes []byte) ([]sops.KeyGroup, error) {
b := bytes.NewReader(publicKeyBytes)
bufferedReader := bufio.NewReader(b)
entities, err := openpgp.ReadArmoredKeyRing(bufferedReader)
if err != nil {
return nil, err
}
fingerprint := fmt.Sprintf("%X", entities[0].PrimaryKey.Fingerprint[:])
pgpKeys := make([]keys.MasterKey, 1)
for index, k := range pgp.MasterKeysFromFingerprintString(fingerprint) {
pgpKeys[index] = k
}
var group sops.KeyGroup
group = append(group, pgpKeys...)
return []sops.KeyGroup{group}, nil
}
func (lg *localGpg) ensureNoMetadata(branch sops.TreeBranch) error {
for _, b := range branch {
if b.Key == "sops" {
return fmt.Errorf("file already encrypted")
}
}
return nil
}
func writeFile(path string, content []byte) error {
return ioutil.WriteFile(path, content, 0600)
}
func getEncryptionRegex(yamlContent []byte) (string, error) {
jsonContents, err := yaml.ToJSON(yamlContent)
if err != nil {
return "", err
}
object, err := runtime.Decode(unstructured.UnstructuredJSONScheme, jsonContents)
if err != nil {
return "", err
}
accessor, err := meta.Accessor(object)
if err != nil {
return "", err
}
if accessor.GetAnnotations() != nil &&
accessor.GetAnnotations()[encryptionFilterAnnotationKey] == "true" &&
accessor.GetAnnotations()[encryptionRegexAnnotationKey] != "" {
return accessor.GetAnnotations()[encryptionRegexAnnotationKey], nil
}
return "", nil
}