-
Notifications
You must be signed in to change notification settings - Fork 51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Keyring #2557
feat: Keyring #2557
Conversation
cli/keyring.go
Outdated
"errors" | ||
"syscall" | ||
|
||
"github.com/99designs/keyring" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: How confident are we in this dependency? The repo hasn't been updated in more than two years, and it is managed by a company that looks to have a focus way away from the security world and may lack the technical expertise or attention to keep it secure (assuming it and its dependencies were secure 2 years ago).
Some of its' dependencies are very out of date, and a couple are even more stale and suspicious looking than this package (e.g. https://github.com/gsterjov/go-libsecret with 14 stars, presumably responsible for the very old version of https://github.com/godbus/dbus)
Different websites say different things RE its' size, one says it only had 16 employees in 2022 (when the repo was last updated), other top search engine hits estimate 300, and 3777 employees.
99Designs:
Global creative platform for designers and clients
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've replaced this dependency with a custom implementation using a newer library.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dependencies and code of the new one look much healthier, thanks Keenan :)
cli/keyring.go
Outdated
FilePasswordFunc: prompt, | ||
FileDir: dir, | ||
KeyCtlScope: "user", | ||
KeyCtlPerm: 0, // TODO |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
todo: Please create a task if this is something that needs to be handled later. Otherwise it will linger.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will fix before merging.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ended up replacing this dependency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: I wonder if all this logic with generating hashes should be scoped to this cli package.
Maybe it makes sense to extract it in another place so other that other packages can use it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that would be nice. Would it make sense to have a crypto
package?
@@ -74,5 +74,12 @@ func NewStore(opts ...StoreOpt) (datastore.RootStore, error) { | |||
badgerOpts.ValueLogFileSize = options.valueLogFileSize | |||
badgerOpts.EncryptionKey = options.encryptionKey | |||
|
|||
if len(options.encryptionKey) > 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: I prefer if options.encryptionKey != ""
as it clearly shows that the type of the field is string, not a slice
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you may have misread the type. encryptionKey
is a []byte
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, my bad
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me, I think I only have documentation requests for you :)
Thanks a bunch Keenan :)
keyring/file.go
Outdated
|
||
type fileKeyring struct { | ||
dir string | ||
key []byte |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
todo: A comment on this prop explaining that this is the key to the keyring file, and not some cached content from within it would be handy please, it threw me a little bit at first :)
keyring/keyring.go
Outdated
) | ||
|
||
// ErrNotFound is returned when a keyring item is not found. | ||
var ErrNotFound = keyring.ErrNotFound |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
todo: I think this would be much easier to find/notice if declared in an errors.go file, than here. Is a little inconsistent with the rest of our codebase, and it will encourage bad behaviour if this package needs a second, third, etc error added later.
keyring/system.go
Outdated
|
||
type systemKeyring struct{} | ||
|
||
func newSystemKeyring() *systemKeyring { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
todo: A comment documenting that this keying implementation defers keying responsibility to the machine executing the code could be handy. Otherwise readers will need to guess/read the implementation.
if err != nil { | ||
return err | ||
} | ||
storeOpts = append(storeOpts, node.WithEncryptionKey(encryptionKey)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: This implies that if the keyring is enabled, then we also use use at-rest encryption. Should these technically be separate config options?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch! I've added a datastore.encryptionDisabled
config item.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update: I've made the key optional instead of the config option.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Heading out, but wanted to get this TODO on your radar before this got merged. May have more comments soon.
keyring/file.go
Outdated
if err := os.MkdirAll(dir, 0755); err != nil { | ||
return nil, err | ||
} | ||
key := pbkdf2.Key(password, []byte("defradb"), 4096, 32, sha1.New) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
todo: Shouldn't be using a static salt here. I believe if you use the jwa.PBES2_HS512_A256KW
instead of jwa.A256KW
here then the Encrypt
and Decrypt
calls will handle the KDF and salt for you, and the salt then gets included in the JWE headers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
then the key
is just the password
defined by the user, and you don't have to do the KDF manually.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and the added benefit that it will use sha256
instead of sha1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was hoping there was something like this in the library. I've replaced it as suggested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
one more, kk now im really heading out, will be AFK
keyring/system.go
Outdated
|
||
func (systemKeyring) Set(name string, key []byte) error { | ||
enc := base64.StdEncoding.EncodeToString(key) | ||
return keyring.Set(service, name, enc) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
todo: Adding comment here, but it applies to all the system Get/Set calls.
By using a static service name here, then if you have two nodes running on the same machine, that both want to use a the system store, their keys will collide.
Can either
A) Use a paramaterized service name via the config
B) Use the single service name as you have here, but add a namespace on the item names during the Get/Set
, which is set via the config.
C) Anything else you think of ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Decided to go with option A.
cli/config.go
Outdated
@@ -75,6 +75,7 @@ func defaultConfig() *viper.Viper { | |||
|
|||
cfg.SetDefault("datastore.badger.path", "data") | |||
cfg.SetDefault("datastore.encryptionDisabled", false) | |||
cfg.SetDefault("keyring.service", "defradb") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: "namespace" might be a more approachable/understandable name from an external UX POV
@@ -48,7 +48,7 @@ jobs: | |||
|
|||
- name: Attempt to start binary | |||
run: | | |||
./build/defradb start & | |||
./build/defradb start --no-keyring & |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: Why do we need to start with this flag?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Otherwise it will prompt for a password before starting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
todo: Might be worth adding instructions for this extra configuration step in the root README.md
(perhaps in the configuration section?) where as that is the default, and mention this flag to skip the keyring configuration.
|
||
cmd.PersistentFlags().Bool( | ||
"no-keyring", | ||
false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: By default we want the keyring?
question: For my understanding, what happens to the flow for using keys when there is no keyring? Does it make sense to make keyring functionality (disable or active) dependent on weather acp is on or off?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think enabled by default is a sensible option. The keyring also stores the peer key and encryption key. If you disable the keyring the peer key will be ephemeral and encryption disabled. I don't think the keyring should be dependent on ACP because of the other keys.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, would be nice to document this with my other todo
in the README.md
cli/keyring_generate.go
Outdated
defradb keyring generate --no-encryption-key | ||
|
||
Example: with system keyring | ||
defradb keyring generate --backend system`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
todo: This example is wrong Error: unknown flag: --backend
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's the global flag --keyring-backend
that is correct here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch!
"github.com/spf13/cobra" | ||
) | ||
|
||
func MakeKeyringExportCommand() *cobra.Command { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: What are your plans for testing all of this? Are you looking to just rely on manual testing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the file and system keyrings require user input, I don't think they are worth testing. I could add an option for mock (in memory) keyring and add tests for that if you or others think they would be useful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't need to mock to test it I don't think - tests can handle things like password prompts without to much hassle.
I'm a bit nervous about not pushing for this (either here or in a follow up PR), as apart from the openAPI stuff it would be the only thing in the whole repo that really requires manual testing (and thus is unexpected/surprising, as well as being generally more expensive and unreliable compared to automated testing).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll work on adding some tests. I think I have an idea on how to avoid the user input.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added unit tests for all keyring commands.
|
||
// Keyring provides a simple set/get interface for a keyring service. | ||
type Keyring interface { | ||
// Set stores the given key in the keystore under the given name. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: I think it is worth explicitly defining what should happen when calling Set
and a key of that name already exists, in the documentation on this interface.
suggestion: I think it is worth explicitly defining what should happen when calling Get
and Delete
and a name of a key that doesn't exist, in the documentation on this interface.
cli/keyring_generate.go
Outdated
return keyring.Set(peerKeyName, peerKey) | ||
}, | ||
} | ||
cmd.Flags().BoolVar(&noEncryption, "no-encryption-key", false, "Skip generating an encryption key") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: I think it might be worth highlighting that this would disable doc-encryption in the description text.
@@ -22,14 +22,14 @@ import ( | |||
|
|||
"github.com/bxcodec/faker/support/slice" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
todo: Can you please add some automated testing of encryption at rest? Perhaps a new test environment variable, and a CI job that runs with it enabled?
Maybe two env vars and CI jobs, one for each keyring type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a new variant for encrypted datastore to the test workflow
cmd.PersistentFlags().Bool( | ||
"no-keyring", | ||
false, | ||
"Disable the keyring and generate ephemeral keys", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: Would this disable encryption at rest? If so, I suggest noting that in this description.
question: What happens if keyring commands are called if the keyring is disabled?
question: Is this flag available for all commands? Not just start
/init
? If so what happens if you disable it whilst/after it has been in use (do you lose access to all data in the node?)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is what happens when you use --no-keyring
after it has been in use:
defradb start --no-keyring
2024-05-07T14:44:30.085-0400 ERROR badger v4@v4.2.1-0.20231113215945-a63444ca5276/db.go:263 Received err: Encryption key mismatch. Cleaning up...
Error: Encryption key mismatch
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: What happens if keyring commands are called if the keyring is disabled?
--no-keyring
is unused for keyring commands. It won't affect the outcome.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling it in start
is of less a worry to me than if someone uses it in an active instance, for example:
defradb client collection get --name User bae-123 --no-keyring
// or
defradb client collection create --name User '{ "name": "Bob" }' --no-keyring
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: Would this disable encryption at rest? If so, I suggest noting that in this description.
Yes, I'll update the description.
question: What happens if keyring commands are called if the keyring is disabled?
You can still use the keyring commands to manage keys.
question: Is this flag available for all commands? Not just start/init? If so what happens if you disable it whilst/after it has been in use?
This currently only applies to the start command. I'm planning on moving the flags that only apply to the start command in a future PR.
If you disable the keyring after using it, the peer key will be randomly generated, and at rest encryption will be disabled.
If at rest encryption was previously enabled, the datastore will fail to open due to the encryption key not matching.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
--no-keyring is unused for keyring commands. It won't affect the outcome.
It is a no-op? Might it be worth excluding it as a flag from those commands then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This currently only applies to the start command. I'm planning on moving the flags that only apply to the start command in a future PR.
Okay nice, I'm happy with that - thanks Keenan :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks Keenan - code is really readable, and I feel like we are getting good value per line out of this :)
Thanks for answering my questions :)
I have a couple of todos around testing, but perhaps we can probably get away with them being done in follow-up PRs if not here (and if no push back :)). If we want them to be done in new PRs, please create tickets for them before merge.
## Relevant issue(s) Resolves #2556 ## Description This PR adds a keyring to the cli. Notable changes: - created `keyring` package - file backed keystore uses a password based key generation to encrypt - system backed keystore uses the OS managed keyring - added `keyring` root command - `--no-keyring` disables the keyring and generates ephemeral keys - `--keyring-backend` flag sets the keyring backend (file or system) - `--keyring-path` flag sets the keyring directory when using file backend - `--keyring-namespace` flag sets the service name when using system backend - added `keyring generate` generates required keys - `--no-encryption-key` skips generating encryption key (disables datastore encryption) - added `keyring import` import external keys (hexadecimal for now) - added `keyring export` export existing key (hexadecimal for now) - moved key generation to `crypto` package ## Tasks - [x] I made sure the code is well commented, particularly hard-to-understand areas. - [x] I made sure the repository-held documentation is changed accordingly. - [x] I made sure the pull request title adheres to the conventional commit style (the subset used in the project can be found in [tools/configs/chglog/config.yml](tools/configs/chglog/config.yml)). - [x] I made sure to discuss its limitations such as threats to validity, vulnerability to mistake and misuse, robustness to invalidation of assumptions, resource requirements, ... ## How has this been tested? Manually tested on MacOS. Specify the platform(s) on which this was tested: - MacOS
Relevant issue(s)
Resolves #2556
Description
This PR adds a keyring to the cli.
Notable changes:
keyring
packagekeyring
root command--no-keyring
disables the keyring and generates ephemeral keys--keyring-backend
flag sets the keyring backend (file or system)--keyring-path
flag sets the keyring directory when using file backend--keyring-namespace
flag sets the service name when using system backendkeyring generate
generates required keys--no-encryption-key
skips generating encryption key (disables datastore encryption)keyring import
import external keys (hexadecimal for now)keyring export
export existing key (hexadecimal for now)crypto
packageTasks
How has this been tested?
Manually tested on MacOS.
Specify the platform(s) on which this was tested: