github.com/StackExchange/blackbox/v2@v2.0.1-0.20220331193400-d84e904973ab/pkg/box/verbs.go (about) 1 package box 2 3 // This file implements the business logic related to a black box. 4 // These functions are usually called from cmd/blackbox/drive.go or 5 // external sytems that use box as a module. 6 import ( 7 "bufio" 8 "fmt" 9 "io/ioutil" 10 "os" 11 "path/filepath" 12 "sort" 13 "strconv" 14 "strings" 15 16 "github.com/StackExchange/blackbox/v2/pkg/bbutil" 17 "github.com/StackExchange/blackbox/v2/pkg/makesafe" 18 "github.com/olekukonko/tablewriter" 19 ) 20 21 // AdminAdd adds admins. 22 func (bx *Box) AdminAdd(nom string, sdir string) error { 23 err := bx.getAdmins() 24 if err != nil { 25 return err 26 } 27 28 //fmt.Printf("ADMINS=%q\n", bx.Admins) 29 30 // Check for duplicates. 31 if i := sort.SearchStrings(bx.Admins, nom); i < len(bx.Admins) && bx.Admins[i] == nom { 32 return fmt.Errorf("Admin %v already an admin", nom) 33 } 34 35 bx.logDebug.Printf("ADMIN ADD rbd=%q\n", bx.RepoBaseDir) 36 changedFiles, err := bx.Crypter.AddNewKey(nom, bx.RepoBaseDir, sdir, bx.ConfigPath) 37 if err != nil { 38 return fmt.Errorf("AdminAdd failed AddNewKey: %v", err) 39 } 40 41 // TODO(tlim): Try the json file. 42 43 // Try the legacy file: 44 fn := filepath.Join(bx.ConfigPath, "blackbox-admins.txt") 45 bx.logDebug.Printf("Admins file: %q", fn) 46 err = bbutil.AddLinesToSortedFile(fn, nom) 47 if err != nil { 48 return fmt.Errorf("could not update file (%q,%q): %v", fn, nom, err) 49 } 50 changedFiles = append([]string{fn}, changedFiles...) 51 52 bx.Vcs.NeedsCommit("NEW ADMIN: "+nom, bx.RepoBaseDir, changedFiles) 53 return nil 54 } 55 56 // AdminList lists the admin id's. 57 func (bx *Box) AdminList() error { 58 err := bx.getAdmins() 59 if err != nil { 60 return err 61 } 62 63 for _, v := range bx.Admins { 64 fmt.Println(v) 65 } 66 return nil 67 } 68 69 // AdminRemove removes an id from the admin list. 70 func (bx *Box) AdminRemove([]string) error { 71 return fmt.Errorf("NOT IMPLEMENTED: AdminRemove") 72 } 73 74 // Cat outputs a file, unencrypting if needed. 75 func (bx *Box) Cat(names []string) error { 76 if err := anyGpg(names); err != nil { 77 return fmt.Errorf("cat: %w", err) 78 } 79 80 err := bx.getFiles() 81 if err != nil { 82 return err 83 } 84 85 for _, name := range names { 86 var out []byte 87 var err error 88 if _, ok := bx.FilesSet[name]; ok { 89 out, err = bx.Crypter.Cat(name) 90 } else { 91 out, err = ioutil.ReadFile(name) 92 } 93 if err != nil { 94 bx.logErr.Printf("BX_CRY3\n") 95 return fmt.Errorf("cat: %w", err) 96 } 97 fmt.Print(string(out)) 98 } 99 return nil 100 } 101 102 // Decrypt decrypts a file. 103 func (bx *Box) Decrypt(names []string, overwrite bool, bulkpause bool, setgroup string) error { 104 var err error 105 106 if err := anyGpg(names); err != nil { 107 return err 108 } 109 110 err = bx.getFiles() 111 if err != nil { 112 return err 113 } 114 115 if bulkpause { 116 gpgAgentNotice() 117 } 118 119 groupchange := false 120 gid := -1 121 if setgroup != "" { 122 gid, err = parseGroup(setgroup) 123 if err != nil { 124 return fmt.Errorf("Invalid group name or gid: %w", err) 125 } 126 groupchange = true 127 } 128 bx.logDebug.Printf("DECRYPT GROUP %q %v,%v\n", setgroup, groupchange, gid) 129 130 if len(names) == 0 { 131 names = bx.Files 132 } 133 return decryptMany(bx, names, overwrite, groupchange, gid) 134 } 135 136 func decryptMany(bx *Box, names []string, overwrite bool, groupchange bool, gid int) error { 137 138 // TODO(tlim): If we want to decrypt them in parallel, go has a helper function 139 // called "sync.WaitGroup()"" which would be useful here. We would probably 140 // want to add a flag on the command line (stored in a field such as bx.ParallelMax) 141 // that limits the amount of parallelism. The default for the flag should 142 // probably be runtime.NumCPU(). 143 144 for _, name := range names { 145 fmt.Printf("========== DECRYPTING %q\n", name) 146 if !bx.FilesSet[name] { 147 bx.logErr.Printf("Skipping %q: File not registered with Blackbox", name) 148 continue 149 } 150 if (!overwrite) && bbutil.FileExistsOrProblem(name) { 151 bx.logErr.Printf("Skipping %q: Will not overwrite existing file", name) 152 continue 153 } 154 155 // TODO(tlim) v1 detects zero-length files and removes them, even 156 // if overwrite is disabled. I don't think anyone has ever used that 157 // feature. That said, if we want to do that, we would implement it here. 158 159 // TODO(tlim) v1 takes the md5 hash of the plaintext before it decrypts, 160 // then compares the new plaintext's md5. It prints "EXTRACTED" if 161 // there is a change. 162 163 err := bx.Crypter.Decrypt(name, bx.Umask, overwrite) 164 if err != nil { 165 bx.logErr.Printf("%q: %v", name, err) 166 continue 167 } 168 169 // FIXME(tlim): Clone the file perms from the .gpg file to the plaintext file. 170 171 if groupchange { 172 // FIXME(tlim): Also "chmod g+r" the file. 173 os.Chown(name, -1, gid) 174 } 175 } 176 return nil 177 } 178 179 // Diff ... 180 func (bx *Box) Diff([]string) error { 181 return fmt.Errorf("NOT IMPLEMENTED: Diff") 182 } 183 184 // Edit unencrypts, calls editor, calls encrypt. 185 func (bx *Box) Edit(names []string) error { 186 187 if err := anyGpg(names); err != nil { 188 return err 189 } 190 191 err := bx.getFiles() 192 if err != nil { 193 return err 194 } 195 196 for _, name := range names { 197 if _, ok := bx.FilesSet[name]; ok { 198 if !bbutil.FileExistsOrProblem(name) { 199 err := bx.Crypter.Decrypt(name, bx.Umask, false) 200 if err != nil { 201 return fmt.Errorf("edit failed %q: %w", name, err) 202 } 203 } 204 } 205 err := bbutil.RunBash(bx.Editor, name) 206 if err != nil { 207 return err 208 } 209 } 210 return nil 211 } 212 213 // Encrypt encrypts a file. 214 func (bx *Box) Encrypt(names []string, shred bool) error { 215 var err error 216 217 if err = anyGpg(names); err != nil { 218 return err 219 } 220 221 err = bx.getAdmins() 222 if err != nil { 223 return err 224 } 225 226 err = bx.getFiles() 227 if err != nil { 228 return err 229 } 230 if len(names) == 0 { 231 names = bx.Files 232 } 233 234 enames, err := encryptMany(bx, names, shred) 235 236 bx.Vcs.NeedsCommit( 237 PrettyCommitMessage("ENCRYPTED", names), 238 bx.RepoBaseDir, 239 enames, 240 ) 241 242 return err 243 } 244 245 func encryptMany(bx *Box, names []string, shred bool) ([]string, error) { 246 var enames []string 247 for _, name := range names { 248 fmt.Printf("========== ENCRYPTING %q\n", name) 249 if !bx.FilesSet[name] { 250 bx.logErr.Printf("Skipping %q: File not registered with Blackbox", name) 251 continue 252 } 253 if !bbutil.FileExistsOrProblem(name) { 254 bx.logErr.Printf("Skipping. Plaintext does not exist: %q", name) 255 continue 256 } 257 ename, err := bx.Crypter.Encrypt(name, bx.Umask, bx.Admins) 258 if err != nil { 259 bx.logErr.Printf("Failed to encrypt %q: %v", name, err) 260 continue 261 } 262 enames = append(enames, ename) 263 if shred { 264 bx.Shred([]string{name}) 265 } 266 } 267 268 return enames, nil 269 } 270 271 // FileAdd enrolls files. 272 func (bx *Box) FileAdd(names []string, shred bool) error { 273 bx.logDebug.Printf("FileAdd(shred=%v, %v)", shred, names) 274 275 // Check for dups. 276 // Encrypt them all. 277 // If that succeeds, add to the blackbox-files.txt file. 278 // (optionally) shred the plaintext. 279 280 // FIXME(tlim): Check if the plaintext is in GIT. If it is, 281 // remove it from Git and print a warning that they should 282 // eliminate the history or rotate any secrets. 283 284 if err := anyGpg(names); err != nil { 285 return err 286 } 287 288 err := bx.getAdmins() 289 if err != nil { 290 return err 291 } 292 err = bx.getFiles() 293 if err != nil { 294 return err 295 } 296 if err := anyGpg(names); err != nil { 297 return err 298 } 299 300 // Check for newlines 301 for _, n := range names { 302 if strings.ContainsAny(n, "\n") { 303 return fmt.Errorf("file %q contains a newlineregistered", n) 304 } 305 } 306 307 // Check for duplicates. 308 for _, n := range names { 309 if i := sort.SearchStrings(bx.Files, n); i < len(bx.Files) && bx.Files[i] == n { 310 return fmt.Errorf("file %q already registered", n) 311 } 312 } 313 314 // Encrypt 315 var needsCommit []string 316 for _, name := range names { 317 s, err := bx.Crypter.Encrypt(name, bx.Umask, bx.Admins) 318 if err != nil { 319 return fmt.Errorf("AdminAdd failed AddNewKey: %v", err) 320 } 321 needsCommit = append(needsCommit, s) 322 } 323 324 // TODO(tlim): Try the json file. 325 326 // Try the legacy file: 327 fn := filepath.Join(bx.ConfigPath, "blackbox-files.txt") 328 bx.logDebug.Printf("Files file: %q", fn) 329 err = bbutil.AddLinesToSortedFile(fn, names...) 330 if err != nil { 331 return fmt.Errorf("could not update file (%q,%q): %v", fn, names, err) 332 } 333 334 err = bx.Shred(names) 335 if err != nil { 336 bx.logErr.Printf("Error while shredding: %v", err) 337 } 338 339 bx.Vcs.CommitTitle("BLACKBOX ADD FILE: " + makesafe.FirstFew(makesafe.ShellMany(names))) 340 341 bx.Vcs.IgnoreFiles(bx.RepoBaseDir, names) 342 343 bx.Vcs.NeedsCommit( 344 PrettyCommitMessage("blackbox-files.txt add", names), 345 bx.RepoBaseDir, 346 append([]string{filepath.Join(bx.ConfigPath, "blackbox-files.txt")}, needsCommit...), 347 ) 348 return nil 349 } 350 351 // FileList lists the files. 352 func (bx *Box) FileList() error { 353 err := bx.getFiles() 354 if err != nil { 355 return err 356 } 357 for _, v := range bx.Files { 358 fmt.Println(v) 359 } 360 return nil 361 } 362 363 // FileRemove de-enrolls files. 364 func (bx *Box) FileRemove(names []string) error { 365 return fmt.Errorf("NOT IMPLEMENTED: FileRemove") 366 } 367 368 // Info prints debugging info. 369 func (bx *Box) Info() error { 370 371 err := bx.getFiles() 372 if err != nil { 373 bx.logErr.Printf("Info getFiles: %v", err) 374 } 375 376 err = bx.getAdmins() 377 if err != nil { 378 bx.logErr.Printf("Info getAdmins: %v", err) 379 } 380 381 fmt.Println("BLACKBOX:") 382 fmt.Printf(" Debug: %v\n", bx.Debug) 383 fmt.Printf(" Team: %q\n", bx.Team) 384 fmt.Printf(" RepoBaseDir: %q\n", bx.RepoBaseDir) 385 fmt.Printf(" ConfigPath: %q\n", bx.ConfigPath) 386 fmt.Printf(" Umask: %04o\n", bx.Umask) 387 fmt.Printf(" Editor: %v\n", bx.Editor) 388 fmt.Printf(" Shredder: %v\n", bbutil.ShredInfo()) 389 fmt.Printf(" Admins: count=%v\n", len(bx.Admins)) 390 fmt.Printf(" Files: count=%v\n", len(bx.Files)) 391 fmt.Printf(" FilesSet: count=%v\n", len(bx.FilesSet)) 392 fmt.Printf(" Vcs: %v\n", bx.Vcs) 393 fmt.Printf(" VcsName: %q\n", bx.Vcs.Name()) 394 fmt.Printf(" Crypter: %v\n", bx.Crypter) 395 fmt.Printf(" CrypterName: %q\n", bx.Crypter.Name()) 396 397 return nil 398 } 399 400 // Init initializes a repo. 401 func (bx *Box) Init(yes, vcsname string) error { 402 fmt.Printf("VCS root is: %q\n", bx.RepoBaseDir) 403 404 fmt.Printf("team is: %q\n", bx.Team) 405 fmt.Printf("configdir will be: %q\n", bx.ConfigPath) 406 407 if yes != "yes" { 408 fmt.Printf("Enable blackbox for this %v repo? (yes/no)? ", bx.Vcs.Name()) 409 input := bufio.NewScanner(os.Stdin) 410 input.Scan() 411 ans := input.Text() 412 b, err := strconv.ParseBool(ans) 413 if err != nil { 414 b = false 415 if len(ans) > 0 { 416 if ans[0] == 'y' || ans[0] == 'Y' { 417 b = true 418 } 419 } 420 } 421 if !b { 422 fmt.Println("Ok. Maybe some other time.") 423 return nil 424 } 425 } 426 427 err := os.Mkdir(bx.ConfigPath, 0o750) 428 if err != nil { 429 return err 430 } 431 432 ba := filepath.Join(bx.ConfigPath, "blackbox-admins.txt") 433 bf := filepath.Join(bx.ConfigPath, "blackbox-files.txt") 434 bbutil.Touch(ba) 435 bbutil.Touch(bf) 436 bx.Vcs.SetFileTypeUnix(bx.RepoBaseDir, ba, bf) 437 438 bx.Vcs.IgnoreAnywhere(bx.RepoBaseDir, []string{ 439 "pubring.gpg~", 440 "pubring.kbx~", 441 "secring.gpg", 442 }) 443 444 fs := []string{ba, bf} 445 bx.Vcs.NeedsCommit( 446 "NEW: "+strings.Join(makesafe.RedactMany(fs), " "), 447 bx.RepoBaseDir, 448 fs, 449 ) 450 451 bx.Vcs.CommitTitle("INITIALIZE BLACKBOX") 452 return nil 453 } 454 455 // Reencrypt decrypts and reencrypts files. 456 func (bx *Box) Reencrypt(names []string, overwrite bool, bulkpause bool) error { 457 458 allFiles := false 459 460 if err := anyGpg(names); err != nil { 461 return err 462 } 463 if err := bx.getAdmins(); err != nil { 464 return err 465 } 466 if err := bx.getFiles(); err != nil { 467 return err 468 } 469 if len(names) == 0 { 470 names = bx.Files 471 allFiles = true 472 } 473 474 if bulkpause { 475 gpgAgentNotice() 476 } 477 478 fmt.Println("========== blackbox administrators are:") 479 bx.AdminList() 480 fmt.Println("========== (the above people will be able to access the file)") 481 482 if overwrite { 483 bbutil.ShredFiles(names) 484 } else { 485 warned := false 486 for _, n := range names { 487 if bbutil.FileExistsOrProblem(n) { 488 if !warned { 489 fmt.Printf("========== Shred these files?\n") 490 warned = true 491 } 492 fmt.Println("SHRED?", n) 493 } 494 } 495 if warned { 496 shouldWeOverwrite() 497 } 498 } 499 500 // Decrypt 501 if err := decryptMany(bx, names, overwrite, false, 0); err != nil { 502 return fmt.Errorf("reencrypt failed decrypt: %w", err) 503 } 504 enames, err := encryptMany(bx, names, false) 505 if err != nil { 506 return fmt.Errorf("reencrypt failed encrypt: %w", err) 507 } 508 if err := bbutil.ShredFiles(names); err != nil { 509 return fmt.Errorf("reencrypt failed shred: %w", err) 510 } 511 512 if allFiles { 513 // If the "--all" flag was used, don't try to list all the files. 514 bx.Vcs.NeedsCommit( 515 "REENCRYPT all files", 516 bx.RepoBaseDir, 517 enames, 518 ) 519 } else { 520 bx.Vcs.NeedsCommit( 521 PrettyCommitMessage("REENCRYPT", names), 522 bx.RepoBaseDir, 523 enames, 524 ) 525 526 } 527 528 return nil 529 } 530 531 // Shred shreds files. 532 func (bx *Box) Shred(names []string) error { 533 534 if err := anyGpg(names); err != nil { 535 return err 536 } 537 538 err := bx.getFiles() 539 // Calling getFiles() has the benefit of making sure we are in a repo. 540 if err != nil { 541 return err 542 } 543 544 if len(names) == 0 { 545 names = bx.Files 546 } 547 548 return bbutil.ShredFiles(names) 549 } 550 551 // Status prints the status of files. 552 func (bx *Box) Status(names []string, nameOnly bool, match string) error { 553 554 err := bx.getFiles() 555 if err != nil { 556 return err 557 } 558 559 var flist []string 560 if len(names) == 0 { 561 flist = bx.Files 562 } else { 563 flist = names 564 } 565 566 var data [][]string 567 var onlylist []string 568 thirdColumn := false 569 var tcData bool 570 571 for _, name := range flist { 572 var stat string 573 var err error 574 if _, ok := bx.FilesSet[name]; ok { 575 stat, err = FileStatus(name) 576 } else { 577 stat, err = "NOTREG", nil 578 } 579 if (match == "") || (stat == match) { 580 if err == nil { 581 data = append(data, []string{stat, name}) 582 onlylist = append(onlylist, name) 583 } else { 584 thirdColumn = tcData 585 data = append(data, []string{stat, name, fmt.Sprintf("%v", err)}) 586 onlylist = append(onlylist, fmt.Sprintf("%v: %v", name, err)) 587 } 588 } 589 } 590 591 if nameOnly { 592 fmt.Println(strings.Join(onlylist, "\n")) 593 return nil 594 } 595 596 table := tablewriter.NewWriter(os.Stdout) 597 table.SetAutoWrapText(false) 598 if thirdColumn { 599 table.SetHeader([]string{"Status", "Name", "Error"}) 600 } else { 601 table.SetHeader([]string{"Status", "Name"}) 602 } 603 for _, v := range data { 604 table.Append(v) 605 } 606 table.Render() // Send output 607 608 return nil 609 } 610 611 // TestingInitRepo initializes a repo. 612 // Uses bx.Vcs to create ".git" or whatever. 613 // Uses bx.Vcs to discover what was created, testing its work. 614 func (bx *Box) TestingInitRepo() error { 615 616 if bx.Vcs == nil { 617 fmt.Println("bx.Vcs is nil") 618 fmt.Printf("BLACKBOX_VCS=%q\n", os.Getenv("BLACKBOX_VCS")) 619 os.Exit(1) 620 } 621 fmt.Printf("ABOUT TO CALL TestingInitRepo\n") 622 fmt.Printf("vcs = %v\n", bx.Vcs.Name()) 623 err := bx.Vcs.TestingInitRepo() 624 fmt.Printf("RETURNED from TestingInitRepo: %v\n", err) 625 fmt.Println(os.Getwd()) 626 if err != nil { 627 return fmt.Errorf("TestingInitRepo returned: %w", err) 628 } 629 if b, _ := bx.Vcs.Discover(); !b { 630 return fmt.Errorf("TestingInitRepo failed Discovery") 631 } 632 return nil 633 }