@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.
Files changed (91) hide show
  1. package/README.md +285 -0
  2. package/bin/eldlock +11 -0
  3. package/docs/architecture.md +164 -0
  4. package/docs/threat-model.md +47 -0
  5. package/eldlock-cli/README.md +56 -0
  6. package/eldlock-cli/bin/eldlock +3 -0
  7. package/eldlock-cli/package-lock.json +805 -0
  8. package/eldlock-cli/package.json +71 -0
  9. package/eldlock-cli/src/api.ts +250 -0
  10. package/eldlock-cli/src/cli.ts +490 -0
  11. package/eldlock-cli/src/main.ts +10 -0
  12. package/eldlock-cli/src/tui.ts +676 -0
  13. package/eldlock-cli/tsconfig.json +13 -0
  14. package/eldlock-cli/vendor/npm/ansi-regex-6.2.2.tgz +0 -0
  15. package/eldlock-cli/vendor/npm/bun-ffi-structs-0.2.2.tgz +0 -0
  16. package/eldlock-cli/vendor/npm/diff-9.0.0.tgz +0 -0
  17. package/eldlock-cli/vendor/npm/emoji-regex-10.6.0.tgz +0 -0
  18. package/eldlock-cli/vendor/npm/esbuild-0.28.0.tgz +0 -0
  19. package/eldlock-cli/vendor/npm/esbuild-darwin-arm64-0.28.0.tgz +0 -0
  20. package/eldlock-cli/vendor/npm/esbuild-darwin-x64-0.28.0.tgz +0 -0
  21. package/eldlock-cli/vendor/npm/esbuild-linux-arm64-0.28.0.tgz +0 -0
  22. package/eldlock-cli/vendor/npm/esbuild-linux-x64-0.28.0.tgz +0 -0
  23. package/eldlock-cli/vendor/npm/fsevents-2.3.3.tgz +0 -0
  24. package/eldlock-cli/vendor/npm/get-east-asian-width-1.6.0.tgz +0 -0
  25. package/eldlock-cli/vendor/npm/marked-17.0.1.tgz +0 -0
  26. package/eldlock-cli/vendor/npm/opentui-core-0.3.1.tgz +0 -0
  27. package/eldlock-cli/vendor/npm/opentui-core-darwin-arm64-0.3.1.tgz +0 -0
  28. package/eldlock-cli/vendor/npm/opentui-core-darwin-x64-0.3.1.tgz +0 -0
  29. package/eldlock-cli/vendor/npm/opentui-core-linux-arm64-0.3.1.tgz +0 -0
  30. package/eldlock-cli/vendor/npm/opentui-core-linux-x64-0.3.1.tgz +0 -0
  31. package/eldlock-cli/vendor/npm/string-width-7.2.0.tgz +0 -0
  32. package/eldlock-cli/vendor/npm/strip-ansi-7.1.2.tgz +0 -0
  33. package/eldlock-cli/vendor/npm/tsx-4.22.4.tgz +0 -0
  34. package/eldlock-cli/vendor/npm/types-node-22.19.19.tgz +0 -0
  35. package/eldlock-cli/vendor/npm/typescript-5.9.3.tgz +0 -0
  36. package/eldlock-cli/vendor/npm/undici-types-6.21.0.tgz +0 -0
  37. package/eldlock-cli/vendor/npm/web-tree-sitter-0.25.10.tgz +0 -0
  38. package/eldlock-cli/vendor/npm/yoga-layout-3.2.1.tgz +0 -0
  39. package/eldlock-server/cmd/eldlock-server/main.go +132 -0
  40. package/eldlock-server/go.mod +10 -0
  41. package/eldlock-server/go.sum +11 -0
  42. package/eldlock-server/internal/api/README.md +14 -0
  43. package/eldlock-server/internal/api/core.go +126 -0
  44. package/eldlock-server/internal/api/exec.go +97 -0
  45. package/eldlock-server/internal/api/secrets.go +358 -0
  46. package/eldlock-server/internal/api/server.go +72 -0
  47. package/eldlock-server/internal/api/service_test.go +416 -0
  48. package/eldlock-server/internal/api/types.go +48 -0
  49. package/eldlock-server/internal/api/vault.go +69 -0
  50. package/eldlock-server/internal/api/vendor.go +44 -0
  51. package/eldlock-server/internal/libfido2/LICENSE +21 -0
  52. package/eldlock-server/internal/libfido2/README.md +127 -0
  53. package/eldlock-server/internal/libfido2/examples_test.go +614 -0
  54. package/eldlock-server/internal/libfido2/fido2.go +1234 -0
  55. package/eldlock-server/internal/libfido2/fido2_darwin.go +7 -0
  56. package/eldlock-server/internal/libfido2/fido2_other.go +9 -0
  57. package/eldlock-server/internal/libfido2/fido2_test.go +101 -0
  58. package/eldlock-server/internal/libfido2/go.mod +10 -0
  59. package/eldlock-server/internal/libfido2/go.sum +16 -0
  60. package/eldlock-server/internal/libfido2/log.go +87 -0
  61. package/eldlock-server/internal/store/README.md +7 -0
  62. package/eldlock-server/internal/store/store.go +434 -0
  63. package/eldlock-server/internal/store/store_test.go +125 -0
  64. package/eldlock-server/internal/yubikey/README.md +25 -0
  65. package/eldlock-server/internal/yubikey/default_fido2.go +7 -0
  66. package/eldlock-server/internal/yubikey/default_stub.go +7 -0
  67. package/eldlock-server/internal/yubikey/fido2_disabled.go +9 -0
  68. package/eldlock-server/internal/yubikey/fido2_libfido2.go +225 -0
  69. package/eldlock-server/internal/yubikey/fido2_libfido2_test.go +66 -0
  70. package/eldlock-server/internal/yubikey/passkey.go +139 -0
  71. package/eldlock-server/internal/yubikey/passkey_test.go +36 -0
  72. package/eldlock-server/vendor/github.com/keys-pub/go-libfido2/LICENSE +21 -0
  73. package/eldlock-server/vendor/github.com/keys-pub/go-libfido2/README.md +127 -0
  74. package/eldlock-server/vendor/github.com/keys-pub/go-libfido2/fido2.go +1234 -0
  75. package/eldlock-server/vendor/github.com/keys-pub/go-libfido2/fido2_darwin.go +7 -0
  76. package/eldlock-server/vendor/github.com/keys-pub/go-libfido2/fido2_other.go +9 -0
  77. package/eldlock-server/vendor/github.com/keys-pub/go-libfido2/log.go +87 -0
  78. package/eldlock-server/vendor/github.com/pkg/errors/.travis.yml +10 -0
  79. package/eldlock-server/vendor/github.com/pkg/errors/LICENSE +23 -0
  80. package/eldlock-server/vendor/github.com/pkg/errors/Makefile +44 -0
  81. package/eldlock-server/vendor/github.com/pkg/errors/README.md +59 -0
  82. package/eldlock-server/vendor/github.com/pkg/errors/appveyor.yml +32 -0
  83. package/eldlock-server/vendor/github.com/pkg/errors/errors.go +288 -0
  84. package/eldlock-server/vendor/github.com/pkg/errors/go113.go +38 -0
  85. package/eldlock-server/vendor/github.com/pkg/errors/stack.go +177 -0
  86. package/eldlock-server/vendor/modules.txt +7 -0
  87. package/examples/eldlock.toml +17 -0
  88. package/install.sh +66 -0
  89. package/package.json +66 -0
  90. package/scripts/build-production.mjs +177 -0
  91. 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.