package admin
import (
"crypto/rand"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"unicode"
"unicode/utf8"
"github.com/openshift/origin/pkg/cmd/util/term"
"github.com/spf13/cobra"
kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
configapi "github.com/openshift/origin/pkg/cmd/server/api"
"github.com/openshift/origin/pkg/cmd/templates"
pemutil "github.com/openshift/origin/pkg/cmd/util/pem"
)
const EncryptCommandName = "encrypt"
type EncryptOptions struct {
// CleartextFile contains cleartext data to encrypt.
CleartextFile string
// CleartextData is cleartext data to encrypt.
CleartextData []byte
// CleartextReader reads cleartext data to encrypt if CleartextReader and CleartextFile are unspecified.
CleartextReader io.Reader
// EncryptedFile has encrypted data written to it.
EncryptedFile string
// EncryptedWriter has encrypted data written to it if EncryptedFile is unspecified.
EncryptedWriter io.Writer
// KeyFile contains the password in PEM format (as previously written by GenKeyFile)
KeyFile string
// GenKeyFile indicates a key should be generated and written
GenKeyFile string
// PromptWriter is used to write status and prompt messages
PromptWriter io.Writer
}
var encryptExample = templates.Examples(`
# Encrypt the content of secret.txt with a generated key:
%[1]s --genkey=secret.key --in=secret.txt --out=secret.encrypted
# Encrypt the content of secret2.txt with an existing key:
%[1]s --key=secret.key < secret2.txt > secret2.encrypted`)
func NewCommandEncrypt(commandName string, fullName string, out io.Writer, errout io.Writer) *cobra.Command {
options := &EncryptOptions{
CleartextReader: os.Stdin,
EncryptedWriter: out,
PromptWriter: errout,
}
cmd := &cobra.Command{
Use: commandName,
Short: "Encrypt data with AES-256-CBC encryption",
Example: fmt.Sprintf(encryptExample, fullName),
Run: func(cmd *cobra.Command, args []string) {
kcmdutil.CheckErr(options.Validate(args))
kcmdutil.CheckErr(options.Encrypt())
},
}
flags := cmd.Flags()
flags.StringVar(&options.CleartextFile, "in", options.CleartextFile, "File containing the data to encrypt. Read from stdin if omitted.")
flags.StringVar(&options.EncryptedFile, "out", options.EncryptedFile, "File to write the encrypted data to. Written to stdout if omitted.")
flags.StringVar(&options.KeyFile, "key", options.KeyFile, "File containing the encrypting key from in the format written by --genkey.")
flags.StringVar(&options.GenKeyFile, "genkey", options.GenKeyFile, "File to write a randomly generated key to.")
// autocompletion hints
cmd.MarkFlagFilename("in")
cmd.MarkFlagFilename("out")
cmd.MarkFlagFilename("key")
cmd.MarkFlagFilename("genkey")
return cmd
}
func (o *EncryptOptions) Validate(args []string) error {
if len(args) != 0 {
return errors.New("no arguments are supported")
}
if len(o.CleartextFile) == 0 && len(o.CleartextData) == 0 && o.CleartextReader == nil {
return errors.New("an input file, data, or reader is required")
}
if len(o.CleartextFile) > 0 && len(o.CleartextData) > 0 {
return errors.New("cannot specify both an input file and data")
}
if len(o.EncryptedFile) == 0 && o.EncryptedWriter == nil {
return errors.New("an output file or writer is required")
}
if len(o.GenKeyFile) > 0 && len(o.KeyFile) > 0 {
return errors.New("either --genkey or --key may be specified, not both")
}
if len(o.GenKeyFile) == 0 && len(o.KeyFile) == 0 {
return errors.New("--genkey or --key is required")
}
return nil
}
func (o *EncryptOptions) Encrypt() error {
// Get data
var data []byte
var warnWhitespace = true
switch {
case len(o.CleartextFile) > 0:
if d, err := ioutil.ReadFile(o.CleartextFile); err != nil {
return err
} else {
data = d
}
case len(o.CleartextData) > 0:
// Don't warn in cases where we're explicitly being given the data to use
warnWhitespace = false
data = o.CleartextData
case o.CleartextReader != nil && term.IsTerminalReader(o.CleartextReader) && o.PromptWriter != nil:
// Read a single line from stdin with prompting
data = []byte(term.PromptForString(o.CleartextReader, o.PromptWriter, "Data to encrypt: "))
case o.CleartextReader != nil:
// Read data from stdin without prompting (allows binary data and piping)
if d, err := ioutil.ReadAll(o.CleartextReader); err != nil {
return err
} else {
data = d
}
}
if warnWhitespace && (o.PromptWriter != nil) && (len(data) > 0) {
r1, _ := utf8.DecodeRune(data)
r2, _ := utf8.DecodeLastRune(data)
if unicode.IsSpace(r1) || unicode.IsSpace(r2) {
fmt.Fprintln(o.PromptWriter, "Warning: Data includes leading or trailing whitespace, which will be included in the encrypted value")
}
}
// Get key
var key []byte
switch {
case len(o.KeyFile) > 0:
if block, ok, err := pemutil.BlockFromFile(o.KeyFile, configapi.StringSourceKeyBlockType); err != nil {
return err
} else if !ok {
return fmt.Errorf("%s does not contain a valid PEM block of type %q", o.KeyFile, configapi.StringSourceKeyBlockType)
} else if len(block.Bytes) == 0 {
return fmt.Errorf("%s does not contain a key", o.KeyFile)
} else {
key = block.Bytes
}
case len(o.GenKeyFile) > 0:
key = make([]byte, 32)
if _, err := rand.Read(key); err != nil {
return err
}
}
if len(key) == 0 {
return errors.New("--genkey or --key is required")
}
// Encrypt
dataBlock, err := x509.EncryptPEMBlock(rand.Reader, configapi.StringSourceEncryptedBlockType, data, key, x509.PEMCipherAES256)
if err != nil {
return err
}
// Write data
if len(o.EncryptedFile) > 0 {
if err := pemutil.BlockToFile(o.EncryptedFile, dataBlock, os.FileMode(0644)); err != nil {
return err
}
} else if o.EncryptedWriter != nil {
encryptedBytes, err := pemutil.BlockToBytes(dataBlock)
if err != nil {
return err
}
n, err := o.EncryptedWriter.Write(encryptedBytes)
if err != nil {
return err
}
if n != len(encryptedBytes) {
return fmt.Errorf("could not completely write encrypted data")
}
}
// Write key
if len(o.GenKeyFile) > 0 {
keyBlock := &pem.Block{Bytes: key, Type: configapi.StringSourceKeyBlockType}
if err := pemutil.BlockToFile(o.GenKeyFile, keyBlock, os.FileMode(0600)); err != nil {
return err
}
}
return nil
}