Skip to content

Commit

Permalink
Improve clipboard safety (gopasspw#2065)
Browse files Browse the repository at this point in the history
This commit finally incorporates the changes suggested by rianadon in

Additionally it switches the unclip checksum from SHA256 to Argon2id.
Although the risk is close to zero (i.e. if someone can access our
process env we're doomed anyway) but there is just no reason to not
do it properly.

RELEASE_NOTES=[ENHANCEMENT] Hide password on MacOS clipboards

Fixes gopasspw#1816

Signed-off-by: Dominik Schulz <[email protected]>
  • Loading branch information
dominikschulz authored Dec 28, 2021
1 parent 28479c0 commit 39e39be
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 10 deletions.
2 changes: 1 addition & 1 deletion pkg/clipboard/clipboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func CopyTo(ctx context.Context, name string, content []byte, timeout int) error
return nil
}

if err := clipboard.WriteAll(string(content)); err != nil {
if err := copyToClipboard(ctx, content); err != nil {
_ = notify.Notify(ctx, "gopass - clipboard", "failed to write to clipboard")
return fmt.Errorf("failed to write to clipboard: %w", err)
}
Expand Down
7 changes: 5 additions & 2 deletions pkg/clipboard/clipboard_others.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ package clipboard
import (
"bytes"
"context"
"crypto/sha256"
"fmt"
"os"
"os/exec"
"strconv"
"syscall"

"github.com/gopasspw/gopass/internal/pwschemes/argon2id"
"github.com/gopasspw/gopass/pkg/ctxutil"
)

Expand All @@ -21,7 +21,10 @@ import (
// of the clipboard and erase it if it still contains the data gopass copied
// to it.
func clear(ctx context.Context, content []byte, timeout int) error {
hash := fmt.Sprintf("%x", sha256.Sum256(content))
hash, err := argon2id.Generate(string(content), 0)
if err != nil {
return err
}

// kill any pending unclip processes
_ = killPrecedessors()
Expand Down
13 changes: 10 additions & 3 deletions pkg/clipboard/clipboard_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,29 @@ package clipboard

import (
"context"
"crypto/sha256"
"fmt"
"os"
"os/exec"
"strconv"

"github.com/gopasspw/gopass/internal/pwschemes/argon2id"
"github.com/gopasspw/gopass/pkg/ctxutil"
)

// clear will spwan a copy of gopass that waits in a detached background
// process group until the timeout is expired. It will then compare the contents
// of the clipboard and erase it if it still contains the data gopass copied
// to it.
func clear(ctx context.Context, content []byte, timeout int) error {
hash := fmt.Sprintf("%x", sha256.Sum256(content))
hash, err := argon2id.Generate(string(content), 0)
if err != nil {
return err
}

cmd := exec.CommandContext(ctx, os.Args[0], "unclip", "--timeout", strconv.Itoa(timeout))
cmd.Env = append(os.Environ(), "GOPASS_UNCLIP_CHECKSUM="+hash)
if !ctxutil.IsNotifications(ctx) {
cmd.Env = append(cmd.Env, "GOPASS_NO_NOTIFY=true")
}
return cmd.Start()
}

Expand Down
77 changes: 77 additions & 0 deletions pkg/clipboard/copy_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//go:build darwin
// +build darwin

package clipboard

import (
"context"
"fmt"
"io"
"os"
"os/exec"

"github.com/atotto/clipboard"
"github.com/gopasspw/gopass/pkg/debug"
)

func copyToClipboard(ctx context.Context, content []byte) error {
password := string(content)

// first try to copy via osascript, if that fails fallback to pbcopy
if err := copyViaOsascript(ctx, password); err != nil {
debug.Log("failed to copy via osascript: %s", err)
return clipboard.WriteAll(password)
}
return nil
}

func copyViaOsascript(ctx context.Context, password string) error {
args := []string{
// The Foundation library has the Objective C symbols for pasteboard
"-e", `use framework "Foundation"`,
// Need to use scripting additions for access to "do shell script"
"-e", "use scripting additions",
// type = a reference to the ObjC constant NSPasteboardTypeString
// which is needed to indentify clioboard contents as text
"-e", "set type to current application's NSPasteboardTypeString",
// pb = a reference to the system's pasteboard
"-e", "set pb to current application's NSPasteboard's generalPasteboard()",
// Must clear contents before adding a new item to pasteboard
"-e", "pb's clearContents()",
// Set the flag ConcealedType so clipboard history managers don't record the password.
// The first argument can by anything, but an empty string will do fine.
"-e", `pb's setString:"" forType:"org.nspasteboard.ConcealedType"`,
// AppleScript cannot read from stdin, so pipe fd#3 to stdin of cat and read the output.
// This output is put in the clipboard, setting type = string type
"-e", `pb's setString:(do shell script "cat 0<&3") forType:type`,
}
cmd := exec.CommandContext(ctx, "osascript", args...)
r, w, err := os.Pipe()
if err != nil {
return err
}

// This connects the pipe to stdin of the osascript command, see the "do shell script"
// part around line 46. The pipe is created and written to before the osascript command
// is run so we shouldn't need to worry about partial writes (I hope!).
//
// TODO: We might be able to use `cmd.Stdin = strings.NewReader(password)` instead
cmd.ExtraFiles = []*os.File{r} // Receiving end of pipes is connected to fd#3
go func() {
defer w.Close()
io.WriteString(w, password) // Write the password to fd#3
}()

out, err := cmd.Output()
if err != nil {
return err
}

// osascript should print true (return value of the last setString call) on success
if string(out) != "true\n" {
// Fallback to using attoto's pbcopy
return fmt.Errorf("osascript failed to set password: %s", string(out))
}
debug.Log("copied via osascript")
return nil
}
14 changes: 14 additions & 0 deletions pkg/clipboard/copy_others.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//go:build !darwin
// +build !darwin

package clipboard

import (
"context"

"github.com/atotto/clipboard"
)

func copyToClipboard(ctx context.Context, content []byte) error {
return clipboard.WriteAll(string(content))
}
15 changes: 11 additions & 4 deletions pkg/clipboard/unclip.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package clipboard

import (
"context"
"crypto/sha256"
"fmt"

"github.com/atotto/clipboard"
"github.com/gopasspw/gopass/internal/notify"
"github.com/gopasspw/gopass/internal/pwschemes/argon2id"
"github.com/gopasspw/gopass/pkg/debug"

"github.com/atotto/clipboard"
)

// Clear will attempt to erase the clipboard
Expand All @@ -20,8 +22,12 @@ func Clear(ctx context.Context, checksum string, force bool) error {
return fmt.Errorf("failed to read clipboard: %w", err)
}

hash := fmt.Sprintf("%x", sha256.Sum256([]byte(cur)))
if hash != checksum && !force {
match, err := argon2id.Validate(cur, checksum)
if err != nil {
debug.Log("failed to validate checksum %s: %s", checksum, err)
return nil
}
if !match && !force {
return nil
}

Expand All @@ -39,5 +45,6 @@ func Clear(ctx context.Context, checksum string, force bool) error {
return fmt.Errorf("failed to send unclip notification: %w", err)
}

debug.Log("clipboard cleared (%s)", checksum)
return nil
}

0 comments on commit 39e39be

Please sign in to comment.