diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e805f3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +releases +a +t +gowipe diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d25b893 --- /dev/null +++ b/Makefile @@ -0,0 +1,89 @@ +# Copyright © 2023 Thomas von Dein + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +# +# no need to modify anything below +tool = gowipe +VERSION = $(shell grep VERSION main.go | head -1 | cut -d '"' -f2) +archs = darwin freebsd linux windows +PREFIX = /usr/local +UID = root +GID = 0 +HAVE_POD := + +all: $(tool) buildlocal + +buildlocal: + CGO_LDFLAGS='-static' go build -tags osusergo,netgo -ldflags "-extldflags=-static" -o $(tool) + +install: buildlocal + install -d -o $(UID) -g $(GID) $(PREFIX)/bin + install -d -o $(UID) -g $(GID) $(PREFIX)/man/man1 + install -o $(UID) -g $(GID) -m 555 $(tool) $(PREFIX)/sbin/ + install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/ + +clean: + rm -rf $(tool) coverage.out + +test: + go test -v ./... + +singletest: + @echo "Call like this: ''make singletest TEST=TestPrepareColumns" + go test -run $(TEST) + +cover-report: + go test ./... -cover -coverprofile=coverage.out + go tool cover -html=coverage.out + +goupdate: + go get -t -u=patch ./... + +buildall: + ./mkrel.sh $(tool) $(VERSION) + +release: buildall + gh release create v$(VERSION) --generate-notes releases/* + +show-versions: buildlocal + @echo "### gowipe version:" + @./gowipe -v + + @echo + @echo "### go module versions:" + @go list -m all + + @echo + @echo "### go version used for building:" + @grep -m 1 go go.mod + + +dir: + rm -rf a + mkdir -p a/b/c + date > a/filea + date > a/b/fileb + date > a/b/c/filec + +bench: all + dd if=/dev/zero of=t/fileZ bs=1024 count=200000 + dd if=/dev/zero of=t/fileM bs=1024 count=200000 + dd if=/dev/zero of=t/fileS bs=1024 count=200000 + dd if=/dev/zero of=t/fileE bs=1024 count=200000 + /usr/bin/time -f "%S" ./gowipe -Z t/fileZ + /usr/bin/time -f "%S" ./gowipe -M t/fileM + /usr/bin/time -f "%S" ./gowipe -S t/fileS + /usr/bin/time -f "%S" ./gowipe -E t/fileE diff --git a/README.md b/README.md new file mode 100644 index 0000000..c72f28f Binary files /dev/null and b/README.md differ diff --git a/crypto.go b/crypto.go new file mode 100644 index 0000000..a977af5 --- /dev/null +++ b/crypto.go @@ -0,0 +1,244 @@ +/* +Copyright © 2022 Thomas von Dein + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ +package main + +import ( + "crypto/cipher" + cryptorand "crypto/rand" + "errors" + "fmt" + "io" + "math/big" + mathrand "math/rand" + "os" + "time" + "unsafe" + + "golang.org/x/crypto/argon2" + chapo "golang.org/x/crypto/chacha20poly1305" +) + +const ( + SaltSize = 32 // in bytes + NonceSize = 24 // in bytes. taken from aead.NonceSize() + KeySize = uint32(32) // KeySize is 32 bytes (256 bits). + KeyTime = uint32(5) + KeyMemory = uint32(1024 * 64) // KeyMemory in KiB. here, 64 MiB. + KeyThreads = uint8(4) + chunkSize = 1024 * 32 // chunkSize in bytes. here, 32 KiB. + + letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-" + + letterIdxBits = 6 // 6 bits to represent a letter index + letterIdxMask = 1<= 0; { + if remain == 0 { + cache, remain = src.Int63(), letterIdxMax + } + if idx := int(cache & letterIdxMask); idx < len(letters) { + b[i] = letters[idx] + i-- + } + cache >>= letterIdxBits + remain-- + } + + return *(*string)(unsafe.Pointer(&b)) +} + +func GetRandomKey() ([]byte, error) { + password, err := GenerateSecureRandomBytes(int(chapo.KeySize)) + if err != nil { + return nil, err + } + + salt, err := GenerateSecureRandomBytes(chapo.NonceSize) + if err != nil { + return nil, err + } + + key := argon2.IDKey(password, salt, KeyTime, KeyMemory, KeyThreads, chapo.KeySize) + + return key, nil +} + +func Encrypt(c *Conf, filename string) error { + info, err := os.Stat(filename) + if err != nil { + return err + } + + size := info.Size() + + outfile, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0666) + if err != nil { + return err + } + defer outfile.Close() + + key, err := GetRandomKey() + if err != nil { + return err + } + + aead, err := chapo.NewX(key) + if err != nil { + return err + } + + for i := 0; i < c.count; i++ { + for { + EncryptChunk(aead, outfile, size) + size = size - chunkSize + + if size <= 0 { + break + } + } + } + + return nil +} + +func EncryptChunk(aead cipher.AEAD, file *os.File, size int64) error { + chunk := make([]byte, size) + nonce, err := GenerateSecureRandomBytes(int(chapo.NonceSize)) + if err != nil { + return err + } + + cipher := aead.Seal(nil, nonce, chunk, nil) + + n, err := file.Write(cipher[:size]) + if err != nil { + return err + } + + if int64(n) != size { + return errors.New("invalid number of bytes written") + } + + return nil +} + +/* +func Encrypt(c *Conf, filename string) error { + salt, err := GetRand(KeySize) + if err != nil { + return err + } + + salt1, err := GetRand(KeySize) + if err != nil { + return err + } + + outfile, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0666) + if err != nil { + return err + } + defer outfile.Close() + + key := argon2.IDKey(salt1, salt, KeyTime, KeyMemory, KeyThreads, KeySize) + + aead, err := chacha20poly1305.NewX(key) + if err != nil { + return err + } + + buf := make([]byte, chunkSize) + ad_counter := 0 // associated data is a counter + + for { + if n > 0 { + // Select a random nonce, and leave capacity for the ciphertext. + nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+n+aead.Overhead()) + if m, err := cryptorand.Read(nonce); err != nil || m != aead.NonceSize() { + return err + } + + msg := buf[:n] + // Encrypt the message and append the ciphertext to the nonce. + encryptedMsg := aead.Seal(nonce, nonce, msg, []byte(string(ad_counter))) + outfile.Write(encryptedMsg) + ad_counter += 1 + } + + if err == io.EOF { + break + } + + if err != nil { + log.Println("Error when reading input file chunk :", err) + panic(err) + } + } +} +*/ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..02fb021 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module gowipe + +go 1.20 + +require ( + github.com/JojiiOfficial/shred v1.2.1 // indirect + github.com/lu4p/shred v0.0.0-20201211173428-0347b645d724 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/crypto v0.15.0 // indirect + golang.org/x/sys v0.14.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..049ed97 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/JojiiOfficial/shred v1.2.1 h1:658CFVTqcAkYVg815vW+guYnyJTLOIoS15tMyPTYhNo= +github.com/JojiiOfficial/shred v1.2.1/go.mod h1:/OAxd6eYOhrXb3KW+2wmDog2BiFlUld8oJEKa+xblxU= +github.com/lu4p/shred v0.0.0-20201211173428-0347b645d724 h1:nLJRUakdvy2j7JsefrtAUqGRbfJUKKqRu/3BCRA9mIQ= +github.com/lu4p/shred v0.0.0-20201211173428-0347b645d724/go.mod h1:6b1kEKx7IPBboPSTnoJZE5sbSDjcNkHHO3Hii8TU8XY= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/main.go b/main.go new file mode 100644 index 0000000..b9ed8fe --- /dev/null +++ b/main.go @@ -0,0 +1,238 @@ +/* +Copyright © 2022 Thomas von Dein + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ +package main + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" + + "github.com/JojiiOfficial/shred" + flag "github.com/spf13/pflag" +) + +const VERSION string = "0.0.2" +const Usage string = `This is gowipe - destruct files in a non-recoverable way. + +Usage: gowipe [-rcvz] ... + +Options: +-r --recursive Delete recursively +-c --count Overwrite files times +-m --mode Use for overwriting (or use -E, -S, -M, -Z) +-n --nodelete Do not delete files after overwriting +-N --norename Do not rename the files +-v --verbose Verbose output +-V --version Show program version +-h --help Show usage + +Available modes: +zero Overwrite with zeroes (-Z) +math Overwrite with math random bytes (-M) +secure Overwrite with secure random bytes (default) (-S) +encrypt Overwrite with ChaCha2Poly1305 encryption (most secure) (-E)` + +type Conf struct { + mode string + count int + recurse bool + nodelete bool + norename bool + verbose bool +} + +func main() { + showversion := false + showhelp := false + optzero := false + optsecure := false + optmath := false + optencrypt := false + + c := Conf{ + verbose: false, + mode: `secure`, + count: 30, + recurse: false, + nodelete: false, + norename: false, + } + + flag.BoolVarP(&showversion, "version", "V", showversion, "show version") + flag.BoolVarP(&showhelp, "help", "h", showversion, "show help") + flag.BoolVarP(&c.verbose, "verbose", "v", c.verbose, "verbose") + + flag.StringVarP(&c.mode, "mode", "m", c.mode, "overwrite mode") + + flag.BoolVarP(&optzero, "zero", "Z", optzero, "zero mode") + flag.BoolVarP(&optsecure, "secure", "S", optsecure, "secure mode") + flag.BoolVarP(&optmath, "math", "M", optmath, "math mode") + flag.BoolVarP(&optmath, "encrypt", "E", optmath, "encrypt mode") + + flag.BoolVarP(&c.recurse, "recursive", "r", c.recurse, "recursive") + flag.BoolVarP(&c.nodelete, "nodelete", "n", c.nodelete, "don't delete") + flag.BoolVarP(&c.norename, "norename", "N", c.norename, "don't rename") + flag.IntVarP(&c.count, "count", "c", c.count, "overwrite count") + + flag.Parse() + + if showversion { + fmt.Printf("This is gowipe version %s\n", VERSION) + os.Exit(0) + } + + if showhelp { + fmt.Println(Usage) + os.Exit(0) + } + + if len(flag.Args()) == 0 { + fmt.Println(Usage) + os.Exit(0) + } + + var option shred.WriteOptions + + if optzero { + option = shred.WriteZeros + } + if optmath { + option = shred.WriteRand + } + if optsecure { + option = shred.WriteRandSecure + } + if optencrypt { + c.mode = "encrypt" + } + + switch c.mode { + case `secure`: + option = shred.WriteRandSecure + case `math`: + option = shred.WriteRand + case `zero`: + option = shred.WriteZeros + case `encrypt`: + optencrypt = true + default: + option = shred.WriteRandSecure + } + + shredder := shred.Shredder{} + shredconf := shred.NewShredderConf(&shredder, option, c.count, !c.nodelete) + + for _, file := range flag.Args() { + Wipe(file, &c, shredconf) + } +} + +func Wipe(file string, c *Conf, wiper *shred.ShredderConf) { + if info, err := os.Stat(file); err == nil { + + if info.IsDir() { + if !c.recurse { + fmt.Printf("-r not set, ignoring directory %s\n", file) + return + } + + files, err := ioutil.ReadDir(file) + if err != nil { + log.Fatal(err) + } + + for _, entry := range files { + Wipe(filepath.Join(file, entry.Name()), c, wiper) + } + + if !c.nodelete { + err = os.Remove(Rename(file, c)) + if err != nil { + log.Fatal(err) + } + } + } else { + if c.mode == "encrypt" { + err := Encrypt(c, file) + if err != nil { + log.Fatal(err) + } + + Rename(file, c) + } else { + wiper.ShredFile(Rename(file, c)) + } + } + + if c.verbose { + fmt.Printf("Wiped %d times: %s\n", c.count, file) + } + } else { + if os.IsNotExist(err) { + fmt.Printf("No such file or directory: %s\n", file) + } else { + fmt.Println(err) + } + + os.Exit(1) + } +} + +func Rename(file string, c *Conf) string { + var newname string + dir := filepath.Dir(file) + base := filepath.Base(file) + length := len(base) + + for i := 0; i < c.count; i++ { + for { + switch c.mode { + case `secure`: + new, err := GenerateSecureRandomString(length) + if err != nil { + log.Fatal(err) + } + newname = new + case `math`: + newname = GenerateMathRandomString(length) + case `zero`: + newname = strings.Repeat("0", length) + } + if newname != base { + break + } + } + + /* + if c.verbose { + fmt.Printf("renaming %s/%s => %s/%s\n", dir, base, dir, newname) + } + */ + + err := os.Rename(filepath.Join(dir, base), filepath.Join(dir, newname)) + if err != nil { + log.Fatal(err) + } + + base = newname + } + + return filepath.Join(dir, newname) +} diff --git a/mkrel.sh b/mkrel.sh new file mode 100755 index 0000000..894deaf --- /dev/null +++ b/mkrel.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +# Copyright © 2022 Thomas von Dein + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +# get list with: go tool dist list +DIST="darwin/amd64 +freebsd/amd64 +linux/amd64 +netbsd/amd64 +openbsd/amd64 +windows/amd64" + +tool="$1" +version="$2" + +if test -z "$version"; then + echo "Usage: $0 " + exit 1 +fi + +rm -rf releases +mkdir -p releases + + +for D in $DIST; do + os=${D/\/*/} + arch=${D/*\//} + binfile="releases/${tool}-${os}-${arch}-${version}" + tardir="${tool}-${os}-${arch}-${version}" + tarfile="releases/${tool}-${os}-${arch}-${version}.tar.gz" + set -x + GOOS=${os} GOARCH=${arch} go build -o ${binfile} -ldflags "-X 'github.com/tlinden/tablizer/cfg.VERSION=${version}'" + mkdir -p ${tardir} + cp ${binfile} README.md LICENSE ${tardir}/ + echo 'tool = tablizer +PREFIX = /usr/local +UID = root +GID = 0 + +install: + install -d -o $(UID) -g $(GID) $(PREFIX)/bin + install -d -o $(UID) -g $(GID) $(PREFIX)/man/man1 + install -o $(UID) -g $(GID) -m 555 $(tool) $(PREFIX)/sbin/ + install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/' > ${tardir}/Makefile + tar cpzf ${tarfile} ${tardir} + sha256sum ${binfile} | cut -d' ' -f1 > ${binfile}.sha256 + sha256sum ${tarfile} | cut -d' ' -f1 > ${tarfile}.sha256 + rm -rf ${tardir} + set +x +done +