Skip to content

Commit 989f3fa

Browse files
committed
New feature: Shred files and directories
The new 'shred' command allows to safely remove files and directories from the filesystem while ensuring dangling data is not recoverable.
1 parent 43a7380 commit 989f3fa

File tree

10 files changed

+266
-686
lines changed

10 files changed

+266
-686
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
- name: Set up Go
1818
uses: actions/setup-go@v3
1919
with:
20-
go-version: 1.18.x
20+
go-version: 1.19.x
2121

2222
# Checkout code
2323
- name: Checkout repository

.github/workflows/maintenance.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ jobs:
77
name: "close stale issues and pull requests"
88
runs-on: ubuntu-latest
99
steps:
10-
- uses: actions/stale@v5
10+
- uses: actions/stale@v6
1111
with:
1212
# On the 'debug' mode the action will not perform any operation.
1313
# Add the secret ACTIONS_STEP_DEBUG with a value of 'true' in the repository.

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
- name: Set up Go
1616
uses: actions/setup-go@v3
1717
with:
18-
go-version: 1.18.x
18+
go-version: 1.19.x
1919

2020
# Checkout code
2121
- name: Checkout repository

.golangci.yml

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,7 @@ run:
33
issues-exit-code: 1
44
tests: true
55
build-tags: []
6-
skip-dirs:
7-
- vendor$
8-
- third_party$
9-
- testdata$
10-
- examples$
11-
- Godeps$
12-
- builtin$
6+
skip-dirs-use-default: true
137
skip-files:
148
- ".*\\.pb\\.go$"
159
- ".*\\.pb\\.gw\\.go$"
@@ -28,7 +22,6 @@ linters:
2822
- gofmt
2923
- ineffassign
3024
- staticcheck
31-
- structcheck
3225
- typecheck
3326
- varcheck
3427
- gocyclo
@@ -52,7 +45,6 @@ linters:
5245
- noctx
5346
- predeclared
5447
- exportloopref
55-
- wastedassign
5648
- whitespace
5749
#- wrapcheck
5850
#- nestif
@@ -62,6 +54,9 @@ linters:
6254
- deadcode
6355
- unused
6456
- dupl
57+
# https://github.com/golangci/golangci-lint/issues/2649
58+
- structcheck
59+
- wastedassign
6560
issues:
6661
exclude-use-default: false
6762
exclude-rules:

cmd/decrypt.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ func runDecrypt(_ *cobra.Command, args []string) error {
116116
}
117117

118118
// Start tred workers pool
119-
pool, err := newPool(viper.GetInt("decrypt.workers"), key, viper.GetString("decrypt.cipher"), log)
119+
wp, err := newPool(viper.GetInt("decrypt.workers"), key, viper.GetString("decrypt.cipher"), log)
120120
if err != nil {
121121
log.WithField("error", err).Fatal("could not initialize TRED workers")
122122
return err
@@ -157,19 +157,19 @@ func runDecrypt(_ *cobra.Command, args []string) error {
157157
}
158158

159159
// Add job to processing pool
160-
pool.add(job{file: f, showBar: false})
160+
wp.add(job{file: f, showBar: false})
161161
return nil
162162
})
163163
} else {
164164
// Process single file
165-
pool.add(job{file: input, showBar: true})
165+
wp.add(job{file: input, showBar: true})
166166
}
167167

168168
// Wait for operations to complete
169-
pool.done()
169+
wp.done()
170170
log.WithFields(xlog.Fields{
171171
"time": time.Since(start),
172-
"files": pool.count,
172+
"files": wp.count,
173173
}).Info("operation completed")
174174
return err
175175
}

cmd/encrypt.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ func runEncrypt(_ *cobra.Command, args []string) error {
116116
}
117117

118118
// Start tred workers pool
119-
pool, err := newPool(viper.GetInt("encrypt.workers"), key, viper.GetString("encrypt.cipher"), log)
119+
wp, err := newPool(viper.GetInt("encrypt.workers"), key, viper.GetString("encrypt.cipher"), log)
120120
if err != nil {
121121
log.WithField("error", err).Fatal("could not initialize TRED workers")
122122
return err
@@ -157,19 +157,19 @@ func runEncrypt(_ *cobra.Command, args []string) error {
157157
}
158158

159159
// Add job to processing pool
160-
pool.add(job{file: f, showBar: false, encrypt: true})
160+
wp.add(job{file: f, showBar: false, encrypt: true})
161161
return nil
162162
})
163163
} else {
164164
// Process single file
165-
pool.add(job{file: input, showBar: true, encrypt: true})
165+
wp.add(job{file: input, showBar: true, encrypt: true})
166166
}
167167

