Skip to content

Commit

Permalink
Easier unencrypted keys access. (#41)
Browse files Browse the repository at this point in the history
Added a with_deep_deundescored_keys util method that iterates over all keys in a secrets hash and duplicates the underscored ones as de-underscored. This so we can more eassily access the unencrypted secrets without having to call it with an underscore in our rails application.
  • Loading branch information
svanhesteren authored Jul 17, 2024
1 parent c04cc1d commit 6698013
Show file tree
Hide file tree
Showing 14 changed files with 91 additions and 9 deletions.
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
eyaml (0.4.0)
eyaml (0.4.3)
rbnacl (~> 7.1)
thor (~> 1.1)

Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,22 @@ If you're using the new Apple M1, you need to ensure that you're using a `ffi` t
gem "ffi", github: "cheddar-me/ffi", branch: "apple-m1", submodules: true
```

### Underscored vs de-underscored keys

Keys that start with an underscore are treated as-is and are assumed unencrypted in the secrets/credentials files.
To make our lives a little easier in calling them in the application they are callable without the underscore. So a `_secret` can be called with

```ruby
Rails.application.credentials.secret
```
and
```ruby
Rails.application.credentials._secret
```

To prevent conflicts with having the same name underscored and not, we don't allow that and the gem will raise an exception.
This makes sense since we believe it could be a security hazard to have an encrypted key also unencrypted. The best solution is to give either a different name to make the intention clear.

## Development

To get started, make sure you have a working version of Ruby locally. Then clone the repo, and run `bin/setup` (this will install `libsodium` if you're on a Mac and setup bundler). Running `bundle exec rake` or `bundle exec rake spec` will run the test suite.
Expand Down
1 change: 0 additions & 1 deletion lib/eyaml/encryption_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ def traverse(tree, &block)
if value.is_a?(Hash)
next [key, traverse(value, &block)]
end
# TODO(es): Add tests for keys with an underscore prefix not doing a nested skip
if key.start_with?("_")
next [key, value]
end
Expand Down
3 changes: 2 additions & 1 deletion lib/eyaml/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ class ConflictError < StandardError
# for a public/private key in the key directory (either $EJSON_KEYDIR, if set, or /opt/ejson/keys)
cipherdata = YAML.load_file(file)
secrets = EYAML.decrypt(cipherdata, private_key: ENV[PRIVATE_KEY_ENV_VAR])
.except("_public_key")
secrets = EYAML::Util.with_deep_deundescored_keys(secrets)
.deep_symbolize_keys
.except(:_public_key)

break Rails.application.send(secrets_or_credentials).deep_merge!(secrets)
end
Expand Down
20 changes: 20 additions & 0 deletions lib/eyaml/util.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,26 @@ class << self
def pretty_yaml(some_hash)
some_hash.to_yaml.delete_prefix("---\n")
end

# This will look for any keys that starts with an underscore and duplicates that key-value pair
# but without the starting underscore.
# So {_a: "abab"} will become {_a: "abab", a: "abab"}
# This so we can easilly access our unencrypted secrets without having to add an underscore
def with_deep_deundescored_keys(hash)
hash.each_with_object({}) do |(key, value), total|
value = with_deep_deundescored_keys(value) if value.is_a?(Hash)

if key.start_with?("_")
deunderscored_key = key[1..]
# We don't want to have an underscored and de-underscored key with the same name, so raise. This could be a security issue
raise KeyError, "De-underscored key '#{key[1..]}' already exists." if total.key?(deunderscored_key)

total[deunderscored_key] = value unless total.key?(deunderscored_key)
end

total[key] = value
end
end
end
end
end
2 changes: 1 addition & 1 deletion lib/eyaml/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module EYAML
VERSION = "0.4.2"
VERSION = "0.4.3"
end
18 changes: 16 additions & 2 deletions spec/eyaml/encrypted_manager_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@
"_public_key" => public_key,
"secret" => "EJ[1:egJgZHLIZfR836f9cOM7g49aPELl7ZgKRz7oDNGLa3s=:1NucdUwyqVGtv7Vj7fH7hfWzg70wUbKn:N5adZhS8xuySyQ2MvY7f027p0VqO3Qeb]",
"s3cr3t" => "p4ssw0rd",
"_secret" => "EJ[1:egJgZHLIZfR836f9cOM7g49aPELl7ZgKRz7oDNGLa3s=:1NucdUwyqVGtv7Vj7fH7hfWzg70wUbKn:N5adZhS8xuySyQ2MvY7f027p0VqO3Qeb]"
"_secret" => "EJ[1:egJgZHLIZfR836f9cOM7g49aPELl7ZgKRz7oDNGLa3s=:1NucdUwyqVGtv7Vj7fH7hfWzg70wUbKn:N5adZhS8xuySyQ2MvY7f027p0VqO3Qeb]",
"deep_nested" => {
"_underscored_secret" => "highly secret"
}
}
}

Expand All @@ -48,6 +51,10 @@
expect(subject.decrypt).to include("_secret" => data["_secret"])
end

it "doesn't skip nested secrets" do
expect(subject.decrypt.dig("deep_nested", "_underscored_secret")).to eq("highly secret")
end

it "accepts a private key with trailing newline" do
manager = EYAML::EncryptionManager.new(data, public_key, "#{private_key}\n")
expect { manager.decrypt }.not_to raise_error
Expand Down Expand Up @@ -75,7 +82,10 @@
"s3cr3t" => "p4ssw0rd",
"_skip_me" => "not_secret",
"_dont_skip_me" => {
"another_secret" => "ssshhh"
"another_secret" => "ssshhh",
"dont_skip_me_too" => {
"_deep_secret" => "ssh[FILTERED]hh"
}
}
}
}
Expand All @@ -96,6 +106,10 @@
expect(subject.encrypt.dig("_dont_skip_me", "another_secret")).to match(/\AEJ\[[\w:\/+=]+\]\z/)
end

it "doesn't skip deep nested underscored secrets" do
expect(subject.encrypt.dig("_dont_skip_me", "dont_skip_me_too", "_deep_secret")).to eq("ssh[FILTERED]hh")
end

it "encrypts values with the EJSON v1 format" do
expect(subject.encrypt["s3cr3t"]).to match(/\AEJ\[1:/)
end
Expand Down
12 changes: 12 additions & 0 deletions spec/eyaml/railtie_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,18 +74,21 @@
remove_auth_files_that_dont_end_with(".eyaml")
run_load_hooks
expect(credentials).to(include(_extension: "eyaml"))
expect(credentials).to(include(extension: "eyaml"))
end

it "eyml" do
remove_auth_files_that_dont_end_with(".eyml")
run_load_hooks
expect(credentials).to(include(_extension: "eyml"))
expect(credentials).to(include(extension: "eyml"))
end

it "ejson" do
remove_auth_files_that_dont_end_with(".ejson")
run_load_hooks
expect(credentials).to(include(_extension: "ejson"))
expect(credentials).to(include(extension: "ejson"))
end
end

Expand All @@ -99,19 +102,22 @@
remove_auth_files_that_dont_end_with(".eyaml")
run_load_hooks
expect(credentials).to(include(_extension: "eyaml"))
expect(credentials).to(include(extension: "eyaml"))
end

it "eyml" do
remove_auth_files_that_dont_end_with(".eyml")

run_load_hooks
expect(credentials).to(include(_extension: "eyml"))
expect(credentials).to(include(extension: "eyml"))
end

it "ejson" do
remove_auth_files_that_dont_end_with(".ejson")
run_load_hooks
expect(credentials).to(include(_extension: "ejson"))
expect(credentials).to(include(extension: "ejson"))
end
end

Expand Down Expand Up @@ -185,18 +191,21 @@
remove_auth_files_that_dont_end_with(".eyaml")
run_load_hooks
expect(secrets).to(include(_extension: "eyaml"))
expect(secrets).to(include(extension: "eyaml"))
end

it "eyml" do
remove_auth_files_that_dont_end_with(".eyml")
run_load_hooks
expect(secrets).to(include(_extension: "eyml"))
expect(secrets).to(include(extension: "eyml"))
end

it "ejson" do
remove_auth_files_that_dont_end_with(".ejson")
run_load_hooks
expect(secrets).to(include(_extension: "ejson"))
expect(secrets).to(include(extension: "ejson"))
end
end

Expand All @@ -210,19 +219,22 @@
remove_auth_files_that_dont_end_with(".eyaml")
run_load_hooks
expect(secrets).to(include(_extension: "eyaml"))
expect(secrets).to(include(extension: "eyaml"))
end

it "eyml" do
remove_auth_files_that_dont_end_with(".eyml")

run_load_hooks
expect(secrets).to(include(_extension: "eyml"))
expect(secrets).to(include(extension: "eyml"))
end

it "ejson" do
remove_auth_files_that_dont_end_with(".ejson")
run_load_hooks
expect(secrets).to(include(_extension: "ejson"))
expect(secrets).to(include(extension: "ejson"))
end
end

Expand Down
16 changes: 15 additions & 1 deletion spec/eyaml/util_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,21 @@
describe ".pretty_yaml" do
it "will return a hash as YAML without the three dash prefix" do
yaml_without_prefix = File.read(fixtures_root.join("pretty.yml"))
expect(EYAML::Util.pretty_yaml({"a" => "1", "b" => "2"})).to eq(yaml_without_prefix)
expect(EYAML::Util.pretty_yaml({"a"=>"1", "b"=>"2", "_c"=>{"_d"=>"3"}})).to eq(yaml_without_prefix)
end
end

describe ".with_deep_deundescored_keys" do
it "will return a hash with all undescored entries duplicated" do
yaml_without_prefix = YAML.load_file(fixtures_root.join("pretty.yml"))

expect(EYAML::Util.with_deep_deundescored_keys(yaml_without_prefix)).to eq({"a"=>"1", "b"=>"2", "c"=>{"d"=>"3", "_d"=>"3"}, "_c"=>{"d"=>"3", "_d"=>"3"}})
end

it "will raise when a de-underscored key already exists" do
yaml_without_prefix = YAML.load_file(fixtures_root.join("pretty.yml")).merge("_b" => "X")

expect { EYAML::Util.with_deep_deundescored_keys(yaml_without_prefix) }.to raise_error(KeyError)
end
end
end
3 changes: 2 additions & 1 deletion spec/fixtures/data.ejson
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"_skip_me": "not_secret",
"_extension": "ejson",
"_dont_skip_me": {
"another_secret": "EJ[1:vr6e0PrO6xzH5N9c6Rs8ERt+DJXZeS0rZPDIZxMJWDg=:B1Iyfp3NBN/Kox9kQXWLV7F8BkNCckTA:MJh0KNGPimGadsUbQUZY21/nZFlFtw==]"
"another_secret": "EJ[1:vr6e0PrO6xzH5N9c6Rs8ERt+DJXZeS0rZPDIZxMJWDg=:B1Iyfp3NBN/Kox9kQXWLV7F8BkNCckTA:MJh0KNGPimGadsUbQUZY21/nZFlFtw==]",
"_underscored_secret": "not encrypted"
}
}
1 change: 1 addition & 0 deletions spec/fixtures/data.eyaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ _skip_me: "not_secret"
_extension: "eyaml"
_dont_skip_me:
another_secret: "ssshhh"
_underscored_secret: "not encrypted"
1 change: 1 addition & 0 deletions spec/fixtures/data.eyml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ _skip_me: "not_secret"
_extension: "eyml"
_dont_skip_me:
another_secret: "ssshhh"
_underscored_secret: "not encrypted"
2 changes: 2 additions & 0 deletions spec/fixtures/pretty.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
a: '1'
b: '2'
_c:
_d: '3'
3 changes: 2 additions & 1 deletion spec/support/encryption_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ module EncryptionHelper
"_skip_me" => "not_secret",
"_extension" => "ejson", # This is only the correct value for data.ejson
"_dont_skip_me" => {
"another_secret" => "ssshhh"
"another_secret" => "ssshhh",
"_underscored_secret" => "not encrypted"
}
}
}
Expand Down

0 comments on commit 6698013

Please sign in to comment.