1package action
2
3import (
4	"bytes"
5	"context"
6	"crypto/sha256"
7	"encoding/base64"
8	"fmt"
9	"io"
10	"os"
11	"path/filepath"
12
13	"github.com/gopasspw/gopass/internal/out"
14	"github.com/gopasspw/gopass/pkg/ctxutil"
15	"github.com/gopasspw/gopass/pkg/debug"
16	"github.com/gopasspw/gopass/pkg/fsutil"
17	"github.com/gopasspw/gopass/pkg/gopass"
18	"github.com/gopasspw/gopass/pkg/gopass/secrets"
19
20	"github.com/urfave/cli/v2"
21)
22
23var (
24	binstdin = os.Stdin
25)
26
27// Cat prints to or reads from STDIN/STDOUT
28func (s *Action) Cat(c *cli.Context) error {
29	ctx := ctxutil.WithGlobalFlags(c)
30	name := c.Args().First()
31	if name == "" {
32		return ExitError(ExitNoName, nil, "Usage: %s cat <NAME>", c.App.Name)
33	}
34
35	// handle pipe to stdin
36	info, err := binstdin.Stat()
37	if err != nil {
38		return ExitError(ExitIO, err, "failed to stat stdin: %s", err)
39	}
40
41	// if content is piped to stdin, read and save it
42	if info.Mode()&os.ModeCharDevice == 0 {
43		debug.Log("Reading from STDIN ...")
44		content := &bytes.Buffer{}
45
46		if written, err := io.Copy(content, binstdin); err != nil {
47			return ExitError(ExitIO, err, "Failed to copy after %d bytes: %s", written, err)
48		}
49
50		return s.Store.Set(
51			ctxutil.WithCommitMessage(ctx, "Read secret from STDIN"),
52			name,
53			secFromBytes(name, "STDIN", content.Bytes()),
54		)
55	}
56
57	buf, err := s.binaryGet(ctx, name)
58	if err != nil {
59		return ExitError(ExitDecrypt, err, "failed to read secret: %s", err)
60	}
61
62	fmt.Fprint(stdout, string(buf))
63	return nil
64}
65
66func secFromBytes(dst, src string, in []byte) gopass.Secret {
67	debug.Log("Read %d bytes from %s to %s", len(in), src, dst)
68
69	sec := secrets.NewKV()
70	if err := sec.Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filepath.Base(src))); err != nil {
71		debug.Log("Failed to set Content-Disposition: %q", err)
72	}
73
74	sec.Write([]byte(base64.StdEncoding.EncodeToString(in)))
75	if err := sec.Set("Content-Transfer-Encoding", "Base64"); err != nil {
76		debug.Log("Failed to set Content-Transfer-Encoding: %q", err)
77	}
78
79	return sec
80}
81
82// BinaryCopy copies either from the filesystem to the store or from the store
83// to the filesystem
84func (s *Action) BinaryCopy(c *cli.Context) error {
85	ctx := ctxutil.WithGlobalFlags(c)
86	from := c.Args().Get(0)
87	to := c.Args().Get(1)
88
89	// argument checking is in s.binaryCopy
90	if err := s.binaryCopy(ctx, c, from, to, false); err != nil {
91		return ExitError(ExitUnknown, err, "%s", err)
92	}
93	return nil
94}
95
96// BinaryMove works like Copy but will remove (shred/wipe) the source
97// after a successful copy. Mostly useful for securely moving secrets into
98// the store if they are no longer needed / wanted on disk afterwards
99func (s *Action) BinaryMove(c *cli.Context) error {
100	ctx := ctxutil.WithGlobalFlags(c)
101	from := c.Args().Get(0)
102	to := c.Args().Get(1)
103
104	// argument checking is in s.binaryCopy
105	if err := s.binaryCopy(ctx, c, from, to, true); err != nil {
106		return ExitError(ExitUnknown, err, "%s", err)
107	}
108	return nil
109}
110
111// binaryCopy implements the control flow for copy and move. We support two
112// workflows:
113// 1. From the filesystem to the store
114// 2. From the store to the filesystem
115//
116// Copying secrets in the store must be done through the regular copy command
117func (s *Action) binaryCopy(ctx context.Context, c *cli.Context, from, to string, deleteSource bool) error {
118	if from == "" || to == "" {
119		op := "copy"
120		if deleteSource {
121			op = "move"
122		}
123		return fmt.Errorf("usage: %s fs%s from to", c.App.Name, op)
124	}
125
126	switch {
127	case fsutil.IsFile(from) && fsutil.IsFile(to):
128		// copying from on file to another file is not supported
129		return fmt.Errorf("ambiguity detected. Only from or to can be a file")
130	case s.Store.Exists(ctx, from) && s.Store.Exists(ctx, to):
131		// copying from one secret to another secret is not supported
132		return fmt.Errorf("ambiguity detected. Either from or to must be a file")
133	case fsutil.IsFile(from) && !fsutil.IsFile(to):
134		return s.binaryCopyFromFileToStore(ctx, from, to, deleteSource)
135	case !fsutil.IsFile(from):
136		return s.binaryCopyFromStoreToFile(ctx, from, to, deleteSource)
137	default:
138		return fmt.Errorf("ambiguity detected. Unhandled case. Please report a bug")
139	}
140}
141
142func (s *Action) binaryCopyFromFileToStore(ctx context.Context, from, to string, deleteSource bool) error {
143	// if the source is a file the destination must no to avoid ambiguities
144	// if necessary this can be resolved by using a absolute path for the file
145	// and a relative one for the secret
146
147	// copy from FS to store
148	buf, err := os.ReadFile(from)
149	if err != nil {
150		return fmt.Errorf("failed to read file from %q: %w", from, err)
151	}
152
153	if err := s.Store.Set(
154		ctxutil.WithCommitMessage(ctx, fmt.Sprintf("Copied data from %s to %s", from, to)), to, secFromBytes(to, from, buf)); err != nil {
155		return fmt.Errorf("failed to save buffer to store: %w", err)
156	}
157
158	if !deleteSource {
159		return nil
160	}
161
162	// it's important that we return if the validation fails, because
163	// in that case we don't want to shred our (only) copy of this data!
164	if err := s.binaryValidate(ctx, buf, to); err != nil {
165		return fmt.Errorf("failed to validate written data: %w", err)
166	}
167	if err := fsutil.Shred(from, 8); err != nil {
168		return fmt.Errorf("failed to shred data: %w", err)
169	}
170	return nil
171}
172
173func (s *Action) binaryCopyFromStoreToFile(ctx context.Context, from, to string, deleteSource bool) error {
174	// if the source is no file we assume it's a secret and to is a filename
175	// (which may already exist or not)
176
177	// copy from store to FS
178	buf, err := s.binaryGet(ctx, from)
179	if err != nil {
180		return fmt.Errorf("failed to read data from %q: %w", from, err)
181	}
182	if err := os.WriteFile(to, buf, 0600); err != nil {
183		return fmt.Errorf("failed to write data to %q: %w", to, err)
184	}
185
186	if !deleteSource {
187		return nil
188	}
189
190	// as before: if validation of the written data fails, we MUST NOT
191	// delete the (only) source
192	if err := s.binaryValidate(ctx, buf, from); err != nil {
193		return fmt.Errorf("failed to validate the written data: %w", err)
194	}
195	if err := s.Store.Delete(ctx, from); err != nil {
196		return fmt.Errorf("failed to delete %q from the store: %w", from, err)
197	}
198	return nil
199}
200
201func (s *Action) binaryValidate(ctx context.Context, buf []byte, name string) error {
202	h := sha256.New()
203	_, _ = h.Write(buf)
204	fileSum := fmt.Sprintf("%x", h.Sum(nil))
205	h.Reset()
206
207	debug.Log("in: %s - %q", fileSum, string(buf))
208
209	var err error
210	buf, err = s.binaryGet(ctx, name)
211	if err != nil {
212		return fmt.Errorf("failed to read %q from the store: %w", name, err)
213	}
214	_, _ = h.Write(buf)
215	storeSum := fmt.Sprintf("%x", h.Sum(nil))
216
217	debug.Log("store: %s - %q", storeSum, string(buf))
218
219	if fileSum != storeSum {
220		return fmt.Errorf("hashsum mismatch (file: %s, store: %s)", fileSum, storeSum)
221	}
222	return nil
223}
224
225func (s *Action) binaryGet(ctx context.Context, name string) ([]byte, error) {
226	sec, err := s.Store.Get(ctx, name)
227	if err != nil {
228		return nil, fmt.Errorf("failed to read %q from the store: %w", name, err)
229	}
230
231	if cte, _ := sec.Get("content-transfer-encoding"); cte != "Base64" {
232		return []byte(sec.Body()), nil
233	}
234
235	buf, err := base64.StdEncoding.DecodeString(sec.Body())
236	if err != nil {
237		return nil, fmt.Errorf("failed to encode to base64: %w", err)
238	}
239	return buf, nil
240}
241
242// Sum decodes binary content and computes the SHA256 checksum
243func (s *Action) Sum(c *cli.Context) error {
244	ctx := ctxutil.WithGlobalFlags(c)
245	name := c.Args().First()
246	if name == "" {
247		return ExitError(ExitUsage, nil, "Usage: %s sha256 name", c.App.Name)
248	}
249
250	buf, err := s.binaryGet(ctx, name)
251	if err != nil {
252		return ExitError(ExitDecrypt, err, "failed to read secret: %s", err)
253	}
254
255	h := sha256.New()
256	_, _ = h.Write(buf)
257	out.Printf(ctx, "%x", h.Sum(nil))
258
259	return nil
260}
261