Implement blackbox in Golang (#250)

* Initial release
This commit is contained in:
Tom Limoncelli
2020-07-24 14:21:33 -04:00
committed by GitHub
parent e049c02655
commit 1c77c87555
86 changed files with 6074 additions and 22 deletions

91
integrationTest/NOTES.txt Normal file
View File

@@ -0,0 +1,91 @@
This should accept VCS-type and --crypto flags.
Then a shell script should run various combinations of VCS and crypters.
# Startup
* Create a repo (git, none)
# Test basic operations:
* As Alice:
* initialize blackbox, add her keys to it, see that the usual files
exist. See her name in bb-admins.txt
* encrypt a file, see that the plaintext is deleted, see the file in bb-files.txt
* decrypt the file, see the original plaintext is recovered.
* Encrypt a file --noshred.
* Decrypt the file, it should fail as the plaintext exists.
* Remove the plaintext.
* Decrypt the file, it should fail as the plaintext exists.
# Test hand-off from Alice to Bob.
* As Bob
* add himself to the admins.
* As Alice
* Update-all-files
* Create a new file. Encrypt it.
* As Bob
* Decrypt both files
* Verify contents of the new file, and the file from previous.
* Create a new file. Encrypt it.
* As Alice:
* Decrypt all files.
* Verify contents of the 3 plaintext files.
# Test a git-less directory
* Copy the old repo somewhere. Remove the .git directory.
* As Alice:
* Decrypt all
* Verify plaintext contents
# Test post-deploy with/without GID
* Back at the original repo:
* Shred all
* Run post-deploy. Verify.
* Shred all
* Run post-deploy with a custom GID. Verify.
# Test removing an admin
* As Bob:
* removes Alice. (Verify)
* Re-encrypt
* Decrypt all & verify.
* As alice
* Decrypting should fail.
# Test funny names and paths
* my/path/to/relsecrets.txt
* cwd=other/place ../../my/path/to/relsecrets.txt
* !important!.txt
* #andpounds.txt
* stars*bars?.txt
* space space.txt
* Do add/encrypt/decrypt
* Do blackbox_update_all_files
* Do remove them all
# When people start asking for commands to work with relative paths
# Test from outside the repo
* mkdir ../other/place
* cd ../other/place
* decrypt ../../secret1.txt
* encrypt ../../secret1.txt
# Test specific commands:
# blackbox admins list
# blackbox file list
# blackbox status --name-only (create 1 of each "type")
# blackbox status --type=FOO
# These should all fail:
# blackbox file list --all
# blackbox file list blah
# blackbox shred list --all
# blackbox shred list blah
rm -rf /tmp/bbhome-* && BLACKBOX_DEBUG=true go test -verbose -long -nocleanup
rm -rf /tmp/bbhome-* && go test -long -nocleanup
( gbb && cd cmd/blackbox && go install ) && blackbox
cd /tmp && rm -rf /tmp/bbhome-* ; mkdir /tmp/bbhome-1 ; cd /tmp/bbhome-1 && git init ; gitmeWork ; ( gbb && cd cmd/blackbox && go install ) && blackbox init yes && gitmeWork ; git commit -mm -a ; blackbox admin add tlimoncelli ; git commit -mnewadmin -a ; echo secrt > secret.txt ; blackbox file add secret.txt

View File

@@ -0,0 +1,55 @@
Each test does the following:
1. Copy the files from testdata/NNNN
2. Run the command in test_NNNN.sh
3.
TEST ENROLLMENT:
PHASE 'Alice creates a repo. She creates secret.txt.'
PHASE 'Alice wants to be part of the secret system.'
PHASE 'She creates a GPG key...'
PHASE 'Initializes BB...'
PHASE 'and adds herself as an admin.'
PHASE 'Bob arrives.'
PHASE 'Bob creates a gpg key.'
PHASE 'Alice does the second part to enroll bob.'
PHASE 'She enrolls bob.'
PHASE 'She enrolls secrets.txt.'
PHASE 'She decrypts secrets.txt.'
PHASE 'She edits secrets.txt.'
PHASE 'Alice copies files to a non-repo directory. (NO REPO)'
PHASE 'Alice shreds these non-repo files. (NO REPO)'
PHASE 'Alice decrypts secrets.txt (NO REPO).'
PHASE 'Alice edits secrets.txt. (NO REPO EDIT)'
PHASE 'Alice decrypts secrets.txt (NO REPO EDIT).'
PHASE 'appears.'
#PHASE 'Bob makes sure he has all new keys.'
TEST INDIVIDUAL COMMANDS:
PHASE 'Bob postdeploys... default.'
PHASE 'Bob postdeploys... with a GID.'
PHASE 'Bob cleans up the secret.'
PHASE 'Bob removes Alice.'
PHASE 'Bob reencrypts files so alice can not access them.'
PHASE 'Bob decrypts secrets.txt.'
PHASE 'Bob edits secrets.txt.'
PHASE 'Bob decrypts secrets.txt VERSION 3.'
PHASE 'Bob exposes a secret in the repo.'
PHASE 'Bob corrects it by registering it.'
PHASE 'Bob enrolls my/path/to/relsecrets.txt.'
PHASE 'Bob decrypts relsecrets.txt.'
PHASE 'Bob enrolls !important!.txt'
PHASE 'Bob enrolls #andpounds.txt'
PHASE 'Bob enrolls stars*bars?.txt'
PHASE 'Bob enrolls space space.txt'
PHASE 'Bob checks out stars*bars?.txt.'
PHASE 'Bob checks out space space.txt.'
PHASE 'Bob shreds all exposed files.'
PHASE 'Bob updates all files.'
PHASE 'Bob DEregisters mistake.txt'
PHASE 'Bob enrolls multiple files: multi1.txt and multi2.txt'
PHASE 'Alice returns. She should be locked out'
PHASE 'Alice tries to decrypt secret.txt. Is blocked.'

View File

@@ -0,0 +1,68 @@
package main
import (
"io/ioutil"
"os"
"testing"
"github.com/andreyvit/diff"
)
func assertFileMissing(t *testing.T, name string) {
t.Helper()
_, err := os.Stat(name)
if err != nil && os.IsNotExist(err) {
return
}
if err == nil {
t.Fatalf("assertFileMissing failed: %v exists", name)
}
t.Fatalf("assertFileMissing: %q: %v", name, err)
}
func assertFileExists(t *testing.T, name string) {
t.Helper()
_, err := os.Stat(name)
if err == nil {
return
}
if os.IsNotExist(err) {
t.Fatalf("assertFileExists failed: %v not exist", name)
}
t.Fatalf("assertFileExists: file can't be accessed: %v: %v", name, err)
}
func assertFileEmpty(t *testing.T, name string) {
t.Helper()
c, err := ioutil.ReadFile(name)
if err != nil {
t.Fatal(err)
}
if len(c) != 0 {
t.Fatalf("got=%v want=%v: %v", len(c), 0, name)
}
}
func assertFileContents(t *testing.T, name string, contents string) {
t.Helper()
c, err := ioutil.ReadFile(name)
if err != nil {
t.Fatal(err)
}
if w, g := contents, string(c); w != g {
t.Errorf("assertFileContents(%q) mismatch (-got +want):\n%s",
name, diff.LineDiff(g, w))
}
}
func assertFilePerms(t *testing.T, name string, perms os.FileMode) {
t.Helper()
s, err := os.Stat(name)
if err != nil {
t.Fatal(err)
}
if s.Mode() != perms {
t.Fatalf("got=%#o want=%#o: %v", s.Mode(), perms, name)
}
}

View File

@@ -0,0 +1,343 @@
package main
import (
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"testing"
"github.com/StackExchange/blackbox/v2/pkg/bblog"
_ "github.com/StackExchange/blackbox/v2/pkg/bblog"
_ "github.com/StackExchange/blackbox/v2/pkg/vcs/_all"
)
var vcsToTest = flag.String("testvcs", "GIT", "VCS to test")
var longTests = flag.Bool("long", false, "Run long version of tests")
//var crypterToTest = flag.String("crypter", "GnuPG", "crypter to test")
func init() {
testing.Init()
flag.Parse()
op, err := os.Getwd()
if err != nil {
panic(err)
}
originPath = op
}
func compile(t *testing.T) {
if PathToBlackBox() != "" {
// It's been compiled already.
return
}
// Make sure we have the latest binary
fmt.Println("========== Compiling")
cmd := exec.Command("go", "build", "-o", "../bbintegration", "../cmd/blackbox")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
t.Fatalf("setup_compile: %v", err)
}
cwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
SetPathToBlackBox(filepath.Join(cwd, "../bbintegration"))
}
func setup(t *testing.T) {
logDebug := bblog.GetDebug(*verbose)
logDebug.Printf("flag.testvcs is %v", *vcsToTest)
vh := getVcs(t, *vcsToTest)
logDebug.Printf("Using BLACKBOX_VCS=%v", vh.Name())
os.Setenv("BLACKBOX_VCS", vh.Name())
}
func TestInit(t *testing.T) {
if !*longTests {
return
}
compile(t)
makeHomeDir(t, "init")
// Only zero or one args are permitted.
invalidArgs(t, "init", "one", "two")
invalidArgs(t, "init", "one", "two", "three")
runBB(t, "init", "yes")
assertFileEmpty(t, ".blackbox/blackbox-admins.txt")
assertFileEmpty(t, ".blackbox/blackbox-files.txt")
assertFilePerms(t, ".blackbox/blackbox-admins.txt", 0o640)
assertFilePerms(t, ".blackbox/blackbox-files.txt", 0o640)
}
func TestList(t *testing.T) {
if !*longTests {
return
}
compile(t)
makeHomeDir(t, "init")
runBB(t, "init", "yes")
createDummyFilesAdmin(t)
checkOutput("000-admin-list.txt", t, "admin", "list")
checkOutput("000-file-list.txt", t, "file", "list")
invalidArgs(t, "file", "list", "extra")
invalidArgs(t, "admin", "list", "extra")
}
func TestStatus(t *testing.T) {
if !*longTests {
return
}
compile(t)
makeHomeDir(t, "init")
runBB(t, "init", "yes")
createFilesStatus(t)
checkOutput("000-status.txt", t, "status")
}
func TestShred(t *testing.T) {
if !*longTests {
return
}
compile(t)
makeHomeDir(t, "shred")
runBB(t, "init", "yes")
makeFile(t, "shredme.txt", "File with SHREDME in it.\n")
assertFileExists(t, "shredme.txt")
runBB(t, "shred", "shredme.txt")
assertFileMissing(t, "shredme.txt")
}
func TestStatus_notreg(t *testing.T) {
if !*longTests {
return
}
compile(t)
makeHomeDir(t, "init")
runBB(t, "init", "yes")
createFilesStatus(t)
checkOutput("status-noreg.txt", t, "status", "status-ENCRYPTED.txt", "blah.txt")
}
// TestHard tests the functions using a fake homedir and repo.
func TestHard(t *testing.T) {
if !*longTests {
return
}
// These are basic tests that work on a fake repo.
// The repo has mostly real data, except any .gpg file
// is just garbage.
compile(t)
setup(t)
for _, cx := range []struct{ subname, prefix string }{
//{subname: ".", prefix: "."},
{subname: "mysub", prefix: ".."},
} {
subname := cx.subname
prefix := cx.prefix
_ = prefix
phase("========== SUBDIR = " + subname + " ==========")
makeHomeDir(t, "BasicAlice")
plaintextFoo := "I am the foo.txt file!\n"
plainAltered := "I am the altered file!\n"
runBB(t, "testing_init") // Runs "git init" or equiv
assertFileExists(t, ".git")
runBB(t, "init", "yes") // Creates .blackbox or equiv
if subname != "." {
err := os.Mkdir(subname, 0770)
if err != nil {
t.Fatal(fmt.Errorf("hard-mk-home %q: %v", subname, err))
}
}
olddir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
os.Chdir(subname)
os.Chdir(olddir)
phase("Alice creates a GPG key")
gpgdir := makeAdmin(t, "alice", "Alice Example", "alice@example.com")
become(t, "alice")
phase("Alice enrolls as an admin")
//os.Chdir(subname)
runBB(t, "admin", "add", "alice@example.com", gpgdir)
//os.Chdir(olddir)
// encrypt
phase("Alice registers foo.txt")
makeFile(t, "foo.txt", plaintextFoo)
//os.Chdir(subname)
//runBB(t, "file", "add", "--shred", filepath.Join(prefix, "foo.txt"))
runBB(t, "file", "add", "--shred", "foo.txt")
//os.Chdir(olddir)
// "file add" encrypts the file.
// We shred the plaintext so that we are sure that when Decrypt runs,
// we can verify the contents wasn't just sitting there all the time.
assertFileMissing(t, "foo.txt")
assertFileExists(t, "foo.txt.gpg")
phase("Alice decrypts foo.txt")
// decrypt
//os.Chdir(subname)
runBB(t, "decrypt", "foo.txt")
//runBB(t, "decrypt", filepath.Join(prefix, "foo.txt"))
//os.Chdir(olddir)
assertFileExists(t, "foo.txt")
assertFileExists(t, "foo.txt.gpg")
assertFileContents(t, "foo.txt", plaintextFoo)
// encrypts (without shredding)
phase("Alice encrypts foo.txt (again)")
runBB(t, "encrypt", "foo.txt")
assertFileExists(t, "foo.txt")
assertFileExists(t, "foo.txt.gpg")
assertFileContents(t, "foo.txt", plaintextFoo)
// reencrypt
phase("Alice reencrypts")
checkOutput("basic-status.txt", t, "status")
runBB(t, "reencrypt", "--overwrite", "foo.txt")
// Test variations of cat
// foo.txt=plain result=plain
phase("Alice cats plain:plain")
makeFile(t, "foo.txt", plaintextFoo)
assertFileExists(t, "foo.txt")
runBB(t, "encrypt", "foo.txt")
assertFileExists(t, "foo.txt")
assertFileExists(t, "foo.txt.gpg")
checkOutput("alice-cat-plain.txt", t, "cat", "foo.txt")
assertFileExists(t, "foo.txt")
assertFileExists(t, "foo.txt.gpg")
// foo.txt=altered result=plain
phase("Alice cats altered:plain")
makeFile(t, "foo.txt", plainAltered)
assertFileExists(t, "foo.txt")
assertFileExists(t, "foo.txt.gpg")
checkOutput("alice-cat-plain.txt", t, "cat", "foo.txt")
assertFileExists(t, "foo.txt")
assertFileExists(t, "foo.txt.gpg")
// foo.txt=missing result=plain
phase("Alice cats missing:plain")
removeFile(t, "foo.txt")
assertFileMissing(t, "foo.txt")
assertFileMissing(t, "foo.txt")
assertFileExists(t, "foo.txt.gpg")
checkOutput("alice-cat-plain.txt", t, "cat", "foo.txt")
assertFileMissing(t, "foo.txt")
assertFileExists(t, "foo.txt.gpg")
// Chapter 2: Bob
// Alice adds Bob.
// Bob encrypts a file.
// Bob makes sure he can decrypt alice's file.
// Bob removes Alice.
// Alice verifies she CAN'T decrypt files.
// Bob adds Alice back.
// Alice verifies she CAN decrypt files.
// Bob adds an encrypted file by mistake, "bb add" and fixes it.
// Bob corrupts the blackbox-admins.txt file, verifies that commands fail.
}
}
// TestEvilFilenames verifies commands work with "difficult" file names
func TestEvilFilenames(t *testing.T) {
if !*longTests {
return
}
compile(t)
setup(t)
makeHomeDir(t, "Mallory")
runBB(t, "testing_init") // Runs "git init" or equiv
assertFileExists(t, ".git")
runBB(t, "init", "yes") // Creates .blackbox or equiv
phase("Malory creates a GPG key")
gpgdir := makeAdmin(t, "mallory", "Mallory Evil", "mallory@example.com")
become(t, "mallory")
phase("Mallory enrolls as an admin")
runBB(t, "admin", "add", "mallory@example.com", gpgdir)
_ = os.MkdirAll("my/path/to", 0o770)
_ = os.Mkdir("other", 0o770)
for i, name := range []string{
"!important!.txt",
"#andpounds.txt",
"stars*bars?.txt",
"space space.txt",
"tab\ttab.txt",
"ret\rret.txt",
"smile😁eyes",
"¡que!",
"thé",
"pound£",
"*.go",
"rm -f erase ; echo done",
`smile☺`,
`dub𝓦`,
"my/path/to/relsecrets.txt",
//"my/../my/path/../path/to/myother.txt", // Not permitted yet
//"other/../my//path/../path/to/otherother.txt", // Not permitted yet
//"new\nnew.txt", // \n not permitted
//"two\n", // \n not permitted (yet)
//"four\U0010FFFF", // Illegal byte sequence. git won't accept.
} {
phase(fmt.Sprintf("Mallory tries %02d: %q", i, name))
contents := "the name of this file is the talking heads... i mean, " + name
makeFile(t, name, contents)
assertFileExists(t, name)
assertFileMissing(t, name+".gpg")
assertFileContents(t, name, contents)
runBB(t, "file", "add", name)
assertFileMissing(t, name)
assertFileExists(t, name+".gpg")
runBB(t, "decrypt", name)
assertFileExists(t, name)
assertFileExists(t, name+".gpg")
assertFileContents(t, name, contents)
runBB(t, "encrypt", name)
assertFileExists(t, name)
assertFileExists(t, name+".gpg")
assertFileContents(t, name, contents)
runBB(t, "shred", name)
assertFileMissing(t, name)
assertFileExists(t, name+".gpg")
}
}
// More tests to implement.
// 1. Verify that the --gid works (blackbox decrypt --gid)

