| package gpg |
| |
| import ( |
| "context" |
| "encoding/hex" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "os" |
| "os/exec" |
| "path" |
| "strings" |
| "time" |
| ) |
| |
| var ExecutionTimeLimit = 30 * time.Second |
| var BinaryPath = "gpg" |
| |
| type Encryptor interface { |
| ReadCipherText(size int) ([]byte, error) |
| WritePlainText(chunk []byte) error |
| Finish() |
| Close() |
| } |
| |
| type EncryptorFactory interface { |
| Get(recipient []byte, keyRing []byte) (Encryptor, error) |
| } |
| |
| type CLIEncryptorFactory struct { |
| } |
| |
| func (CLIEncryptorFactory) Get(recipient []byte, keyRing []byte) (Encryptor, error) { |
| return NewCLIEncryptor(recipient, keyRing) |
| } |
| |
| type CLIEncryptor struct { |
| tempDir string |
| recipient []byte |
| cmd *exec.Cmd |
| stdout io.ReadCloser |
| stderr io.ReadCloser |
| stdin io.WriteCloser |
| initialized bool |
| } |
| |
| func NewCLIEncryptor(recipient []byte, keyRing []byte) (*CLIEncryptor, error) { |
| temp, err := getGpgTempDir() |
| if err != nil { |
| return nil, err |
| } |
| |
| keyRingTempPath := path.Join(temp, "_keyring") |
| err = ioutil.WriteFile(keyRingTempPath, keyRing, 0600) |
| if err != nil { |
| return nil, err |
| } |
| |
| // TODO(lb5tr): test for command injection |
| importParams := []string{ |
| "--homedir", temp, |
| "--import", keyRingTempPath, |
| } |
| |
| ctx, cancel := context.WithTimeout(context.Background(), ExecutionTimeLimit) |
| defer cancel() |
| |
| cmd := exec.CommandContext(ctx, BinaryPath, importParams...) |
| err = cmd.Start() |
| if err != nil { |
| return nil, fmt.Errorf("failed to start import command: %v", err) |
| } |
| |
| err = cmd.Wait() |
| if err != nil { |
| return nil, fmt.Errorf("workspace initialization failed: %v", err) |
| } |
| |
| ok, err := isOkExitCode(cmd) |
| if !ok || err != nil { |
| return nil, fmt.Errorf("failed to initialize gpg workspace due to gpg execution failure") |
| } |
| |
| // spawn background encryption process |
| // TODO(lb5tr): test for command injection |
| encryptParams := []string{ |
| "--encrypt", |
| "--homedir", temp, |
| "--recipient", strings.ToUpper(hex.EncodeToString(recipient)), |
| "--trust-model", "always", |
| "--yes", |
| "--batch", |
| } |
| |
| cmd = exec.Command(BinaryPath, encryptParams...) |
| stdout, stderr, stdin, err := makePipes(cmd) |
| if err != nil { |
| return nil, err |
| } |
| |
| err = cmd.Start() |
| if err != nil { |
| return nil, fmt.Errorf("failed to start encryptor process: %v", err) |
| } |
| |
| encryptor := CLIEncryptor{ |
| tempDir: temp, |
| recipient: recipient, |
| cmd: cmd, |
| stdin: stdin, |
| stderr: stderr, |
| stdout: stdout, |
| initialized: true, |
| } |
| |
| return &encryptor, nil |
| } |
| |
| func (encryptor *CLIEncryptor) WritePlainText(chunk []byte) error { |
| if !encryptor.initialized { |
| return fmt.Errorf("encryptor is not initialized") |
| } |
| |
| encryptor.stdin.Write(chunk) |
| return nil |
| } |
| |
| func (encryptor *CLIEncryptor) ReadCipherText(size int) ([]byte, error) { |
| if !encryptor.initialized { |
| return nil, fmt.Errorf("encryptor is not initialized") |
| } |
| |
| buf := make([]byte, size) |
| n, err := encryptor.stdout.Read(buf) |
| |
| return buf[:n], err |
| } |
| |
| func (encryptor *CLIEncryptor) Finish() { |
| encryptor.stdin.Close() |
| } |
| |
| func (encryptor *CLIEncryptor) Close() { |
| if encryptor.stdout != nil { |
| encryptor.stdout.Close() |
| } |
| if encryptor.stderr != nil { |
| encryptor.stderr.Close() |
| } |
| if encryptor.stdin != nil { |
| encryptor.stdin.Close() |
| } |
| |
| os.RemoveAll(encryptor.tempDir) |
| } |
| |
| func getGpgTempDir() (string, error) { |
| temp, err := ioutil.TempDir("", "gpg-") |
| if err != nil { |
| return "", fmt.Errorf("failed to create temporary gpg workspace %v", err) |
| } |
| |
| return temp, nil |
| } |
| |
| func cleanupProcess(cmd *exec.Cmd) { |
| if !cmd.ProcessState.Exited() { |
| cmd.Process.Kill() |
| } |
| } |
| |
| func isOkExitCode(cmd *exec.Cmd) (bool, error) { |
| exitCode := cmd.ProcessState.ExitCode() |
| |
| if exitCode == -1 { |
| return false, fmt.Errorf("process is either still runing or was terminated by a signal") |
| } |
| |
| return exitCode == 0, nil |
| } |
| |
| func makePipes(cmd *exec.Cmd) (stdout io.ReadCloser, stderr io.ReadCloser, stdin io.WriteCloser, error error) { |
| stdout, err := cmd.StdoutPipe() |
| if err != nil { |
| return nil, nil, nil, fmt.Errorf("cmd.StdoutPipe() failed %v", err) |
| } |
| |
| stdin, err = cmd.StdinPipe() |
| if err != nil { |
| return nil, nil, nil, fmt.Errorf("cmd.StdinPipe() failed %v", err) |
| } |
| |
| stderr, err = cmd.StderrPipe() |
| if err != nil { |
| return nil, nil, nil, fmt.Errorf("cmd.StderrPipe() failed %v", err) |
| } |
| |
| return stdout, stderr, stdin, nil |
| } |