Skip to content

Commit 8f81347

Browse files
committed
feat: initial implementation
0 parents  commit 8f81347

File tree

8 files changed

+341
-0
lines changed

8 files changed

+341
-0
lines changed

main.go

Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
// You can edit this code!
2+
// Click here and start typing.
3+
package main
4+
5+
import (
6+
"bytes"
7+
"errors"
8+
"flag"
9+
"fmt"
10+
"os"
11+
"os/exec"
12+
"path"
13+
"path/filepath"
14+
"regexp"
15+
"strings"
16+
)
17+
18+
var ignore = struct{}{}
19+
20+
var ignoreFolders = map[string]struct{}{
21+
".git": ignore,
22+
"node_modules": ignore,
23+
"mongo-data": ignore,
24+
}
25+
26+
const (
27+
encryptCmd string = "seal"
28+
decryptCmd string = "open"
29+
)
30+
31+
var verbose bool
32+
var dryRun bool
33+
var projectRoot string
34+
var key string
35+
36+
/*
37+
secrets open <root folder>
38+
secrets open :: take root folder from pwd
39+
secrets seal
40+
*/
41+
42+
func isIgnoredFolder(path string) bool {
43+
_, ok := ignoreFolders[path]
44+
// fmt.Println(path, value, ok)
45+
return ok
46+
}
47+
48+
func findEncryptedFiles(root string) ([]string, error) {
49+
return findFiles(root, *regexp.MustCompile(`\.enc$`))
50+
}
51+
52+
func findUnencryptedFiles(root string) ([]string, error) {
53+
return findFiles(root, *regexp.MustCompile(`secret\.(yaml|yml)$`))
54+
}
55+
56+
func findFiles(root string, re regexp.Regexp) ([]string, error) {
57+
result := make([]string, 0, 1)
58+
59+
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
60+
if err != nil {
61+
return err
62+
}
63+
64+
if isIgnoredFolder(info.Name()) {
65+
return filepath.SkipDir
66+
}
67+
68+
if !info.IsDir() && re.MatchString(path) {
69+
absolutePath, _ := filepath.Abs(path)
70+
result = append(result, absolutePath)
71+
}
72+
73+
return nil
74+
})
75+
76+
if err != nil {
77+
fmt.Fprintf(os.Stderr, "%s\n", err)
78+
}
79+
80+
return result, nil
81+
}
82+
83+
func printIf(str string) {
84+
if len(str) > 0 {
85+
fmt.Println(str)
86+
}
87+
}
88+
89+
func callKms(operation string, keyName string, plaintextFile string, ciphertextFile string) error {
90+
if dryRun {
91+
return nil
92+
}
93+
cmd := exec.Command(
94+
"gcloud",
95+
"kms",
96+
operation,
97+
"--location", "global",
98+
"--keyring", "immi-project-secrets",
99+
"--key", keyName,
100+
"--plaintext-file", plaintextFile,
101+
"--ciphertext-file", ciphertextFile,
102+
)
103+
var stdOut bytes.Buffer
104+
var stdErr bytes.Buffer
105+
cmd.Stdout = &stdOut
106+
cmd.Stderr = &stdErr
107+
err := cmd.Run()
108+
if err != nil {
109+
if strings.Contains(stdErr.String(), "NOT_FOUND: ") {
110+
err := createKey(keyName)
111+
if err != nil {
112+
return err
113+
}
114+
return callKms(operation, keyName, plaintextFile, ciphertextFile)
115+
}
116+
printIf(fmt.Sprintf("out: %s", stdOut.String()))
117+
printIf(fmt.Sprintf("err: %s", stdErr.String()))
118+
fmt.Fprintf(os.Stderr, "%s\n", err)
119+
}
120+
return nil
121+
}
122+
123+
func createKey(keyName string) error {
124+
fmt.Printf("creating key for the project %s\n", keyName)
125+
if dryRun {
126+
return nil
127+
}
128+
cmd := exec.Command(
129+
"gcloud",
130+
"kms",
131+
"keys",
132+
"create", keyName,
133+
"--purpose", "encryption",
134+
"--rotation-period", "100d",
135+
"--next-rotation-time", "+p100d",
136+
"--location", "global",
137+
"--keyring", "immi-project-secrets",
138+
)
139+
var stdOut bytes.Buffer
140+
var stdErr bytes.Buffer
141+
cmd.Stdout = &stdOut
142+
cmd.Stderr = &stdErr
143+
err := cmd.Run()
144+
if err != nil {
145+
printIf(fmt.Sprintf("out: %s", stdOut.String()))
146+
printIf(fmt.Sprintf("err: %s", stdErr.String()))
147+
fmt.Fprintf(os.Stderr, "%s\n", err)
148+
return err
149+
}
150+
return nil
151+
}
152+
153+
func encrypt(keyName string, plaintextFile string) {
154+
callKms("encrypt", keyName, plaintextFile, plaintextFile+".enc")
155+
}
156+
157+
func decrypt(keyName string, ciphertextFile string) {
158+
re := regexp.MustCompile(`\.enc$`)
159+
plaintextFile := re.ReplaceAllString(ciphertextFile, "")
160+
if plaintextFile == ciphertextFile {
161+
fmt.Fprintf(os.Stderr, "Not a .enc file: %s\n", ciphertextFile)
162+
os.Exit(1)
163+
}
164+
callKms("decrypt", keyName, plaintextFile, ciphertextFile)
165+
}
166+
167+
func isProjectRoot(path string) bool {
168+
info, err := os.Stat(filepath.Join(path, ".git"))
169+
if err != nil {
170+
return false
171+
}
172+
return info.IsDir()
173+
}
174+
175+
func findProjectRoot(path string) (string, error) {
176+
path, err := filepath.Abs(path)
177+
if err != nil {
178+
return "", err
179+
}
180+
nextPath := filepath.Join(path, "..")
181+
if path == nextPath {
182+
return path, errors.New("not in project. Run the script inside a project folder(git repo) or provide it as an argument")
183+
}
184+
if isProjectRoot(path) {
185+
return path, nil
186+
}
187+
return findProjectRoot(nextPath)
188+
}
189+
190+
func remove(slice []string, s int) []string {
191+
return append(slice[:s], slice[s+1:]...)
192+
}
193+
194+
func popCommand(args []string) (string, []string, error) {
195+
for i, a := range args {
196+
if i == 0 {
197+
continue
198+
}
199+
if !strings.HasPrefix(a, "-") {
200+
return a, remove(args, i), nil
201+
}
202+
}
203+
return "", args, errors.New("command not found")
204+
}
205+
206+
func isGitTracked(projectRoot string, filePath string) (bool, error) {
207+
// fmt.Println("is", filePath, "tracked in", projectRoot)
208+
cmd := exec.Command(
209+
"git",
210+
"-C", projectRoot,
211+
"ls-files", "--error-unmatch", filePath,
212+
)
213+
var stdOut bytes.Buffer
214+
var stdErr bytes.Buffer
215+
cmd.Stdout = &stdOut
216+
cmd.Stderr = &stdErr
217+
err := cmd.Run()
218+
if err != nil {
219+
// printIf(fmt.Sprintf("out: %s", stdOut.String()))
220+
// printIf(fmt.Sprintf("err: %s", stdErr.String()))
221+
return false, err
222+
}
223+
return true, nil
224+
}
225+
226+
func isGitIgnored(projectRoot string, filePath string) (bool, error) {
227+
// fmt.Println("is", filePath, "ignored in", projectRoot)
228+
cmd := exec.Command(
229+
"git",
230+
"-C", projectRoot,
231+
"check-ignore", filePath,
232+
)
233+
var stdOut bytes.Buffer
234+
var stdErr bytes.Buffer
235+
cmd.Stdout = &stdOut
236+
cmd.Stderr = &stdErr
237+
err := cmd.Run()
238+
if err != nil {
239+
printIf(fmt.Sprintf("out: %s", stdOut.String()))
240+
printIf(fmt.Sprintf("err: %s", stdErr.String()))
241+
return false, err
242+
}
243+
return (strings.TrimSpace(stdOut.String()) == filePath), nil
244+
}
245+
246+
func appendToFile(filePath string, line string) error {
247+
f, err := os.OpenFile(filePath,
248+
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
249+
if err != nil {
250+
return err
251+
}
252+
defer f.Close()
253+
if _, err := f.WriteString(line + "\n"); err != nil {
254+
return err
255+
}
256+
return nil
257+
}
258+
259+
func addGitIgnore(projectRoot string, fileToIgnore string) error {
260+
isTracked, err := isGitTracked(projectRoot, fileToIgnore)
261+
if isTracked {
262+
// fmt.Println("NOT appending", fileToIgnore, "to gitignore because it's already tracked")
263+
return errors.New("file already tracked")
264+
}
265+
isIgnored, err := isGitIgnored(projectRoot, fileToIgnore)
266+
// fmt.Println(isIgnored, err)
267+
if isIgnored {
268+
// fmt.Println("NOT appending", fileToIgnore, "to gitignore because it's already ignored")
269+
return nil
270+
}
271+
relativePath, err := filepath.Rel(projectRoot, fileToIgnore)
272+
if err != nil {
273+
return err
274+
}
275+
return appendToFile(path.Join(projectRoot, ".gitignore"), relativePath)
276+
}
277+
278+
func main() {
279+
// fmt.Println(isGitTracked("/home/rauno/projects/go/secrets", "test/pipeline"))
280+
// fmt.Println(addGitIgnore("/home/rauno/projects/@jobbatical/analytics", "/home/rauno/projects/@jobbatical/analytics/env.run"))
281+
// os.Exit(0)
282+
flag.BoolVar(&verbose, "verbose", false, "Log debug info")
283+
flag.BoolVar(&dryRun, "dry-run", false, "Skip calls to GCP")
284+
flag.StringVar(&projectRoot, "root", "", "Project root folder(name will be used as key name)")
285+
flag.StringVar(&key, "key", "", "Key to use")
286+
var (
287+
cmd string
288+
err error
289+
)
290+
cmd, os.Args, err = popCommand(os.Args)
291+
if err != nil {
292+
fmt.Fprintf(os.Stderr, "%s\n", err)
293+
os.Exit(1)
294+
}
295+
if verbose {
296+
fmt.Println(os.Args)
297+
}
298+
299+
flag.Parse()
300+
301+
if projectRoot == "" {
302+
projectRoot, _ = findProjectRoot(".")
303+
}
304+
305+
if key == "" {
306+
key = filepath.Base(projectRoot)
307+
}
308+
309+
if verbose {
310+
fmt.Printf("dry run: %t\n", dryRun)
311+
fmt.Printf("key: %s\n", key)
312+
fmt.Printf("project root: %s\n", projectRoot)
313+
fmt.Printf("cmd: %s\n", cmd)
314+
}
315+
316+
if cmd == encryptCmd {
317+
files, _ := findUnencryptedFiles(projectRoot)
318+
for _, path := range files {
319+
encrypt(key, path)
320+
addGitIgnore(projectRoot, path)
321+
fmt.Printf("%s encrypted\n", path)
322+
}
323+
os.Exit(0)
324+
}
325+
if cmd == decryptCmd {
326+
files, _ := findEncryptedFiles(projectRoot)
327+
for _, path := range files {
328+
decrypt(key, path)
329+
fmt.Printf("%s decrypted\n", path)
330+
}
331+
os.Exit(0)
332+
}
333+
fmt.Fprintf(os.Stderr, "Unknown command: %s\nUsage: secrets [open|seal] [--dry-run] [--verbose] [--root <project root>] [--key <encryption key name>]\n", cmd)
334+
os.Exit(1)
335+
}

pipeline

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
go build
2+
cp -v secrets ~/bin/
3+
./secrets seal --dry-run --root ./test --key secrets
4+
./secrets open --dry-run --root ./test --key secrets
5+
tree ./test

test/config.secret.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
kala

test/config.secret.yaml.enc

86 Bytes
Binary file not shown.

test/pipeline

Whitespace-only changes.

test/secret1.enc

Whitespace-only changes.

test/somethingelse.enc.txt

Whitespace-only changes.

test/somtin/secret2.enc

Whitespace-only changes.

0 commit comments

Comments
 (0)