View File

@@ -0,0 +1,617 @@
package main
import (
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
"github.com/StackExchange/blackbox/v2/pkg/bblog"
"github.com/StackExchange/blackbox/v2/pkg/bbutil"
"github.com/StackExchange/blackbox/v2/pkg/vcs"
_ "github.com/StackExchange/blackbox/v2/pkg/vcs/_all"
"github.com/andreyvit/diff"
)
var verbose = flag.Bool("verbose", false, "reveal stderr")
var nocleanup = flag.Bool("nocleanup", false, "do not delete the tmp directory")
type userinfo struct {
name string
dir string // .gnupg-$name
agentInfo string // GPG_AGENT_INFO
email string
fullname string
}
var users = map[string]*userinfo{}
func init() {
testing.Init()
flag.Parse()
}
var logErr *log.Logger
var logDebug *log.Logger
func init() {
logErr = bblog.GetErr()
logDebug = bblog.GetDebug(*verbose)
}
func getVcs(t *testing.T, name string) vcs.Vcs {
t.Helper()
// Set up the vcs
for _, v := range vcs.Catalog {
logDebug.Printf("Testing vcs: %v == %v", name, v.Name)
if strings.ToLower(v.Name) == strings.ToLower(name) {
h, err := v.New()
if err != nil {
return nil // No idea how that would happen.
}
return h
}
logDebug.Println("...Nope.")
}
return nil
}
// TestBasicCommands's helpers
func makeHomeDir(t *testing.T, testname string) {
t.Helper()
var homedir string
var err error
if *nocleanup {
// Make a predictable location; don't deleted.
homedir = "/tmp/bbhome-" + testname
os.RemoveAll(homedir)
err = os.Mkdir(homedir, 0770)
if err != nil {
t.Fatal(fmt.Errorf("mk-home %q: %v", homedir, err))
}
} else {
// Make a random location that is deleted automatically
homedir, err = ioutil.TempDir("", filepath.Join("bbhome-"+testname))
defer os.RemoveAll(homedir) // clean up
if err != nil {
t.Fatal(err)
}
}
err = os.Setenv("HOME", homedir)
if err != nil {
t.Fatal(err)
}
logDebug.Printf("TESTING DIR HOME: cd %v\n", homedir)
repodir := filepath.Join(homedir, "repo")
err = os.Mkdir(repodir, 0770)
if err != nil {
t.Fatal(fmt.Errorf("mk-repo %q: %v", repodir, err))
}
err = os.Chdir(repodir)
if err != nil {
t.Fatal(err)
}
}
func createDummyFilesAdmin(t *testing.T) {
// This creates a repo with real data, except any .gpg file
// is just garbage.
addLineSorted(t, ".blackbox/blackbox-admins.txt", "user1@example.com")
addLineSorted(t, ".blackbox/blackbox-admins.txt", "user2@example.com")
addLineSorted(t, ".blackbox/blackbox-files.txt", "foo.txt")
addLineSorted(t, ".blackbox/blackbox-files.txt", "bar.txt")
makeFile(t, "foo.txt", "I am the foo.txt file!")
makeFile(t, "bar.txt", "I am the foo.txt file!")
makeFile(t, "foo.txt.gpg", "V nz gur sbb.gkg svyr!")
makeFile(t, "bar.txt.gpg", "V nz gur one.gkg svyr!")
}
func createFilesStatus(t *testing.T) {
// This creates a few files with real plaintext but fake cyphertext.
// There are a variety of timestamps to enable many statuses.
t.Helper()
// DECRYPTED: File is decrypted and ready to edit (unknown if it has been edited).
// ENCRYPTED: GPG file is newer than plaintext. Indicates recented edited then encrypted.
// SHREDDED: Plaintext is missing.
// GPGMISSING: The .gpg file is missing. Oops?
// PLAINERROR: Can't access the plaintext file to determine status.
// GPGERROR: Can't access .gpg file to determine status.
addLineSorted(t, ".blackbox/blackbox-files.txt", "status-DECRYPTED.txt")
addLineSorted(t, ".blackbox/blackbox-files.txt", "status-ENCRYPTED.txt")
addLineSorted(t, ".blackbox/blackbox-files.txt", "status-SHREDDED.txt")
addLineSorted(t, ".blackbox/blackbox-files.txt", "status-GPGMISSING.txt")
// addLineSorted(t, ".blackbox/blackbox-files.txt", "status-PLAINERROR.txt")
// addLineSorted(t, ".blackbox/blackbox-files.txt", "status-GPGERROR.txt")
addLineSorted(t, ".blackbox/blackbox-files.txt", "status-BOTHMISSING.txt")
// Combination of age difference either missing, file error, both missing.
makeFile(t, "status-DECRYPTED.txt", "File with DECRYPTED in it.")
makeFile(t, "status-DECRYPTED.txt.gpg", "Svyr jvgu QRPELCGRQ va vg.")
makeFile(t, "status-ENCRYPTED.txt", "File with ENCRYPTED in it.")
makeFile(t, "status-ENCRYPTED.txt.gpg", "Svyr jvgu RAPELCGRQ va vg.")
// Plaintext intentionally missing.
makeFile(t, "status-SHREDDED.txt.gpg", "Svyr jvgu FUERQQRQ va vg.")
makeFile(t, "status-GPGMISSING.txt", "File with GPGMISSING in it.")
// gpg file intentionally missing.
// Plaintext intentionally missing. ("status-BOTHMISSING.txt")
// gpg file intentionally missing. ("status-BOTHMISSING.txt.gpg")
// NB(tlim): commented out. I can't think of an error I can reproduce.
// makeFile(t, "status-PLAINERROR.txt", "File with PLAINERROR in it.")
// makeFile(t, "status-PLAINERROR.txt.gpg", "Svyr jvgu CYNVAREEBE va vg.")
// setFilePerms(t, "status-PLAINERROR.txt", 0000)
// NB(tlim): commented out. I can't think of an error I can reproduce.
// makeFile(t, "status-GPGERROR.txt", "File with GPGERROR in it.")
// makeFile(t, "status-GPGERROR.txt.gpg", "Svyr jvgu TCTREEBE va vg.")
// setFilePerms(t, "status-GPGERROR.txt.gpg", 0000)
time.Sleep(200 * time.Millisecond)
if err := bbutil.Touch("status-DECRYPTED.txt"); err != nil {
t.Fatal(err)
}
if err := bbutil.Touch("status-ENCRYPTED.txt.gpg"); err != nil {
t.Fatal(err)
}
}
func addLineSorted(t *testing.T, filename, line string) {
err := bbutil.AddLinesToSortedFile(filename, line)
if err != nil {
t.Fatalf("addLineSorted failed: %v", err)
}
}
func removeFile(t *testing.T, name string) {
os.RemoveAll(name)
}
func makeFile(t *testing.T, name string, content string) {
t.Helper()
err := ioutil.WriteFile(name, []byte(content), 0666)
if err != nil {
t.Fatalf("makeFile can't create %q: %v", name, err)
}
}
func setFilePerms(t *testing.T, name string, perms int) {
t.Helper()
err := os.Chmod(name, os.FileMode(perms))
if err != nil {
t.Fatalf("setFilePerms can't chmod %q: %v", name, err)
}
}
var originPath string // CWD when program started.
// checkOutput runs blackbox with args, the last arg is the filename
// of the expected output. Error if output is not expected.
func checkOutput(name string, t *testing.T, args ...string) {
t.Helper()
cmd := exec.Command(PathToBlackBox(), args...)
cmd.Stdin = nil
cmd.Stdout = nil
cmd.Stderr = os.Stderr
var gb []byte
gb, err := cmd.Output()
if err != nil {
t.Fatal(fmt.Errorf("checkOutput(%q): %w", args, err))
}
got := string(gb)
wb, err := ioutil.ReadFile(filepath.Join(originPath, "test_data", name))
if err != nil {
t.Fatalf("checkOutput can't read %v: %v", name, err)
}
want := string(wb)
//fmt.Printf("CHECKOUTPUT g: %v\n", got)
//fmt.Printf("CHECKOUTPUT w: %v\n", want)
if g, w := got, want; g != w {
t.Errorf("checkOutput(%q) mismatch (-got +want):\n%s",
args, diff.LineDiff(g, w))
}
}
func invalidArgs(t *testing.T, args ...string) {
t.Helper()
logDebug.Printf("invalidArgs(%q): \n", args)
cmd := exec.Command(PathToBlackBox(), args...)
cmd.Stdin = nil
if *verbose {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
err := cmd.Run()
if err == nil {
logDebug.Println("BAD")
t.Fatal(fmt.Errorf("invalidArgs(%q): wanted failure but got success", args))
}
logDebug.Printf("^^^^ (correct error received): err=%q\n", err)
}
// TestAliceAndBob's helpers.
func setupUser(t *testing.T, user, passphrase string) {
t.Helper()
logDebug.Printf("DEBUG: setupUser %q %q\n", user, passphrase)
}
var pathToBlackBox string
// PathToBlackBox returns the path to the executable we compile for integration testing.
func PathToBlackBox() string { return pathToBlackBox }
// SetPathToBlackBox sets the path.
func SetPathToBlackBox(n string) {
logDebug.Printf("PathToBlackBox=%q\n", n)
pathToBlackBox = n
}
func runBB(t *testing.T, args ...string) {
t.Helper()
logDebug.Printf("runBB(%q)\n", args)
cmd := exec.Command(PathToBlackBox(), args...)
cmd.Stdin = nil
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
t.Fatal(fmt.Errorf("runBB(%q): %w", args, err))
}
}
func phase(msg string) {
logDebug.Println("********************")
logDebug.Println("********************")
logDebug.Printf("********* %v\n", msg)
logDebug.Println("********************")
logDebug.Println("********************")
}
func makeAdmin(t *testing.T, name, fullname, email string) string {
testing.Init()
dir, err := filepath.Abs(filepath.Join(os.Getenv("HOME"), ".gnupg-"+name))
if err != nil {
t.Fatal(err)
}
os.Mkdir(dir, 0700)
u := &userinfo{
name: name,
dir: dir,
fullname: fullname,
email: email,
}
users[name] = u
// GNUPGHOME=u.dir
// echo 'pinentry-program' "$(which pinentry-tty)" >> "$GNUPGHOME/gpg-agent.conf"
os.Setenv("GNUPGHOME", u.dir)
if runtime.GOOS != "darwin" {
ai, err := bbutil.RunBashOutput("gpg-agent", "--homedir", u.dir, "--daemon")
// NB(tlim): It should return something like:
// `GPG_AGENT_INFO=/home/tlimoncelli/.gnupg/S.gpg-agent:18548:1; export GPG_AGENT_INFO;`
if err != nil {
//t.Fatal(err)
}
if !strings.HasPrefix(ai, "GPG_AGENT_INFO=") {
fmt.Println("WARNING: gpg-agent didn't output what we expected. Assumed dead.")
} else {
u.agentInfo = ai[15:strings.Index(ai, ";")]
os.Setenv("GPG_AGENT_INFO", u.agentInfo)
fmt.Printf("GPG_AGENT_INFO=%q (was %q)\n", ai, u.agentInfo)
}
}
os.Setenv("GNUPGHOME", u.dir)
// Generate key:
if hasQuick(t) {
fmt.Println("DISCOVERED: NEW GPG")
fmt.Printf("Generating %q using --qgk\n", u.email)
bbutil.RunBash("gpg",
"--homedir", u.dir,
"--batch",
"--passphrase", "",
"--quick-generate-key", u.email,
)
if err != nil {
t.Fatal(err)
}
} else {
fmt.Println("DISCOVERED: OLD GPG")
fmt.Println("MAKING KEY")
tmpfile, err := ioutil.TempFile("", "example")
if err != nil {
log.Fatal(err)
}
defer os.Remove(tmpfile.Name()) // clean up
batch := `%echo Generating a basic OpenPGP key
Key-Type: RSA
Key-Length: 2048
Subkey-Type: RSA
Subkey-Length: 2048
Name-Real: ` + u.fullname + `
Name-Comment: Not for actual use
Name-Email: ` + u.email + `
Expire-Date: 0
%pubring ` + filepath.Join(u.dir, `pubring.gpg`) + `
%secring ` + filepath.Join(u.dir, `secring.gpg`) + `
# Do a commit here, so that we can later print "done"
%commit
%echo done`
//fmt.Printf("BATCH START\n%s\nBATCH END\n", batch)
fmt.Fprintln(tmpfile, batch)
// FIXME(tlim): The batch file should include a password, but then
// we need to figure out how to get "blackbox encrypt" and other
// commands to input a password in an automated way.
// To experiment with this, add after "Expire-Date:" a line like:
// Passphrase: kljfhslfjkhsaljkhsdflgjkhsd
// Current status: without that line GPG keys have no passphrase
// and none is requested.
bbutil.RunBash("gpg",
"--homedir", u.dir,
"--verbose",
"--batch",
"--gen-key",
tmpfile.Name(),
)
if err != nil {
t.Fatal(err)
}
if err := tmpfile.Close(); err != nil {
log.Fatal(err)
}
// We do this just to for gpg to create trustdb.gpg
bbutil.RunBash("gpg",
"--homedir", u.dir,
"--list-keys",
)
if err != nil {
t.Fatal(err)
}
bbutil.RunBash("gpg",
"--homedir", u.dir,
"--list-secret-keys",
)
if err != nil {
t.Fatal(err)
}
}
return u.dir
}
func hasQuick(t *testing.T) bool {
testing.Init()
fmt.Println("========== Do we have --quick-generate-key?")
err := bbutil.RunBash("gpg2",
"--dry-run",
"--quick-generate-key",
"--batch",
"--passphrase", "",
"foo", "rsa", "encr")
fmt.Println("========== Done")
if err == nil {
return true
}
//fmt.Printf("DISCOVER GPG: %d", err.ExitCode())
if exitError, ok := err.(*exec.ExitError); ok {
if exitError.ExitCode() == 0 {
return true
}
}
return false
}
func become(t *testing.T, name string) {
testing.Init()
u := users[name]
os.Setenv("GNUPGHOME", u.dir)
os.Setenv("GPG_AGENT_INFO", u.agentInfo)
bbutil.RunBash("git", "config", "user.name", u.name)
bbutil.RunBash("git", "config", "user.email", u.fullname)
}
// // Get fingerprint:
// // Retrieve fingerprint of generated key.
// // Use it to extract the secret/public keys.
// // (stolen from https://raymii.org/s/articles/GPG_noninteractive_batch_sign_trust_and_send_gnupg_keys.html)
//
// // fpr=`gpg --homedir /tmp/blackbox_createrole --fingerprint --with-colons "$ROLE_NAME" | awk -F: '/fpr:/ {print $10}' | head -n 1`
// var fpr string
// bbutil.RunBashOutput("gpg",
// "--homedir", "/tmp/blackbox_createrole",
// "--fingerprint",
// "--with-colons",
// u.email,
// )
// for i, l := range string.Split(out, "\n") {
// if string.HasPrefix(l, "fpr:") {
// fpr = strings.Split(l, ":")[9]
// }
// break
// }
//
// // Create key key:
// // gpg --homedir "$gpghomedir" --batch --passphrase '' --quick-add-key "$fpr" rsa encr
// bbutil.RunBash("gpg",
// "--homedir", u.dir,
// "--batch",
// "--passphrase", "",
// "--quick-add-key", fpr,
// "rsa", "encr",
// )
// function md5sum_file() {
// # Portably generate the MD5 hash of file $1.
// case $(uname -s) in
// Darwin | FreeBSD )
// md5 -r "$1" | awk '{ print $1 }'
// ;;
// NetBSD )
// md5 -q "$1"
// ;;
// SunOS )
// digest -a md5 "$1"
// ;;
// Linux )
// md5sum "$1" | awk '{ print $1 }'
// ;;
// CYGWIN* )
// md5sum "$1" | awk '{ print $1 }'
// ;;
// * )
// echo 'ERROR: Unknown OS. Exiting.'
// exit 1
// ;;
// esac
// }
//
// function assert_file_missing() {
// if [[ -e "$1" ]]; then
// echo "ASSERT FAILED: ${1} should not exist."
// exit 1
// fi
// }
//
// function assert_file_exists() {
// if [[ ! -e "$1" ]]; then
// echo "ASSERT FAILED: ${1} should exist."
// echo "PWD=$(/usr/bin/env pwd -P)"
// #echo "LS START"
// #ls -la
// #echo "LS END"
// exit 1
// fi
// }
// function assert_file_md5hash() {
// local file="$1"
// local wanted="$2"
// assert_file_exists "$file"
// local found
// found=$(md5sum_file "$file")
// if [[ "$wanted" != "$found" ]]; then
// echo "ASSERT FAILED: $file hash wanted=$wanted found=$found"
// exit 1
// fi
// }
// function assert_file_group() {
// local file="$1"
// local wanted="$2"
// local found
// assert_file_exists "$file"
//
// case $(uname -s) in
// Darwin | FreeBSD | NetBSD )
// found=$(stat -f '%Dg' "$file")
// ;;
// Linux | SunOS )
// found=$(stat -c '%g' "$file")
// ;;
// CYGWIN* )
// echo "ASSERT_FILE_GROUP: Running on Cygwin. Not being tested."
// return 0
// ;;
// * )
// echo 'ERROR: Unknown OS. Exiting.'
// exit 1
// ;;
// esac
//
// echo "DEBUG: assert_file_group X${wanted}X vs. X${found}X"
// echo "DEBUG:" $(which stat)
// if [[ "$wanted" != "$found" ]]; then
// echo "ASSERT FAILED: $file chgrp group wanted=$wanted found=$found"
// exit 1
// fi
// }
// function assert_file_perm() {
// local wanted="$1"
// local file="$2"
// local found
// assert_file_exists "$file"
//
// case $(uname -s) in
// Darwin | FreeBSD | NetBSD )
// found=$(stat -f '%Sp' "$file")
// ;;
// # NB(tlim): CYGWIN hasn't been tested. It might be more like Darwin.
// Linux | CYGWIN* | SunOS )
// found=$(stat -c '%A' "$file")
// ;;
// * )
// echo 'ERROR: Unknown OS. Exiting.'
// exit 1
// ;;
// esac
//
// echo "DEBUG: assert_file_perm X${wanted}X vs. X${found}X"
// echo "DEBUG:" $(which stat)
// if [[ "$wanted" != "$found" ]]; then
// echo "ASSERT FAILED: $file chgrp perm wanted=$wanted found=$found"
// exit 1
// fi
// }
// function assert_line_not_exists() {
// local target="$1"
// local file="$2"
// assert_file_exists "$file"
// if grep -F -x -s -q >/dev/null "$target" "$file" ; then
// echo "ASSERT FAILED: line '$target' should not exist in file $file"
// echo "==== file contents: START $file"
// cat "$file"
// echo "==== file contents: END $file"
// exit 1
// fi
// }
// function assert_line_exists() {
// local target="$1"
// local file="$2"
// assert_file_exists "$file"
// if ! grep -F -x -s -q >/dev/null "$target" "$file" ; then
// echo "ASSERT FAILED: line '$target' should exist in file $file"
// echo "==== file contents: START $file"
// cat "$file"
// echo "==== file contents: END $file"
// exit 1
// fi
// }

View File

@@ -0,0 +1,2 @@
user1@example.com
user2@example.com

View File

@@ -0,0 +1,2 @@
bar.txt
foo.txt

View File

@@ -0,0 +1,9 @@
+-------------+------------------------+
| STATUS | NAME |
+-------------+------------------------+
| BOTHMISSING | status-BOTHMISSING.txt |
| DECRYPTED | status-DECRYPTED.txt |
| ENCRYPTED | status-ENCRYPTED.txt |
| GPGMISSING | status-GPGMISSING.txt |
| SHREDDED | status-SHREDDED.txt |
+-------------+------------------------+

View File

@@ -0,0 +1 @@
I am the foo.txt file!

View File

@@ -0,0 +1,5 @@
+-----------+---------+
| STATUS | NAME |
+-----------+---------+
| ENCRYPTED | foo.txt |
+-----------+---------+

View File

@@ -0,0 +1 @@
I am the foo.txt file!

View File

@@ -0,0 +1,6 @@
+-----------+----------------------+
| STATUS | NAME |
+-----------+----------------------+
| ENCRYPTED | status-ENCRYPTED.txt |
| NOTREG | blah.txt |
+-----------+----------------------+