@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,132 @@
1
+ package main
2
+
3
+ import (
4
+ "context"
5
+ "crypto/sha256"
6
+ "encoding/hex"
7
+ "fmt"
8
+ "os"
9
+ "os/signal"
10
+ "path/filepath"
11
+ "syscall"
12
+
13
+ "github.com/eldlock/eldlock-server/internal/api"
14
+ "github.com/eldlock/eldlock-server/internal/yubikey"
15
+ )
16
+
17
+ func main() {
18
+ if len(os.Args) > 1 && os.Args[1] == "version" {
19
+ fmt.Println("eldlock-server dev")
20
+ return
21
+ }
22
+
23
+ if len(os.Args) > 1 && os.Args[1] == "init" {
24
+ if err := initVault(argAt(2)); err != nil {
25
+ fmt.Fprintln(os.Stderr, err)
26
+ os.Exit(1)
27
+ }
28
+ return
29
+ }
30
+
31
+ if len(os.Args) > 1 && os.Args[1] == "serve" {
32
+ if err := serve(); err != nil {
33
+ fmt.Fprintln(os.Stderr, err)
34
+ os.Exit(1)
35
+ }
36
+ return
37
+ }
38
+
39
+ fmt.Println(`eldlock-server
40
+
41
+ Usage:
42
+ eldlock-server version
43
+ eldlock-server init [name]
44
+ eldlock-server serve
45
+
46
+ The serve command starts the local Unix socket API used by eldlock-cli.`)
47
+ }
48
+
49
+ func initVault(vaultName string) error {
50
+ stateDir := defaultStateDir()
51
+ provider, err := yubikey.NewProviderFromEnv()
52
+ if err != nil {
53
+ return err
54
+ }
55
+ resolver := api.FileStoreResolver{StateDir: stateDir, KeyProvider: provider}
56
+ normalizedName, err := api.NormalizeVaultName(vaultName)
57
+ if err != nil {
58
+ return err
59
+ }
60
+ secretStore, err := resolver.StoreForVault(normalizedName)
61
+ if err != nil {
62
+ return err
63
+ }
64
+ if err := secretStore.Init(); err != nil {
65
+ return err
66
+ }
67
+ fmt.Printf("initialized Eldlock vault %q at %s using %s\n", normalizedName, resolver.PathForVault(normalizedName), provider.Name())
68
+ return nil
69
+ }
70
+
71
+ func serve() error {
72
+ socketPath := defaultSocketPath()
73
+ stateDir := defaultStateDir()
74
+
75
+ provider, err := yubikey.NewProviderFromEnv()
76
+ if err != nil {
77
+ return err
78
+ }
79
+ resolver := api.FileStoreResolver{StateDir: stateDir, KeyProvider: provider}
80
+ defaultStore, err := resolver.StoreForVault(api.DefaultVaultName)
81
+ if err != nil {
82
+ return err
83
+ }
84
+ service := api.NewService(defaultStore)
85
+ service.StoreResolver = resolver
86
+ server := api.Server{
87
+ SocketPath: socketPath,
88
+ Service: service,
89
+ }
90
+
91
+ ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
92
+ defer stop()
93
+
94
+ fmt.Printf("eldlock-server listening on %s\n", socketPath)
95
+ fmt.Printf("encrypted vault state at %s\n", stateDir)
96
+ fmt.Printf("passkey provider %s\n", provider.Name())
97
+ return server.ListenAndServe(ctx)
98
+ }
99
+
100
+ func argAt(index int) string {
101
+ if len(os.Args) > index {
102
+ return os.Args[index]
103
+ }
104
+ return ""
105
+ }
106
+
107
+ func defaultSocketPath() string {
108
+ if value := os.Getenv("ELDLOCK_SOCKET"); value != "" {
109
+ return value
110
+ }
111
+ sum := sha256.Sum256([]byte(defaultStateDir()))
112
+ digest := hex.EncodeToString(sum[:])[:16]
113
+ return filepath.Join(os.TempDir(), "eldlock", digest, "daemon.sock")
114
+ }
115
+
116
+ func defaultStorePath() string {
117
+ if value := os.Getenv("ELDLOCK_STORE"); value != "" {
118
+ return value
119
+ }
120
+ return filepath.Join(defaultStateDir(), "vault.json")
121
+ }
122
+
123
+ func defaultStateDir() string {
124
+ if value := os.Getenv("ELDLOCK_STATE_DIR"); value != "" {
125
+ return value
126
+ }
127
+ cwd, err := os.Getwd()
128
+ if err == nil {
129
+ return filepath.Join(cwd, ".eldlock")
130
+ }
131
+ return filepath.Join(os.TempDir(), "eldlock")
132
+ }
@@ -0,0 +1,10 @@
1
+ module github.com/eldlock/eldlock-server
2
+
3
+ go 1.26
4
+
5
+ require (
6
+ github.com/keys-pub/go-libfido2 v1.5.3 // indirect
7
+ github.com/pkg/errors v0.9.1 // indirect
8
+ )
9
+
10
+ replace github.com/keys-pub/go-libfido2 => ./internal/libfido2
@@ -0,0 +1,11 @@
1
+ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2
+ github.com/keys-pub/go-libfido2 v1.5.3 h1:vtgHxlSB43u6lj0TSuA3VvT6z3E7VI+L1a2hvMFdECk=
3
+ github.com/keys-pub/go-libfido2 v1.5.3/go.mod h1:P0V19qHwJNY0htZwZDe9Ilvs/nokGhdFX7faKFyZ6+U=
4
+ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
5
+ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
6
+ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
7
+ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
8
+ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
9
+ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
10
+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
11
+ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -0,0 +1,14 @@
1
+ # API
2
+
3
+ Local Unix socket API for `eldlock-cli`, shell hooks, and other local integrations.
4
+
5
+ The API is not trusted by default. Every request must be authorized by the server-side policy engine.
6
+
7
+ Current testable commands:
8
+
9
+ - `init` creates the daemon-owned prototype vault.
10
+ - `secret.add` stores env and SSH secret material in the daemon-owned store.
11
+ - `secret.read` releases a secret as either plaintext output or a server-side clipboard write.
12
+ - `secret.list` returns secret names and types without values.
13
+
14
+ The CLI forwards user intent to this API. It must not decide policy, inspect approval state, or write clipboard contents itself.
@@ -0,0 +1,126 @@
1
+ package api
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "fmt"
7
+
8
+ "github.com/eldlock/eldlock-server/internal/store"
9
+ "github.com/eldlock/eldlock-server/internal/yubikey"
10
+ )
11
+
12
+ type SecretStore interface {
13
+ Init() error
14
+ IsInitialized() (bool, error)
15
+ Put(store.Secret) error
16
+ Get(string) (store.Secret, error)
17
+ Delete(string) error
18
+ List() ([]store.SecretSummary, error)
19
+ }
20
+
21
+ type PINSecretStore interface {
22
+ InitWithPIN(string) error
23
+ PutWithPIN(store.Secret, string) error
24
+ GetWithPIN(string, string) (store.Secret, error)
25
+ DeleteWithPIN(string, string) error
26
+ ListWithPIN(string) ([]store.SecretSummary, error)
27
+ }
28
+
29
+ type PINBulkSecretStore interface {
30
+ PutManyWithPIN([]store.Secret, string) error
31
+ }
32
+
33
+ type EnvSecretStore interface {
34
+ EnvWithPIN(string) (map[string]string, error)
35
+ }
36
+
37
+ type Clipboard interface {
38
+ Write(context.Context, string) error
39
+ }
40
+
41
+ type StoreResolver interface {
42
+ StoreForVault(string) (SecretStore, error)
43
+ }
44
+
45
+ type Service struct {
46
+ Store SecretStore
47
+ StoreResolver StoreResolver
48
+ Clipboard Clipboard
49
+ }
50
+
51
+ func NewService(secretStore SecretStore) *Service {
52
+ return &Service{
53
+ Store: secretStore,
54
+ Clipboard: LocalClipboard{},
55
+ }
56
+ }
57
+
58
+ func (s *Service) Handle(ctx context.Context, req Request) Response {
59
+ service, response, ok := s.withRequestStore(req)
60
+ if !ok {
61
+ return response
62
+ }
63
+
64
+ switch req.Action {
65
+ case ActionHealth:
66
+ return Response{OK: true, Message: "eldlock-server ok"}
67
+ case ActionInit:
68
+ return service.initVault(req)
69
+ case ActionAddSecret:
70
+ return service.addSecret(req)
71
+ case ActionReadSecret:
72
+ return service.readSecret(ctx, req)
73
+ case ActionRemoveSecret:
74
+ return service.removeSecret(req)
75
+ case ActionListSecrets:
76
+ return service.listSecrets(req)
77
+ case ActionImportEnv:
78
+ return service.importEnv(req)
79
+ case ActionExec:
80
+ return service.execProcess(ctx, req)
81
+ default:
82
+ return failure("unknown action")
83
+ }
84
+ }
85
+
86
+ func (s *Service) withRequestStore(req Request) (*Service, Response, bool) {
87
+ if req.Action == ActionHealth || s.StoreResolver == nil {
88
+ return s, Response{}, true
89
+ }
90
+ secretStore, err := s.StoreResolver.StoreForVault(req.Vault)
91
+ if err != nil {
92
+ return nil, failure(fmt.Sprintf("resolve vault: %v", err)), false
93
+ }
94
+ scoped := *s
95
+ scoped.Store = secretStore
96
+ scoped.StoreResolver = nil
97
+ return &scoped, Response{}, true
98
+ }
99
+
100
+ func (s *Service) requireInitialized() Response {
101
+ initialized, err := s.Store.IsInitialized()
102
+ if err != nil {
103
+ return failure(fmt.Sprintf("check vault: %v", err))
104
+ }
105
+ if !initialized {
106
+ return failure("vault is not initialized; run eldlock init")
107
+ }
108
+ return Response{OK: true}
109
+ }
110
+
111
+ func failure(message string) Response {
112
+ return Response{OK: false, Error: message}
113
+ }
114
+
115
+ func pinAwareFailure(message string, err error) Response {
116
+ switch {
117
+ case errors.Is(err, yubikey.ErrPINRequired):
118
+ return Response{OK: false, Error: message, Code: "pin_required"}
119
+ case errors.Is(err, yubikey.ErrPINInvalid):
120
+ return Response{OK: false, Error: message, Code: "pin_invalid"}
121
+ case errors.Is(err, yubikey.ErrPINAuthBlocked):
122
+ return Response{OK: false, Error: message, Code: "pin_auth_blocked"}
123
+ default:
124
+ return failure(message)
125
+ }
126
+ }
@@ -0,0 +1,97 @@
1
+ package api
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "errors"
7
+ "fmt"
8
+ "os"
9
+ "os/exec"
10
+ )
11
+
12
+ func (s *Service) execProcess(ctx context.Context, req Request) Response {
13
+ if response := s.requireInitialized(); !response.OK {
14
+ return response
15
+ }
16
+ if !req.Interactive && len(req.Command) == 0 && req.ShellCommand == "" {
17
+ return failure("exec command is required")
18
+ }
19
+ if len(req.Command) > 0 && req.Command[0] == "" {
20
+ return failure("exec command cannot be empty")
21
+ }
22
+ envSecrets, err := s.envWithPIN(req.PIN)
23
+ if err != nil {
24
+ return pinAwareFailure(fmt.Sprintf("load exec environment: %v", err), err)
25
+ }
26
+
27
+ if req.Interactive {
28
+ return Response{OK: true, Env: envSecrets}
29
+ }
30
+
31
+ cmd := execCommand(ctx, req)
32
+ if req.CWD != "" {
33
+ cmd.Dir = req.CWD
34
+ }
35
+ cmd.Env = envList(overlayEnv(req.Env, envSecrets))
36
+
37
+ var stdout bytes.Buffer
38
+ var stderr bytes.Buffer
39
+ cmd.Stdout = &stdout
40
+ cmd.Stderr = &stderr
41
+
42
+ err = cmd.Run()
43
+ exitCode := 0
44
+ if err != nil {
45
+ var exitErr *exec.ExitError
46
+ if errors.As(err, &exitErr) {
47
+ exitCode = exitErr.ExitCode()
48
+ } else {
49
+ return failure(fmt.Sprintf("run exec command: %v", err))
50
+ }
51
+ }
52
+
53
+ return Response{
54
+ OK: true,
55
+ Stdout: stdout.String(),
56
+ Stderr: stderr.String(),
57
+ ExitCode: exitCode,
58
+ }
59
+ }
60
+
61
+ func execCommand(ctx context.Context, req Request) *exec.Cmd {
62
+ if req.ShellCommand == "" {
63
+ return exec.CommandContext(ctx, req.Command[0], req.Command[1:]...)
64
+ }
65
+
66
+ shell := req.Shell
67
+ if shell == "" {
68
+ shell = req.Env["SHELL"]
69
+ }
70
+ if shell == "" {
71
+ shell = os.Getenv("SHELL")
72
+ }
73
+ if shell == "" {
74
+ shell = "/bin/sh"
75
+ }
76
+
77
+ return exec.CommandContext(ctx, shell, "-lc", req.ShellCommand)
78
+ }
79
+
80
+ func overlayEnv(base map[string]string, values map[string]string) map[string]string {
81
+ env := make(map[string]string, len(base)+len(values))
82
+ for key, value := range base {
83
+ env[key] = value
84
+ }
85
+ for key, value := range values {
86
+ env[key] = value
87
+ }
88
+ return env
89
+ }
90
+
91
+ func envList(env map[string]string) []string {
92
+ out := make([]string, 0, len(env))
93
+ for key, value := range env {
94
+ out = append(out, key+"="+value)
95
+ }
96
+ return out
97
+ }