@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,358 @@
1
+ package api
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "fmt"
7
+ "os"
8
+ "path/filepath"
9
+ "strings"
10
+ "unicode"
11
+
12
+ "github.com/eldlock/eldlock-server/internal/store"
13
+ )
14
+
15
+ func (s *Service) addSecret(req Request) Response {
16
+ if response := s.requireInitialized(); !response.OK {
17
+ return response
18
+ }
19
+
20
+ name := strings.TrimSpace(req.Name)
21
+ if name == "" {
22
+ return failure("secret name is required")
23
+ }
24
+ if req.Type != store.SecretTypeEnv && req.Type != store.SecretTypeSSH {
25
+ return failure("secret type must be env or ssh")
26
+ }
27
+
28
+ value := req.Value
29
+ if req.SourcePath != "" {
30
+ raw, err := os.ReadFile(req.SourcePath)
31
+ if err != nil {
32
+ return failure(fmt.Sprintf("read source file: %v", err))
33
+ }
34
+ value = string(raw)
35
+ }
36
+ if value == "" {
37
+ return failure("secret value is required")
38
+ }
39
+
40
+ if err := s.putWithPIN(store.Secret{Name: name, Type: req.Type, Value: value}, req.PIN); err != nil {
41
+ return pinAwareFailure(fmt.Sprintf("store secret: %v", err), err)
42
+ }
43
+
44
+ return Response{OK: true, Message: fmt.Sprintf("stored %s secret %q", req.Type, name)}
45
+ }
46
+
47
+ func (s *Service) readSecret(ctx context.Context, req Request) Response {
48
+ if response := s.requireInitialized(); !response.OK {
49
+ return response
50
+ }
51
+
52
+ name := strings.TrimSpace(req.Name)
53
+ if name == "" {
54
+ return failure("secret name is required")
55
+ }
56
+ if req.Output != OutputPlain && req.Output != OutputClipboard {
57
+ return failure("read output must be plain or clipboard")
58
+ }
59
+
60
+ secret, err := s.getWithPIN(name, req.PIN)
61
+ if errors.Is(err, store.ErrNotFound) {
62
+ return failure("secret not found")
63
+ }
64
+ if err != nil {
65
+ return pinAwareFailure(fmt.Sprintf("read secret: %v", err), err)
66
+ }
67
+
68
+ switch req.Output {
69
+ case OutputPlain:
70
+ return Response{OK: true, Value: secret.Value}
71
+ case OutputClipboard:
72
+ if s.Clipboard == nil {
73
+ return failure("clipboard is not configured")
74
+ }
75
+ if err := s.Clipboard.Write(ctx, secret.Value); err != nil {
76
+ return failure(fmt.Sprintf("write clipboard: %v", err))
77
+ }
78
+ return Response{OK: true, Message: fmt.Sprintf("copied %q to clipboard", name)}
79
+ default:
80
+ return failure("read output must be plain or clipboard")
81
+ }
82
+ }
83
+
84
+ func (s *Service) removeSecret(req Request) Response {
85
+ if response := s.requireInitialized(); !response.OK {
86
+ return response
87
+ }
88
+
89
+ name := strings.TrimSpace(req.Name)
90
+ if name == "" {
91
+ return failure("secret name is required")
92
+ }
93
+
94
+ if err := s.deleteWithPIN(name, req.PIN); errors.Is(err, store.ErrNotFound) {
95
+ return failure("secret not found")
96
+ } else if err != nil {
97
+ return pinAwareFailure(fmt.Sprintf("remove secret: %v", err), err)
98
+ }
99
+
100
+ return Response{OK: true, Message: fmt.Sprintf("removed secret %q", name)}
101
+ }
102
+
103
+ func (s *Service) listSecrets(req Request) Response {
104
+ if response := s.requireInitialized(); !response.OK {
105
+ return response
106
+ }
107
+
108
+ secrets, err := s.listWithPIN(req.PIN)
109
+ if err != nil {
110
+ return pinAwareFailure(fmt.Sprintf("list secrets: %v", err), err)
111
+ }
112
+ return Response{OK: true, Secrets: secrets}
113
+ }
114
+
115
+ func (s *Service) importEnv(req Request) Response {
116
+ if response := s.requireInitialized(); !response.OK {
117
+ return response
118
+ }
119
+
120
+ sourcePath := strings.TrimSpace(req.SourcePath)
121
+ if sourcePath == "" {
122
+ sourcePath = ".env"
123
+ }
124
+ if !filepath.IsAbs(sourcePath) && req.CWD != "" {
125
+ sourcePath = filepath.Join(req.CWD, sourcePath)
126
+ }
127
+
128
+ raw, err := os.ReadFile(sourcePath)
129
+ if err != nil {
130
+ return failure(fmt.Sprintf("read env file: %v", err))
131
+ }
132
+ values, err := parseEnvFile(string(raw))
133
+ if err != nil {
134
+ return failure(fmt.Sprintf("parse env file: %v", err))
135
+ }
136
+ if len(values) == 0 {
137
+ return failure("env file did not contain any variables")
138
+ }
139
+
140
+ secrets := make([]store.Secret, 0, len(values))
141
+ for name, value := range values {
142
+ secrets = append(secrets, store.Secret{Name: name, Type: store.SecretTypeEnv, Value: value})
143
+ }
144
+ if err := s.putManyWithPIN(secrets, req.PIN); err != nil {
145
+ return pinAwareFailure(fmt.Sprintf("import env file: %v", err), err)
146
+ }
147
+
148
+ return Response{OK: true, Imported: len(secrets), Message: fmt.Sprintf("imported %d env secrets", len(secrets))}
149
+ }
150
+
151
+ func (s *Service) putWithPIN(secret store.Secret, pin string) error {
152
+ if store, ok := s.Store.(PINSecretStore); ok {
153
+ return store.PutWithPIN(secret, pin)
154
+ }
155
+ return s.Store.Put(secret)
156
+ }
157
+
158
+ func (s *Service) putManyWithPIN(secrets []store.Secret, pin string) error {
159
+ if store, ok := s.Store.(PINBulkSecretStore); ok {
160
+ return store.PutManyWithPIN(secrets, pin)
161
+ }
162
+ for _, secret := range secrets {
163
+ if err := s.putWithPIN(secret, pin); err != nil {
164
+ return err
165
+ }
166
+ }
167
+ return nil
168
+ }
169
+
170
+ func (s *Service) getWithPIN(name string, pin string) (store.Secret, error) {
171
+ if store, ok := s.Store.(PINSecretStore); ok {
172
+ return store.GetWithPIN(name, pin)
173
+ }
174
+ return s.Store.Get(name)
175
+ }
176
+
177
+ func (s *Service) deleteWithPIN(name string, pin string) error {
178
+ if store, ok := s.Store.(PINSecretStore); ok {
179
+ return store.DeleteWithPIN(name, pin)
180
+ }
181
+ return s.Store.Delete(name)
182
+ }
183
+
184
+ func (s *Service) listWithPIN(pin string) ([]store.SecretSummary, error) {
185
+ if store, ok := s.Store.(PINSecretStore); ok {
186
+ return store.ListWithPIN(pin)
187
+ }
188
+ return s.Store.List()
189
+ }
190
+
191
+ func (s *Service) envWithPIN(pin string) (map[string]string, error) {
192
+ if store, ok := s.Store.(EnvSecretStore); ok {
193
+ return store.EnvWithPIN(pin)
194
+ }
195
+ secrets, err := s.Store.List()
196
+ if err != nil {
197
+ return nil, err
198
+ }
199
+ env := make(map[string]string)
200
+ for _, summary := range secrets {
201
+ if summary.Type == store.SecretTypeEnv {
202
+ secret, err := s.getWithPIN(summary.Name, pin)
203
+ if err != nil {
204
+ return nil, err
205
+ }
206
+ env[summary.Name] = secret.Value
207
+ }
208
+ }
209
+ return env, nil
210
+ }
211
+
212
+ func parseEnvFile(contents string) (map[string]string, error) {
213
+ out := map[string]string{}
214
+ for index, rawLine := range strings.Split(contents, "\n") {
215
+ lineNumber := index + 1
216
+ line := strings.TrimSpace(strings.TrimSuffix(rawLine, "\r"))
217
+ if line == "" || strings.HasPrefix(line, "#") {
218
+ continue
219
+ }
220
+ if strings.HasPrefix(line, "export ") {
221
+ line = strings.TrimSpace(strings.TrimPrefix(line, "export "))
222
+ }
223
+ key, value, ok := strings.Cut(line, "=")
224
+ if !ok {
225
+ return nil, fmt.Errorf("line %d: expected KEY=value", lineNumber)
226
+ }
227
+ key = strings.TrimSpace(key)
228
+ if !validEnvKey(key) {
229
+ return nil, fmt.Errorf("line %d: invalid env key", lineNumber)
230
+ }
231
+ parsed, err := parseEnvValue(value)
232
+ if err != nil {
233
+ return nil, fmt.Errorf("line %d: %w", lineNumber, err)
234
+ }
235
+ out[key] = parsed
236
+ }
237
+ return out, nil
238
+ }
239
+
240
+ func parseEnvValue(value string) (string, error) {
241
+ value = strings.TrimSpace(value)
242
+ if value == "" {
243
+ return "", nil
244
+ }
245
+ switch value[0] {
246
+ case '\'':
247
+ return parseSingleQuotedEnv(value)
248
+ case '"':
249
+ inner, err := parseDoubleQuotedEnv(value)
250
+ if err != nil {
251
+ return "", err
252
+ }
253
+ return unescapeDoubleQuotedEnv(inner)
254
+ default:
255
+ return strings.TrimSpace(stripInlineEnvComment(value)), nil
256
+ }
257
+ }
258
+
259
+ func parseSingleQuotedEnv(value string) (string, error) {
260
+ closing := strings.IndexRune(value[1:], '\'')
261
+ if closing < 0 {
262
+ return "", errors.New("unterminated single-quoted value")
263
+ }
264
+ closing++
265
+ if err := validateQuotedEnvTail(value[closing+1:]); err != nil {
266
+ return "", err
267
+ }
268
+ return value[1:closing], nil
269
+ }
270
+
271
+ func parseDoubleQuotedEnv(value string) (string, error) {
272
+ escaped := false
273
+ for index, char := range value[1:] {
274
+ actualIndex := index + 1
275
+ if escaped {
276
+ escaped = false
277
+ continue
278
+ }
279
+ if char == '\\' {
280
+ escaped = true
281
+ continue
282
+ }
283
+ if char == '"' {
284
+ if err := validateQuotedEnvTail(value[actualIndex+1:]); err != nil {
285
+ return "", err
286
+ }
287
+ return value[1:actualIndex], nil
288
+ }
289
+ }
290
+ return "", errors.New("unterminated double-quoted value")
291
+ }
292
+
293
+ func validateQuotedEnvTail(tail string) error {
294
+ tail = strings.TrimSpace(tail)
295
+ if tail == "" || strings.HasPrefix(tail, "#") {
296
+ return nil
297
+ }
298
+ return errors.New("unexpected characters after quoted value")
299
+ }
300
+
301
+ func stripInlineEnvComment(value string) string {
302
+ for index, char := range value {
303
+ if char == '#' && (index == 0 || unicode.IsSpace(rune(value[index-1]))) {
304
+ return value[:index]
305
+ }
306
+ }
307
+ return value
308
+ }
309
+
310
+ func unescapeDoubleQuotedEnv(value string) (string, error) {
311
+ var builder strings.Builder
312
+ escaped := false
313
+ for _, char := range value {
314
+ if escaped {
315
+ switch char {
316
+ case 'n':
317
+ builder.WriteByte('\n')
318
+ case 'r':
319
+ builder.WriteByte('\r')
320
+ case 't':
321
+ builder.WriteByte('\t')
322
+ case '"', '\\', '$':
323
+ builder.WriteRune(char)
324
+ default:
325
+ builder.WriteRune(char)
326
+ }
327
+ escaped = false
328
+ continue
329
+ }
330
+ if char == '\\' {
331
+ escaped = true
332
+ continue
333
+ }
334
+ builder.WriteRune(char)
335
+ }
336
+ if escaped {
337
+ builder.WriteByte('\\')
338
+ }
339
+ return builder.String(), nil
340
+ }
341
+
342
+ func validEnvKey(key string) bool {
343
+ if key == "" {
344
+ return false
345
+ }
346
+ for index, char := range key {
347
+ if index == 0 {
348
+ if char != '_' && !unicode.IsLetter(char) {
349
+ return false
350
+ }
351
+ continue
352
+ }
353
+ if char != '_' && !unicode.IsLetter(char) && !unicode.IsDigit(char) {
354
+ return false
355
+ }
356
+ }
357
+ return true
358
+ }
@@ -0,0 +1,72 @@
1
+ package api
2
+
3
+ import (
4
+ "bufio"
5
+ "context"
6
+ "encoding/json"
7
+ "fmt"
8
+ "net"
9
+ "os"
10
+ "path/filepath"
11
+ )
12
+
13
+ type Server struct {
14
+ SocketPath string
15
+ Service *Service
16
+ }
17
+
18
+ func (s *Server) ListenAndServe(ctx context.Context) error {
19
+ if s.SocketPath == "" {
20
+ return fmt.Errorf("socket path is required")
21
+ }
22
+ if s.Service == nil {
23
+ return fmt.Errorf("service is required")
24
+ }
25
+ if err := os.MkdirAll(filepath.Dir(s.SocketPath), 0o700); err != nil {
26
+ return fmt.Errorf("create socket directory: %w", err)
27
+ }
28
+ if err := os.Remove(s.SocketPath); err != nil && !os.IsNotExist(err) {
29
+ return fmt.Errorf("remove old socket: %w", err)
30
+ }
31
+
32
+ listener, err := net.Listen("unix", s.SocketPath)
33
+ if err != nil {
34
+ return fmt.Errorf("listen on socket: %w", err)
35
+ }
36
+ defer listener.Close()
37
+ defer os.Remove(s.SocketPath)
38
+
39
+ if unixListener, ok := listener.(*net.UnixListener); ok {
40
+ go func() {
41
+ <-ctx.Done()
42
+ _ = unixListener.Close()
43
+ }()
44
+ }
45
+
46
+ for {
47
+ conn, err := listener.Accept()
48
+ if err != nil {
49
+ if ctx.Err() != nil {
50
+ return nil
51
+ }
52
+ return fmt.Errorf("accept connection: %w", err)
53
+ }
54
+ go s.handleConn(ctx, conn)
55
+ }
56
+ }
57
+
58
+ func (s *Server) handleConn(ctx context.Context, conn net.Conn) {
59
+ defer conn.Close()
60
+
61
+ scanner := bufio.NewScanner(conn)
62
+ scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
63
+ encoder := json.NewEncoder(conn)
64
+ for scanner.Scan() {
65
+ var req Request
66
+ if err := json.Unmarshal(scanner.Bytes(), &req); err != nil {
67
+ _ = encoder.Encode(failure("invalid request json"))
68
+ continue
69
+ }
70
+ _ = encoder.Encode(s.Service.Handle(ctx, req))
71
+ }
72
+ }