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