@reegaviljoen/eldlock 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +285 -0
- package/bin/eldlock +11 -0
- package/docs/architecture.md +164 -0
- package/docs/threat-model.md +47 -0
- package/eldlock-cli/README.md +56 -0
- package/eldlock-cli/bin/eldlock +3 -0
- package/eldlock-cli/package-lock.json +805 -0
- package/eldlock-cli/package.json +71 -0
- package/eldlock-cli/src/api.ts +250 -0
- package/eldlock-cli/src/cli.ts +490 -0
- package/eldlock-cli/src/main.ts +10 -0
- package/eldlock-cli/src/tui.ts +676 -0
- package/eldlock-cli/tsconfig.json +13 -0
- package/eldlock-cli/vendor/npm/ansi-regex-6.2.2.tgz +0 -0
- package/eldlock-cli/vendor/npm/bun-ffi-structs-0.2.2.tgz +0 -0
- package/eldlock-cli/vendor/npm/diff-9.0.0.tgz +0 -0
- package/eldlock-cli/vendor/npm/emoji-regex-10.6.0.tgz +0 -0
- package/eldlock-cli/vendor/npm/esbuild-0.28.0.tgz +0 -0
- package/eldlock-cli/vendor/npm/esbuild-darwin-arm64-0.28.0.tgz +0 -0
- package/eldlock-cli/vendor/npm/esbuild-darwin-x64-0.28.0.tgz +0 -0
- package/eldlock-cli/vendor/npm/esbuild-linux-arm64-0.28.0.tgz +0 -0
- package/eldlock-cli/vendor/npm/esbuild-linux-x64-0.28.0.tgz +0 -0
- package/eldlock-cli/vendor/npm/fsevents-2.3.3.tgz +0 -0
- package/eldlock-cli/vendor/npm/get-east-asian-width-1.6.0.tgz +0 -0
- package/eldlock-cli/vendor/npm/marked-17.0.1.tgz +0 -0
- package/eldlock-cli/vendor/npm/opentui-core-0.3.1.tgz +0 -0
- package/eldlock-cli/vendor/npm/opentui-core-darwin-arm64-0.3.1.tgz +0 -0
- package/eldlock-cli/vendor/npm/opentui-core-darwin-x64-0.3.1.tgz +0 -0
- package/eldlock-cli/vendor/npm/opentui-core-linux-arm64-0.3.1.tgz +0 -0
- package/eldlock-cli/vendor/npm/opentui-core-linux-x64-0.3.1.tgz +0 -0
- package/eldlock-cli/vendor/npm/string-width-7.2.0.tgz +0 -0
- package/eldlock-cli/vendor/npm/strip-ansi-7.1.2.tgz +0 -0
- package/eldlock-cli/vendor/npm/tsx-4.22.4.tgz +0 -0
- package/eldlock-cli/vendor/npm/types-node-22.19.19.tgz +0 -0
- package/eldlock-cli/vendor/npm/typescript-5.9.3.tgz +0 -0
- package/eldlock-cli/vendor/npm/undici-types-6.21.0.tgz +0 -0
- package/eldlock-cli/vendor/npm/web-tree-sitter-0.25.10.tgz +0 -0
- package/eldlock-cli/vendor/npm/yoga-layout-3.2.1.tgz +0 -0
- package/eldlock-server/cmd/eldlock-server/main.go +132 -0
- package/eldlock-server/go.mod +10 -0
- package/eldlock-server/go.sum +11 -0
- package/eldlock-server/internal/api/README.md +14 -0
- package/eldlock-server/internal/api/core.go +126 -0
- package/eldlock-server/internal/api/exec.go +97 -0
- package/eldlock-server/internal/api/secrets.go +358 -0
- package/eldlock-server/internal/api/server.go +72 -0
- package/eldlock-server/internal/api/service_test.go +416 -0
- package/eldlock-server/internal/api/types.go +48 -0
- package/eldlock-server/internal/api/vault.go +69 -0
- package/eldlock-server/internal/api/vendor.go +44 -0
- package/eldlock-server/internal/libfido2/LICENSE +21 -0
- package/eldlock-server/internal/libfido2/README.md +127 -0
- package/eldlock-server/internal/libfido2/examples_test.go +614 -0
- package/eldlock-server/internal/libfido2/fido2.go +1234 -0
- package/eldlock-server/internal/libfido2/fido2_darwin.go +7 -0
- package/eldlock-server/internal/libfido2/fido2_other.go +9 -0
- package/eldlock-server/internal/libfido2/fido2_test.go +101 -0
- package/eldlock-server/internal/libfido2/go.mod +10 -0
- package/eldlock-server/internal/libfido2/go.sum +16 -0
- package/eldlock-server/internal/libfido2/log.go +87 -0
- package/eldlock-server/internal/store/README.md +7 -0
- package/eldlock-server/internal/store/store.go +434 -0
- package/eldlock-server/internal/store/store_test.go +125 -0
- package/eldlock-server/internal/yubikey/README.md +25 -0
- package/eldlock-server/internal/yubikey/default_fido2.go +7 -0
- package/eldlock-server/internal/yubikey/default_stub.go +7 -0
- package/eldlock-server/internal/yubikey/fido2_disabled.go +9 -0
- package/eldlock-server/internal/yubikey/fido2_libfido2.go +225 -0
- package/eldlock-server/internal/yubikey/fido2_libfido2_test.go +66 -0
- package/eldlock-server/internal/yubikey/passkey.go +139 -0
- package/eldlock-server/internal/yubikey/passkey_test.go +36 -0
- package/eldlock-server/vendor/github.com/keys-pub/go-libfido2/LICENSE +21 -0
- package/eldlock-server/vendor/github.com/keys-pub/go-libfido2/README.md +127 -0
- package/eldlock-server/vendor/github.com/keys-pub/go-libfido2/fido2.go +1234 -0
- package/eldlock-server/vendor/github.com/keys-pub/go-libfido2/fido2_darwin.go +7 -0
- package/eldlock-server/vendor/github.com/keys-pub/go-libfido2/fido2_other.go +9 -0
- package/eldlock-server/vendor/github.com/keys-pub/go-libfido2/log.go +87 -0
- package/eldlock-server/vendor/github.com/pkg/errors/.travis.yml +10 -0
- package/eldlock-server/vendor/github.com/pkg/errors/LICENSE +23 -0
- package/eldlock-server/vendor/github.com/pkg/errors/Makefile +44 -0
- package/eldlock-server/vendor/github.com/pkg/errors/README.md +59 -0
- package/eldlock-server/vendor/github.com/pkg/errors/appveyor.yml +32 -0
- package/eldlock-server/vendor/github.com/pkg/errors/errors.go +288 -0
- package/eldlock-server/vendor/github.com/pkg/errors/go113.go +38 -0
- package/eldlock-server/vendor/github.com/pkg/errors/stack.go +177 -0
- package/eldlock-server/vendor/modules.txt +7 -0
- package/examples/eldlock.toml +17 -0
- package/install.sh +66 -0
- package/package.json +66 -0
- package/scripts/build-production.mjs +177 -0
- package/scripts/postinstall-production.mjs +23 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
package api
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"os"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"testing"
|
|
8
|
+
|
|
9
|
+
"github.com/eldlock/eldlock-server/internal/store"
|
|
10
|
+
"github.com/eldlock/eldlock-server/internal/yubikey"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
func TestServiceAddAndReadPlain(t *testing.T) {
|
|
14
|
+
service := NewService(newMemoryStore())
|
|
15
|
+
|
|
16
|
+
init := service.Handle(context.Background(), Request{Action: ActionInit})
|
|
17
|
+
if !init.OK {
|
|
18
|
+
t.Fatalf("init OK = false, error = %q", init.Error)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
add := service.Handle(context.Background(), Request{
|
|
22
|
+
Action: ActionAddSecret,
|
|
23
|
+
Name: "project/API_KEY",
|
|
24
|
+
Type: store.SecretTypeEnv,
|
|
25
|
+
Value: "secret-value",
|
|
26
|
+
})
|
|
27
|
+
if !add.OK {
|
|
28
|
+
t.Fatalf("add OK = false, error = %q", add.Error)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
read := service.Handle(context.Background(), Request{
|
|
32
|
+
Action: ActionReadSecret,
|
|
33
|
+
Name: "project/API_KEY",
|
|
34
|
+
Output: OutputPlain,
|
|
35
|
+
})
|
|
36
|
+
if !read.OK {
|
|
37
|
+
t.Fatalf("read OK = false, error = %q", read.Error)
|
|
38
|
+
}
|
|
39
|
+
if read.Value != "secret-value" {
|
|
40
|
+
t.Fatalf("read Value = %q, want secret value", read.Value)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func TestServiceReadClipboardStaysServerSide(t *testing.T) {
|
|
45
|
+
clipboard := &memoryClipboard{}
|
|
46
|
+
service := NewService(newMemoryStore())
|
|
47
|
+
service.Clipboard = clipboard
|
|
48
|
+
|
|
49
|
+
service.Handle(context.Background(), Request{Action: ActionInit})
|
|
50
|
+
service.Handle(context.Background(), Request{
|
|
51
|
+
Action: ActionAddSecret,
|
|
52
|
+
Name: "ssh/personal",
|
|
53
|
+
Type: store.SecretTypeSSH,
|
|
54
|
+
Value: "private-key",
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
read := service.Handle(context.Background(), Request{
|
|
58
|
+
Action: ActionReadSecret,
|
|
59
|
+
Name: "ssh/personal",
|
|
60
|
+
Output: OutputClipboard,
|
|
61
|
+
})
|
|
62
|
+
if !read.OK {
|
|
63
|
+
t.Fatalf("read OK = false, error = %q", read.Error)
|
|
64
|
+
}
|
|
65
|
+
if read.Value != "" {
|
|
66
|
+
t.Fatalf("clipboard response leaked value %q", read.Value)
|
|
67
|
+
}
|
|
68
|
+
if clipboard.value != "private-key" {
|
|
69
|
+
t.Fatalf("clipboard value = %q, want private key", clipboard.value)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
func TestServiceRequiresInit(t *testing.T) {
|
|
74
|
+
service := NewService(newMemoryStore())
|
|
75
|
+
|
|
76
|
+
add := service.Handle(context.Background(), Request{
|
|
77
|
+
Action: ActionAddSecret,
|
|
78
|
+
Name: "project/API_KEY",
|
|
79
|
+
Type: store.SecretTypeEnv,
|
|
80
|
+
Value: "secret-value",
|
|
81
|
+
})
|
|
82
|
+
if add.OK {
|
|
83
|
+
t.Fatalf("add OK = true before init")
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
func TestServiceReturnsPINRequiredCode(t *testing.T) {
|
|
88
|
+
memory := newMemoryStore()
|
|
89
|
+
memory.initErr = yubikey.ErrPINRequired
|
|
90
|
+
service := NewService(memory)
|
|
91
|
+
|
|
92
|
+
response := service.Handle(context.Background(), Request{Action: ActionInit})
|
|
93
|
+
if response.OK {
|
|
94
|
+
t.Fatalf("init OK = true")
|
|
95
|
+
}
|
|
96
|
+
if response.Code != "pin_required" {
|
|
97
|
+
t.Fatalf("init Code = %q, want pin_required; error = %q", response.Code, response.Error)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
func TestServiceRemoveSecret(t *testing.T) {
|
|
102
|
+
service := NewService(newMemoryStore())
|
|
103
|
+
|
|
104
|
+
service.Handle(context.Background(), Request{Action: ActionInit})
|
|
105
|
+
service.Handle(context.Background(), Request{
|
|
106
|
+
Action: ActionAddSecret,
|
|
107
|
+
Name: "project/API_KEY",
|
|
108
|
+
Type: store.SecretTypeEnv,
|
|
109
|
+
Value: "secret-value",
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
remove := service.Handle(context.Background(), Request{
|
|
113
|
+
Action: ActionRemoveSecret,
|
|
114
|
+
Name: "project/API_KEY",
|
|
115
|
+
})
|
|
116
|
+
if !remove.OK {
|
|
117
|
+
t.Fatalf("remove OK = false, error = %q", remove.Error)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
read := service.Handle(context.Background(), Request{
|
|
121
|
+
Action: ActionReadSecret,
|
|
122
|
+
Name: "project/API_KEY",
|
|
123
|
+
Output: OutputPlain,
|
|
124
|
+
})
|
|
125
|
+
if read.OK {
|
|
126
|
+
t.Fatalf("read OK = true after remove")
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
func TestServiceExecInjectsEnvSecrets(t *testing.T) {
|
|
131
|
+
service := NewService(newMemoryStore())
|
|
132
|
+
|
|
133
|
+
service.Handle(context.Background(), Request{Action: ActionInit})
|
|
134
|
+
service.Handle(context.Background(), Request{
|
|
135
|
+
Action: ActionAddSecret,
|
|
136
|
+
Name: "BG_TEST",
|
|
137
|
+
Type: store.SecretTypeEnv,
|
|
138
|
+
Value: "from-eldlock",
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
exec := service.Handle(context.Background(), Request{
|
|
142
|
+
Action: ActionExec,
|
|
143
|
+
Command: []string{"/bin/sh", "-c", "printf %s \"$BG_TEST\""},
|
|
144
|
+
Env: map[string]string{"BG_TEST": "from-client"},
|
|
145
|
+
})
|
|
146
|
+
if !exec.OK {
|
|
147
|
+
t.Fatalf("exec OK = false, error = %q", exec.Error)
|
|
148
|
+
}
|
|
149
|
+
if exec.Stdout != "from-eldlock" {
|
|
150
|
+
t.Fatalf("exec Stdout = %q, want injected secret", exec.Stdout)
|
|
151
|
+
}
|
|
152
|
+
if exec.ExitCode != 0 {
|
|
153
|
+
t.Fatalf("exec ExitCode = %d, want 0", exec.ExitCode)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
func TestServiceExecShellCommandExpandsInjectedEnvSecrets(t *testing.T) {
|
|
158
|
+
service := NewService(newMemoryStore())
|
|
159
|
+
|
|
160
|
+
service.Handle(context.Background(), Request{Action: ActionInit})
|
|
161
|
+
service.Handle(context.Background(), Request{
|
|
162
|
+
Action: ActionAddSecret,
|
|
163
|
+
Name: "BG_TEST",
|
|
164
|
+
Type: store.SecretTypeEnv,
|
|
165
|
+
Value: "from-eldlock",
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
exec := service.Handle(context.Background(), Request{
|
|
169
|
+
Action: ActionExec,
|
|
170
|
+
Shell: "/bin/sh",
|
|
171
|
+
ShellCommand: "printf %s \"$BG_TEST\"",
|
|
172
|
+
Env: map[string]string{"BG_TEST": "from-client"},
|
|
173
|
+
})
|
|
174
|
+
if !exec.OK {
|
|
175
|
+
t.Fatalf("exec OK = false, error = %q", exec.Error)
|
|
176
|
+
}
|
|
177
|
+
if exec.Stdout != "from-eldlock" {
|
|
178
|
+
t.Fatalf("exec Stdout = %q, want injected secret", exec.Stdout)
|
|
179
|
+
}
|
|
180
|
+
if exec.ExitCode != 0 {
|
|
181
|
+
t.Fatalf("exec ExitCode = %d, want 0", exec.ExitCode)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
func TestServiceInteractiveExecReturnsEnvSecretsForClientShell(t *testing.T) {
|
|
186
|
+
service := NewService(newMemoryStore())
|
|
187
|
+
|
|
188
|
+
service.Handle(context.Background(), Request{Action: ActionInit})
|
|
189
|
+
service.Handle(context.Background(), Request{
|
|
190
|
+
Action: ActionAddSecret,
|
|
191
|
+
Name: "BG_TEST",
|
|
192
|
+
Type: store.SecretTypeEnv,
|
|
193
|
+
Value: "from-eldlock",
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
exec := service.Handle(context.Background(), Request{
|
|
197
|
+
Action: ActionExec,
|
|
198
|
+
Interactive: true,
|
|
199
|
+
})
|
|
200
|
+
if !exec.OK {
|
|
201
|
+
t.Fatalf("exec OK = false, error = %q", exec.Error)
|
|
202
|
+
}
|
|
203
|
+
if exec.Env["BG_TEST"] != "from-eldlock" {
|
|
204
|
+
t.Fatalf("exec Env[BG_TEST] = %q, want injected secret", exec.Env["BG_TEST"])
|
|
205
|
+
}
|
|
206
|
+
if exec.Stdout != "" || exec.Stderr != "" {
|
|
207
|
+
t.Fatalf("interactive exec should not return captured output")
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
func TestServiceExecReturnsChildExitCode(t *testing.T) {
|
|
212
|
+
service := NewService(newMemoryStore())
|
|
213
|
+
|
|
214
|
+
service.Handle(context.Background(), Request{Action: ActionInit})
|
|
215
|
+
exec := service.Handle(context.Background(), Request{
|
|
216
|
+
Action: ActionExec,
|
|
217
|
+
Command: []string{"/bin/sh", "-c", "printf problem >&2; exit 7"},
|
|
218
|
+
})
|
|
219
|
+
if !exec.OK {
|
|
220
|
+
t.Fatalf("exec OK = false, error = %q", exec.Error)
|
|
221
|
+
}
|
|
222
|
+
if exec.Stderr != "problem" {
|
|
223
|
+
t.Fatalf("exec Stderr = %q, want problem", exec.Stderr)
|
|
224
|
+
}
|
|
225
|
+
if exec.ExitCode != 7 {
|
|
226
|
+
t.Fatalf("exec ExitCode = %d, want 7", exec.ExitCode)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
func TestServiceImportEnvFile(t *testing.T) {
|
|
231
|
+
service := NewService(newMemoryStore())
|
|
232
|
+
service.Handle(context.Background(), Request{Action: ActionInit})
|
|
233
|
+
|
|
234
|
+
dir := t.TempDir()
|
|
235
|
+
envPath := filepath.Join(dir, ".env")
|
|
236
|
+
if err := os.WriteFile(envPath, []byte(`
|
|
237
|
+
# ignored
|
|
238
|
+
API_KEY=secret-value
|
|
239
|
+
export QUOTED="hello\nworld"
|
|
240
|
+
SINGLE='literal value'
|
|
241
|
+
TRAILING=visible # hidden comment
|
|
242
|
+
EMPTY=
|
|
243
|
+
`), 0o600); err != nil {
|
|
244
|
+
t.Fatal(err)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
response := service.Handle(context.Background(), Request{
|
|
248
|
+
Action: ActionImportEnv,
|
|
249
|
+
SourcePath: ".env",
|
|
250
|
+
CWD: dir,
|
|
251
|
+
})
|
|
252
|
+
if !response.OK {
|
|
253
|
+
t.Fatalf("import OK = false, error = %q", response.Error)
|
|
254
|
+
}
|
|
255
|
+
if response.Imported != 5 {
|
|
256
|
+
t.Fatalf("import Imported = %d, want 5", response.Imported)
|
|
257
|
+
}
|
|
258
|
+
if response.Value != "" {
|
|
259
|
+
t.Fatalf("import leaked value %q", response.Value)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
read := service.Handle(context.Background(), Request{Action: ActionReadSecret, Name: "QUOTED", Output: OutputPlain})
|
|
263
|
+
if read.Value != "hello\nworld" {
|
|
264
|
+
t.Fatalf("quoted value = %q", read.Value)
|
|
265
|
+
}
|
|
266
|
+
trailing := service.Handle(context.Background(), Request{Action: ActionReadSecret, Name: "TRAILING", Output: OutputPlain})
|
|
267
|
+
if trailing.Value != "visible" {
|
|
268
|
+
t.Fatalf("trailing value = %q", trailing.Value)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
func TestServiceImportEnvFileRejectsInvalidLine(t *testing.T) {
|
|
273
|
+
service := NewService(newMemoryStore())
|
|
274
|
+
service.Handle(context.Background(), Request{Action: ActionInit})
|
|
275
|
+
|
|
276
|
+
dir := t.TempDir()
|
|
277
|
+
if err := os.WriteFile(filepath.Join(dir, ".env"), []byte("not-valid\n"), 0o600); err != nil {
|
|
278
|
+
t.Fatal(err)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
response := service.Handle(context.Background(), Request{
|
|
282
|
+
Action: ActionImportEnv,
|
|
283
|
+
SourcePath: ".env",
|
|
284
|
+
CWD: dir,
|
|
285
|
+
})
|
|
286
|
+
if response.OK {
|
|
287
|
+
t.Fatalf("import OK = true")
|
|
288
|
+
}
|
|
289
|
+
if response.Error == "" {
|
|
290
|
+
t.Fatalf("import error was empty")
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
func TestServiceRoutesRequestsByVault(t *testing.T) {
|
|
295
|
+
resolver := newMemoryStoreResolver()
|
|
296
|
+
service := NewService(nil)
|
|
297
|
+
service.StoreResolver = resolver
|
|
298
|
+
|
|
299
|
+
initDefault := service.Handle(context.Background(), Request{Action: ActionInit})
|
|
300
|
+
if !initDefault.OK {
|
|
301
|
+
t.Fatalf("default init OK = false, error = %q", initDefault.Error)
|
|
302
|
+
}
|
|
303
|
+
initWork := service.Handle(context.Background(), Request{Action: ActionInit, Vault: "work"})
|
|
304
|
+
if !initWork.OK {
|
|
305
|
+
t.Fatalf("work init OK = false, error = %q", initWork.Error)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
service.Handle(context.Background(), Request{
|
|
309
|
+
Action: ActionAddSecret,
|
|
310
|
+
Name: "BG_TEST",
|
|
311
|
+
Type: store.SecretTypeEnv,
|
|
312
|
+
Value: "default-value",
|
|
313
|
+
})
|
|
314
|
+
service.Handle(context.Background(), Request{
|
|
315
|
+
Action: ActionAddSecret,
|
|
316
|
+
Vault: "work",
|
|
317
|
+
Name: "BG_TEST",
|
|
318
|
+
Type: store.SecretTypeEnv,
|
|
319
|
+
Value: "work-value",
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
readDefault := service.Handle(context.Background(), Request{Action: ActionReadSecret, Name: "BG_TEST", Output: OutputPlain})
|
|
323
|
+
if readDefault.Value != "default-value" {
|
|
324
|
+
t.Fatalf("default vault value = %q, want default-value", readDefault.Value)
|
|
325
|
+
}
|
|
326
|
+
readWork := service.Handle(context.Background(), Request{Action: ActionReadSecret, Vault: "work", Name: "BG_TEST", Output: OutputPlain})
|
|
327
|
+
if readWork.Value != "work-value" {
|
|
328
|
+
t.Fatalf("work vault value = %q, want work-value", readWork.Value)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
func TestFileStoreResolverRejectsInvalidVaultName(t *testing.T) {
|
|
333
|
+
_, err := NormalizeVaultName("../nope")
|
|
334
|
+
if err == nil {
|
|
335
|
+
t.Fatalf("NormalizeVaultName() error = nil")
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
type memoryStore struct {
|
|
340
|
+
initialized bool
|
|
341
|
+
secrets map[string]store.Secret
|
|
342
|
+
initErr error
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
type memoryStoreResolver struct {
|
|
346
|
+
stores map[string]*memoryStore
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
func newMemoryStoreResolver() *memoryStoreResolver {
|
|
350
|
+
return &memoryStoreResolver{stores: map[string]*memoryStore{}}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
func (r *memoryStoreResolver) StoreForVault(name string) (SecretStore, error) {
|
|
354
|
+
name, err := NormalizeVaultName(name)
|
|
355
|
+
if err != nil {
|
|
356
|
+
return nil, err
|
|
357
|
+
}
|
|
358
|
+
if r.stores[name] == nil {
|
|
359
|
+
r.stores[name] = newMemoryStore()
|
|
360
|
+
}
|
|
361
|
+
return r.stores[name], nil
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
func newMemoryStore() *memoryStore {
|
|
365
|
+
return &memoryStore{secrets: map[string]store.Secret{}}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
func (m *memoryStore) Init() error {
|
|
369
|
+
if m.initErr != nil {
|
|
370
|
+
return m.initErr
|
|
371
|
+
}
|
|
372
|
+
m.initialized = true
|
|
373
|
+
return nil
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
func (m *memoryStore) IsInitialized() (bool, error) {
|
|
377
|
+
return m.initialized, nil
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
func (m *memoryStore) Put(secret store.Secret) error {
|
|
381
|
+
m.secrets[secret.Name] = secret
|
|
382
|
+
return nil
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
func (m *memoryStore) Get(name string) (store.Secret, error) {
|
|
386
|
+
secret, ok := m.secrets[name]
|
|
387
|
+
if !ok {
|
|
388
|
+
return store.Secret{}, store.ErrNotFound
|
|
389
|
+
}
|
|
390
|
+
return secret, nil
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
func (m *memoryStore) Delete(name string) error {
|
|
394
|
+
if _, ok := m.secrets[name]; !ok {
|
|
395
|
+
return store.ErrNotFound
|
|
396
|
+
}
|
|
397
|
+
delete(m.secrets, name)
|
|
398
|
+
return nil
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
func (m *memoryStore) List() ([]store.SecretSummary, error) {
|
|
402
|
+
summaries := make([]store.SecretSummary, 0, len(m.secrets))
|
|
403
|
+
for _, secret := range m.secrets {
|
|
404
|
+
summaries = append(summaries, store.SecretSummary{Name: secret.Name, Type: secret.Type})
|
|
405
|
+
}
|
|
406
|
+
return summaries, nil
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
type memoryClipboard struct {
|
|
410
|
+
value string
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
func (m *memoryClipboard) Write(_ context.Context, value string) error {
|
|
414
|
+
m.value = value
|
|
415
|
+
return nil
|
|
416
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
package api
|
|
2
|
+
|
|
3
|
+
import "github.com/eldlock/eldlock-server/internal/store"
|
|
4
|
+
|
|
5
|
+
type Request struct {
|
|
6
|
+
Action string `json:"action"`
|
|
7
|
+
Vault string `json:"vault,omitempty"`
|
|
8
|
+
Name string `json:"name,omitempty"`
|
|
9
|
+
Type store.SecretType `json:"type,omitempty"`
|
|
10
|
+
Value string `json:"value,omitempty"`
|
|
11
|
+
SourcePath string `json:"source_path,omitempty"`
|
|
12
|
+
Output string `json:"output,omitempty"`
|
|
13
|
+
PIN string `json:"pin,omitempty"`
|
|
14
|
+
Command []string `json:"command,omitempty"`
|
|
15
|
+
Shell string `json:"shell,omitempty"`
|
|
16
|
+
ShellCommand string `json:"shell_command,omitempty"`
|
|
17
|
+
Interactive bool `json:"interactive,omitempty"`
|
|
18
|
+
CWD string `json:"cwd,omitempty"`
|
|
19
|
+
Env map[string]string `json:"env,omitempty"`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type Response struct {
|
|
23
|
+
OK bool `json:"ok"`
|
|
24
|
+
Message string `json:"message,omitempty"`
|
|
25
|
+
Value string `json:"value,omitempty"`
|
|
26
|
+
Stdout string `json:"stdout,omitempty"`
|
|
27
|
+
Stderr string `json:"stderr,omitempty"`
|
|
28
|
+
ExitCode int `json:"exit_code,omitempty"`
|
|
29
|
+
Imported int `json:"imported,omitempty"`
|
|
30
|
+
Env map[string]string `json:"env,omitempty"`
|
|
31
|
+
Secrets []store.SecretSummary `json:"secrets,omitempty"`
|
|
32
|
+
Error string `json:"error,omitempty"`
|
|
33
|
+
Code string `json:"code,omitempty"`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const (
|
|
37
|
+
ActionHealth = "health"
|
|
38
|
+
ActionInit = "init"
|
|
39
|
+
ActionAddSecret = "secret.add"
|
|
40
|
+
ActionReadSecret = "secret.read"
|
|
41
|
+
ActionRemoveSecret = "secret.remove"
|
|
42
|
+
ActionListSecrets = "secret.list"
|
|
43
|
+
ActionImportEnv = "secret.import_env"
|
|
44
|
+
ActionExec = "exec"
|
|
45
|
+
|
|
46
|
+
OutputPlain = "plain"
|
|
47
|
+
OutputClipboard = "clipboard"
|
|
48
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
package api
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"strings"
|
|
7
|
+
"unicode"
|
|
8
|
+
|
|
9
|
+
"github.com/eldlock/eldlock-server/internal/store"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
const DefaultVaultName = "default"
|
|
13
|
+
|
|
14
|
+
type FileStoreResolver struct {
|
|
15
|
+
StateDir string
|
|
16
|
+
KeyProvider store.KeyProvider
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func (r FileStoreResolver) StoreForVault(name string) (SecretStore, error) {
|
|
20
|
+
vaultName, err := NormalizeVaultName(name)
|
|
21
|
+
if err != nil {
|
|
22
|
+
return nil, err
|
|
23
|
+
}
|
|
24
|
+
if r.StateDir == "" {
|
|
25
|
+
return nil, fmt.Errorf("state dir is required")
|
|
26
|
+
}
|
|
27
|
+
if r.KeyProvider == nil {
|
|
28
|
+
return nil, fmt.Errorf("passkey provider is not configured")
|
|
29
|
+
}
|
|
30
|
+
return store.NewFileStore(r.PathForVault(vaultName), r.KeyProvider), nil
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func (r FileStoreResolver) PathForVault(name string) string {
|
|
34
|
+
if name == DefaultVaultName {
|
|
35
|
+
return filepath.Join(r.StateDir, "vault.json")
|
|
36
|
+
}
|
|
37
|
+
return filepath.Join(r.StateDir, "vaults", name+".json")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
func NormalizeVaultName(name string) (string, error) {
|
|
41
|
+
name = strings.TrimSpace(name)
|
|
42
|
+
if name == "" {
|
|
43
|
+
return DefaultVaultName, nil
|
|
44
|
+
}
|
|
45
|
+
for _, char := range name {
|
|
46
|
+
if unicode.IsLetter(char) || unicode.IsDigit(char) || char == '-' || char == '_' || char == '.' {
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
return "", fmt.Errorf("vault name %q is invalid; use letters, numbers, '.', '-', or '_'", name)
|
|
50
|
+
}
|
|
51
|
+
if name == "." || name == ".." {
|
|
52
|
+
return "", fmt.Errorf("vault name %q is invalid", name)
|
|
53
|
+
}
|
|
54
|
+
return name, nil
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func (s *Service) initVault(req Request) Response {
|
|
58
|
+
if err := s.initWithPIN(req.PIN); err != nil {
|
|
59
|
+
return pinAwareFailure(fmt.Sprintf("initialize vault: %v", err), err)
|
|
60
|
+
}
|
|
61
|
+
return Response{OK: true, Message: "initialized Eldlock vault"}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func (s *Service) initWithPIN(pin string) error {
|
|
65
|
+
if store, ok := s.Store.(PINSecretStore); ok {
|
|
66
|
+
return store.InitWithPIN(pin)
|
|
67
|
+
}
|
|
68
|
+
return s.Store.Init()
|
|
69
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
package api
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"errors"
|
|
6
|
+
"os/exec"
|
|
7
|
+
"runtime"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
type LocalClipboard struct{}
|
|
11
|
+
|
|
12
|
+
func (LocalClipboard) Write(ctx context.Context, value string) error {
|
|
13
|
+
var cmd *exec.Cmd
|
|
14
|
+
switch runtime.GOOS {
|
|
15
|
+
case "darwin":
|
|
16
|
+
cmd = exec.CommandContext(ctx, "pbcopy")
|
|
17
|
+
default:
|
|
18
|
+
if path, err := exec.LookPath("wl-copy"); err == nil {
|
|
19
|
+
cmd = exec.CommandContext(ctx, path)
|
|
20
|
+
} else if path, err := exec.LookPath("xclip"); err == nil {
|
|
21
|
+
cmd = exec.CommandContext(ctx, path, "-selection", "clipboard")
|
|
22
|
+
} else {
|
|
23
|
+
return errors.New("no supported clipboard command found")
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
stdin, err := cmd.StdinPipe()
|
|
28
|
+
if err != nil {
|
|
29
|
+
return err
|
|
30
|
+
}
|
|
31
|
+
if err := cmd.Start(); err != nil {
|
|
32
|
+
return err
|
|
33
|
+
}
|
|
34
|
+
if _, err := stdin.Write([]byte(value)); err != nil {
|
|
35
|
+
_ = stdin.Close()
|
|
36
|
+
_ = cmd.Wait()
|
|
37
|
+
return err
|
|
38
|
+
}
|
|
39
|
+
if err := stdin.Close(); err != nil {
|
|
40
|
+
_ = cmd.Wait()
|
|
41
|
+
return err
|
|
42
|
+
}
|
|
43
|
+
return cmd.Wait()
|
|
44
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 Gabriel Handford
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|