Skip to content

Commit b86e1d4

Browse files
committed
feat: MSX_STORE_KEY multi-mode support (unsafe-plain, aes-256-gcm, keyring) (#6)
- Adds keyring mode using github.com/99designs/keyring (macOS Keychain, Linux Secret Service, Windows DPAPI). Auto-generates key if none exists. - Adds unsafe-plain mode: explicit plain text with no warning. - When MSX_STORE_KEY is unset, defaults to unsafe-plain but prints a clear warning to stderr on every invocation. - aes-256-gcm:<hex> now uses the aes-256-gcm: prefix (breaking change from bare hex/base64 key format). - Consistency enforcement preserved: mode mismatch on read → clear error. - Updates README with full mode documentation. - Adds tests for mode parsing, warning behaviour, and round-trip. Closes #6
1 parent 3366444 commit b86e1d4

5 files changed

Lines changed: 342 additions & 76 deletions

File tree

README.md

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -78,33 +78,72 @@ Default state location:
7878
Override with:
7979
- `MSX_HOME=/some/path`
8080

81-
### Optional encryption at rest
81+
### Token store encryption (`MSX_STORE_KEY`)
8282

83-
By default msx relies on filesystem permissions (`0700` dir, `0600` DB file).
84-
For environments that require more, set `MSX_STORE_KEY` to a 32-byte key:
83+
`MSX_STORE_KEY` controls how token values are stored on disk. Three modes
84+
are supported:
85+
86+
| Value | Behaviour |
87+
|---|---|
88+
| *(not set)* | Plain text, **warning printed to stderr** on every invocation. |
89+
| `unsafe-plain` | Plain text, warning suppressed (explicit acknowledgement). |
90+
| `aes-256-gcm:<hex-key>` | AES-256-GCM encryption; key is 32 bytes as 64 hex chars. |
91+
| `keyring` | Key stored in / retrieved from the platform keyring (see below). |
92+
93+
Token values are encrypted in the bbolt store using **AES-256-GCM** with a
94+
random 12-byte nonce per write. Profile metadata (client ID, authority,
95+
scopes) is not encrypted.
96+
97+
**Consistency is enforced**: if stored tokens were written in one mode and
98+
read in another, msx will fail with a clear error rather than silently
99+
mis-reading data.
100+
101+
#### `aes-256-gcm` mode
85102

86103
```sh
87104
# Generate a key (store it somewhere safe — 1Password works well):
88105
openssl rand -hex 32
89106

90-
# Then export it in every shell session that runs msx:
91-
export MSX_STORE_KEY=<64 hex chars>
107+
# Export it in every shell session that runs msx:
108+
export MSX_STORE_KEY=aes-256-gcm:<64-hex-chars>
92109
```
93110

94-
When the variable is set, token values are encrypted in the bbolt store using
95-
**AES-256-GCM** with a random 12-byte nonce per write. Profile metadata
96-
(client ID, authority, scopes) is not encrypted.
97-
98-
Key management expectations:
99111
- The key is **never persisted to disk** by msx. You are responsible for keeping it.
100112
- If you lose the key you can still recover tokens using `state-export` while
101113
the key is available, then reinstate them with `state-import` once you have
102114
a replacement.
103-
- Consistency is enforced: if a key is set but stored tokens are plain (or
104-
vice versa), msx will fail with a clear error rather than silently
105-
mis-reading data.
106-
- Encryption is optional. Plain-mode operation (no key) is the default and
107-
remains fully supported.
115+
- Best for: automation, CI/CD, scripts where you control the environment.
116+
117+
#### `keyring` mode
118+
119+
```sh
120+
export MSX_STORE_KEY=keyring
121+
```
122+
123+
msx uses the platform keyring to store and retrieve the encryption key:
124+
- **macOS** — Keychain
125+
- **Linux** — Secret Service (GNOME Keyring / KWallet) with a file-backend
126+
fallback for headless environments
127+
- **Windows** — DPAPI / Windows Credential Manager
128+
129+
If no key exists yet, msx generates a 32-byte random key and saves it
130+
automatically. The key is stored under service `msx`, item `store-key`.
131+
132+
- Best for: interactive developer machines where you want zero key management.
133+
- Note: may prompt interactively on some platforms. For automation use
134+
`aes-256-gcm:<hex-key>` instead.
135+
136+
#### `unsafe-plain` mode
137+
138+
```sh
139+
export MSX_STORE_KEY=unsafe-plain
140+
```
141+
142+
Tokens are stored as plain JSON. This is the default when `MSX_STORE_KEY` is
143+
absent, but setting it explicitly suppresses the startup warning.
144+
145+
- Best for: trusted single-user machines or environments where filesystem
146+
permissions (`0700` dir, `0600` DB file) are your only security boundary.
108147

109148
Stored per profile:
110149
- authority / tenant hint

go.mod

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,14 @@ go 1.26
44

55
require go.etcd.io/bbolt v1.4.3
66

7-
require golang.org/x/sys v0.29.0 // indirect
7+
require (
8+
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
9+
github.com/99designs/keyring v1.2.2
10+
github.com/danieljoos/wincred v1.1.2 // indirect
11+
github.com/dvsekhvalnov/jose2go v1.5.0 // indirect
12+
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
13+
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
14+
github.com/mtibben/percent v0.2.1 // indirect
15+
golang.org/x/sys v0.29.0 // indirect
16+
golang.org/x/term v0.3.0 // indirect
17+
)

go.sum

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,40 @@
1+
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs=
2+
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4=
3+
github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0=
4+
github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk=
5+
github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0=
6+
github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0=
7+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
18
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
29
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10+
github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM=
11+
github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU=
12+
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0=
13+
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
14+
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU=
15+
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0=
16+
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
17+
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
18+
github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs=
19+
github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns=
20+
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
321
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
422
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
23+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
24+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
525
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
626
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
727
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
828
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
929
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
1030
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
31+
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1132
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
1233
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
34+
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
35+
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
36+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
37+
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
38+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
1339
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
1440
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)