168168
// Wait for operations to complete
169-
pool.done()
169+
wp.done()
170170
log.WithFields(xlog.Fields{
171171
"time": time.Since(start),
172-
"files": pool.count,
172+
"files": wp.count,
173173
}).Info("operation completed")
174174
return err
175175
}

cmd/pool.go

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type job struct {
1717
file string
1818
showBar bool
1919
encrypt bool
20+
shred bool
2021
}
2122

2223
type pool struct {
@@ -84,7 +85,9 @@ func (w *worker) run() {
8485
for j := range w.jobs {
8586
// Run operation
8687
var err error
87-
if j.encrypt {
88+
if j.shred {
89+
err = w.shred(j.file, j.showBar)
90+
} else if j.encrypt {
8891
err = w.encrypt(j.file, j.showBar)
8992
} else {
9093
err = w.decrypt(j.file, j.showBar)
@@ -199,3 +202,42 @@ func (w *worker) decrypt(file string, withBar bool) error {
199202
}
200203
return err
201204
}
205+
206+
// nolint: gosec
207+
func (w *worker) shred(file string, withBar bool) error {
208+
// Open input file
209+
input, err := os.Open(filepath.Clean(file))
210+
if err != nil {
211+
return err
212+
}
213+
214+
// Create new file for the ciphertext
215+
output, err := os.Create(fmt.Sprintf("%s%s", file, viper.GetString("encrypt.suffix")))
216+
if err != nil {
217+
return err
218+
}
219+
220+
// Get progress bar
221+
var r io.Reader
222+
r = input
223+
if !viper.GetBool("shred.silent") && withBar {
224+
bar := getProgressBar(input)
225+
bar.Start()
226+
defer bar.Finish()
227+
r = bar.NewProxyReader(input)
228+
}
229+
230+
// Encrypt file in-place
231+
_, err = w.tw.Encrypt(r, output)
232+
if err == nil {
233+
_ = input.Close()
234+
_ = output.Close()
235+
if err := os.Remove(input.Name()); err != nil {
236+
w.log.WithField("file", file).Warning("failed to remove file")
237+
}
238+
if err := os.Remove(output.Name()); err != nil {
239+
w.log.WithField("file", file).Warning("failed to remove file")
240+
}
241+
}
242+
return err
243+
}

cmd/shred.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package cmd
2+
3+
import (
4+
"crypto/rand"
5+
"os"
6+
"path/filepath"
7+
"runtime"
8+
"strings"
9+
"time"
10+
11+
"github.com/spf13/cobra"
12+
"github.com/spf13/viper"
13+
"go.bryk.io/pkg/cli"
14+
"go.bryk.io/pkg/errors"
15+
xlog "go.bryk.io/pkg/log"
16+
)
17+
18+
var shredCmd = &cobra.Command{
19+
Use: "shred",
20+
Aliases: []string{"del", "rm"},
21+
Example: "tred shred -ra [INPUT]",
22+
Short: "Securely delete files/directories while preventing contents recovery",
23+
RunE: runShred,
24+
}
25+
26+
func init() {
27+
params := []cli.Param{
28+
{
29+
Name: "recursive",
30+
Usage: "recursively process directories",
31+
FlagKey: "shred.recursive",
32+
ByDefault: false,
33+
Short: "r",
34+
},
35+
{
36+
Name: "all",
37+
Usage: "include hidden files",
38+
FlagKey: "shred.all",
39+
ByDefault: false,
40+
Short: "a",
41+
},
42+
{
43+
Name: "workers",
44+
Usage: "number or workers to run for parallel processing",
45+
FlagKey: "shred.workers",
46+
ByDefault: runtime.NumCPU(),
47+
Short: "w",
48+
},
49+
{
50+
Name: "silent",
51+
Usage: "suppress all output",
52+
FlagKey: "shred.silent",
53+
ByDefault: false,
54+
Short: "s",
55+
},
56+
}
57+
if err := cli.SetupCommandParams(shredCmd, params, viper.GetViper()); err != nil {
58+
panic(err)
59+
}
60+
rootCmd.AddCommand(shredCmd)
61+
}
62+
63+
func runShred(_ *cobra.Command, args []string) error {
64+
log := getLogger(viper.GetBool("shred.silent"))
65+
66+
// Get input
67+
if len(args) == 0 {
68+
log.Fatal("missing required input")
69+
return errors.New("missing required input")
70+
}
71+
72+
// Get input absolute path
73+
input, err := filepath.Abs(args[0])
74+
if err != nil {
75+
log.WithField("error", err).Fatal("invalid source provided")
76+
return err
77+
}
78+
79+
// Get temporary encryption key
80+
key := make([]byte, 64)
81+
if _, err := rand.Read(key); err != nil {
82+
return errors.Wrap(err, "generate encryption key")
83+
}
84+
85+
// Start tred workers pool
86+
wp, err := newPool(viper.GetInt("shred.workers"), key, "chacha", log)
87+
if err != nil {
88+
log.WithField("error", err).Fatal("could not initialize TRED workers")
89+
return err
90+
}
91+
92+
// Process input
93+
cleanUpDirs := []string{}
94+
start := time.Now()
95+
if isDir(input) {
96+
err = filepath.Walk(input, func(f string, i os.FileInfo, err error) error {
97+
// Unexpected error walking the directory
98+
if err != nil {
99+
log.WithFields(xlog.Fields{
100+
"location": f,
101+
"error": err,
102+
}).Warning("failed to traverse location")
103+
return err
104+
}
105+
106+
// Ignore hidden files
107+
if strings.HasPrefix(filepath.Base(f), ".") && !viper.GetBool("shred.all") {
108+
log.WithFields(xlog.Fields{
109+
"location": f,
110+
}).Debug("ignoring hidden file")
111+
return nil
112+
}
113+
114+
// Don't go into subdirectories if not required
115+
if i.IsDir() && !viper.GetBool("shred.recursive") {
116+
log.WithFields(xlog.Fields{
117+
"location": f,
118+
}).Debug("ignoring directory on non-recursive run")
119+
return filepath.SkipDir
120+
}
121+
122+
// Ignore subdir markers
123+
if i.IsDir() {
124+
cleanUpDirs = append(cleanUpDirs, f)
125+
return nil
126+
}
127+
128+
// Add job to processing pool
129+
wp.add(job{file: f, showBar: false, shred: true})
130+
return nil
131+
})
132+
cleanUpDirs = append(cleanUpDirs, input)
133+
} else {
134+
// Process single file
135+
wp.add(job{file: input, showBar: true, shred: true})
136+
}
137+
138+
// Wait for operations to complete
139+
wp.done()
140+
for _, td := range cleanUpDirs {
141+
_ = os.Remove(td) // remove empty directories, ignore errors
142+
}
143+
log.WithFields(xlog.Fields{
144+
"time": time.Since(start),
145+
"files": wp.count,
146+
}).Info("operation completed")
147+
return err
148+
}

go.mod

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,45 @@
11
module github.com/bryk-io/tred-cli
22

3-
go 1.16
3+
go 1.18
44

55
require (
66
github.com/cheggaaa/pb/v3 v3.1.0
77
github.com/pkg/errors v0.9.1
88
github.com/spf13/cobra v1.5.0
9-
github.com/spf13/viper v1.12.0
10-
go.bryk.io/pkg v0.0.0-20220808192228-d72c45988885
11-
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035
9+
github.com/spf13/viper v1.13.0
10+
go.bryk.io/pkg v0.0.0-20221011100809-a2bf420b8f3f
11+
golang.org/x/term v0.0.0-20220919170432-7a66f970e087
12+
)
13+
14+
require (
15+
github.com/VividCortex/ewma v1.1.1 // indirect
16+
github.com/briandowns/spinner v1.19.0 // indirect
17+
github.com/fatih/color v1.13.0 // indirect
18+
github.com/fsnotify/fsnotify v1.5.4 // indirect
19+
github.com/hashicorp/hcl v1.0.0 // indirect
20+
github.com/inconshreveable/mousetrap v1.0.0 // indirect
21+
github.com/magiconair/properties v1.8.6 // indirect
22+
github.com/mattn/go-colorable v0.1.12 // indirect
23+
github.com/mattn/go-isatty v0.0.14 // indirect
24+
github.com/mattn/go-runewidth v0.0.12 // indirect
25+
github.com/mitchellh/mapstructure v1.5.0 // indirect
26+
github.com/pelletier/go-toml v1.9.5 // indirect
27+
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
28+
github.com/rivo/uniseg v0.2.0 // indirect
29+
github.com/rs/zerolog v1.28.0 // indirect
30+
github.com/sirupsen/logrus v1.9.0 // indirect
31+
github.com/spf13/afero v1.8.2 // indirect
32+
github.com/spf13/cast v1.5.0 // indirect
33+
github.com/spf13/jwalterweatherman v1.1.0 // indirect
34+
github.com/spf13/pflag v1.0.5 // indirect
35+
github.com/subosito/gotenv v1.4.1 // indirect
36+
go.uber.org/atomic v1.7.0 // indirect
37+
go.uber.org/multierr v1.6.0 // indirect
38+
go.uber.org/zap v1.23.0 // indirect
39+
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b // indirect
40+
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
41+
golang.org/x/text v0.3.7 // indirect
42+
gopkg.in/ini.v1 v1.67.0 // indirect
43+
gopkg.in/yaml.v2 v2.4.0 // indirect
44+
gopkg.in/yaml.v3 v3.0.1 // indirect
1245
)

0 commit comments

Comments
 (0)