secret_store 0.0.1
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.
- data/.gitignore +6 -0
- data/Gemfile +4 -0
- data/README.md +76 -0
- data/Rakefile +6 -0
- data/lib/secret_store/version.rb +3 -0
- data/lib/secret_store.rb +80 -0
- data/secret_store.gemspec +24 -0
- data/spec/secret_store_spec.rb +100 -0
- data/spec/spec_helper.rb +8 -0
- metadata +93 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
SecretStore
|
2
|
+
===========
|
3
|
+
|
4
|
+
Store secrets for your app in a encrypted in a yaml file.
|
5
|
+
|
6
|
+
Often when working on a web application, you have the need for storing a
|
7
|
+
variety of API keys or secrets to 3rd party services. However, it's not
|
8
|
+
desireable to check those secrets in as plain text to the code respository.
|
9
|
+
|
10
|
+
One common way around this is to set such keys in the environment. This is,
|
11
|
+
for example, what [Heroku recommends](https://devcenter.heroku.com/articles/config-vars)
|
12
|
+
for use on their platform. While it works, it can end up cumbersome, is not easily
|
13
|
+
replicatable, and can require additional restarts in addition to a code
|
14
|
+
deploy depending on the architecture.
|
15
|
+
|
16
|
+
Instead, SecretStore encrypts all secrets with one master password, so there's only
|
17
|
+
one key to inject into the environment.
|
18
|
+
|
19
|
+
Installation
|
20
|
+
------------
|
21
|
+
|
22
|
+
install it via rubygems:
|
23
|
+
|
24
|
+
```
|
25
|
+
gem install secret_store
|
26
|
+
```
|
27
|
+
|
28
|
+
or put it in your Gemfile:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
# Gemfile
|
32
|
+
gem 'secret_store'
|
33
|
+
```
|
34
|
+
|
35
|
+
Usage
|
36
|
+
-----
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
require "secret_store"
|
40
|
+
|
41
|
+
secret_store = SecretStore.new("master_password", "/path/to/data.yml")
|
42
|
+
secret_store.store("some_api_key", "c7dd199")
|
43
|
+
secret_store.get("some_api_key") # => "c7dd199"
|
44
|
+
secret_store.get("unknown_key") # => raises IndexError
|
45
|
+
|
46
|
+
secret_store.store("known_key", "b123fa") => stores
|
47
|
+
secret_store.store("known_key", "new_val") => raises error
|
48
|
+
secret_store.store("known_key", "new_val", :force) => overwrites stored
|
49
|
+
```
|
50
|
+
|
51
|
+
For a typical application, it could be desirable to define a
|
52
|
+
single SecretStore instance; for example, in a rails initializer.
|
53
|
+
|
54
|
+
How
|
55
|
+
---
|
56
|
+
|
57
|
+
SecretStore uses [Gibberish](https://github.com/mdp/gibberish/) under the
|
58
|
+
hood to AES encrypt secrets in a YAML file. Gibbersh currently has only
|
59
|
+
stdlib and core ruby dependencies, so it makes this code easier to read
|
60
|
+
without any extra requirements.
|
61
|
+
|
62
|
+
Caveats
|
63
|
+
-------
|
64
|
+
|
65
|
+
SecretStore code is not recommended for high security (ex: PCI compliance required)
|
66
|
+
use. For your Todos webapp, it's fine!
|
67
|
+
|
68
|
+
Credits
|
69
|
+
-------
|
70
|
+
|
71
|
+
This was inspired by the [passw3rd](https://github.com/oreoshake/passw3rd) gem.
|
72
|
+
|
73
|
+
#### Copyright
|
74
|
+
|
75
|
+
Copyright (c) (2012) Brendon Murphy. See license.txt for details.
|
76
|
+
|
data/Rakefile
ADDED
data/lib/secret_store.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require "gibberish"
|
2
|
+
require "yaml"
|
3
|
+
require "secret_store/version"
|
4
|
+
|
5
|
+
class SecretStore
|
6
|
+
attr_reader :file_path
|
7
|
+
|
8
|
+
def initialize(password, file_path)
|
9
|
+
self.password = password
|
10
|
+
self.file_path = file_path
|
11
|
+
end
|
12
|
+
|
13
|
+
def store(key, secret, force = false)
|
14
|
+
load_data
|
15
|
+
|
16
|
+
if ! force && @data[key.to_s]
|
17
|
+
raise "Key #{key} already stored"
|
18
|
+
end
|
19
|
+
|
20
|
+
@data.merge!(key.to_s => encrypt(secret))
|
21
|
+
store_data
|
22
|
+
load_data[key]
|
23
|
+
end
|
24
|
+
|
25
|
+
def get(key)
|
26
|
+
ciphertext = @data.fetch(key.to_s)
|
27
|
+
cipher.decrypt(ciphertext)
|
28
|
+
end
|
29
|
+
|
30
|
+
def encrypt(secret)
|
31
|
+
cipher.encrypt(secret).chomp
|
32
|
+
end
|
33
|
+
|
34
|
+
def change_password(new_password)
|
35
|
+
new_data = {}
|
36
|
+
|
37
|
+
@data.each_key do |key|
|
38
|
+
new_data[key] = get(key)
|
39
|
+
end
|
40
|
+
|
41
|
+
self.password = new_password
|
42
|
+
|
43
|
+
new_data.each do |key, plaintext|
|
44
|
+
@data[key] = encrypt(plaintext)
|
45
|
+
end
|
46
|
+
|
47
|
+
store_data
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def file_path=(file_path)
|
53
|
+
@file_path = file_path
|
54
|
+
load_data
|
55
|
+
@file_path
|
56
|
+
end
|
57
|
+
|
58
|
+
def password=(password)
|
59
|
+
@cipher = nil
|
60
|
+
@password = password
|
61
|
+
end
|
62
|
+
|
63
|
+
def cipher
|
64
|
+
@cipher ||= Gibberish::AES.new(@password)
|
65
|
+
end
|
66
|
+
|
67
|
+
def load_data
|
68
|
+
begin
|
69
|
+
@data = YAML.load_file(file_path) || {}
|
70
|
+
rescue Errno::ENOENT
|
71
|
+
@data = {}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def store_data
|
76
|
+
File.open(@file_path, File::RDWR|File::CREAT|File::LOCK_EX, 0640) do |f|
|
77
|
+
f.puts YAML.dump @data
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "secret_store/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "secret_store"
|
7
|
+
s.version = SecretStore::VERSION
|
8
|
+
s.authors = ["Brendon Murphy"]
|
9
|
+
s.email = ["xternal1+github@gmail.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Store secrets for your app in a encrypted in a yaml file.}
|
12
|
+
s.description = s.summary
|
13
|
+
|
14
|
+
s.rubyforge_project = "secret_store"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {spec}/*`.split("\n")
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
s.add_dependency "gibberish"
|
21
|
+
|
22
|
+
s.add_development_dependency "rake"
|
23
|
+
s.add_development_dependency "rspec"
|
24
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SecretStore, "initializing" do
|
4
|
+
let(:tmpfile) { Tempfile.new("secret_store") }
|
5
|
+
|
6
|
+
it "takes a password and file path" do
|
7
|
+
subject = SecretStore.new("pass", tmpfile.path)
|
8
|
+
subject.file_path.should == tmpfile.path
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe SecretStore, "storing a secret" do
|
13
|
+
let(:tmpfile) { Tempfile.new("secret_store") }
|
14
|
+
subject { SecretStore.new("the_pass", tmpfile.path) }
|
15
|
+
|
16
|
+
context "when the key is not already stored" do
|
17
|
+
it "stores the value for the key in a yaml data file" do
|
18
|
+
subject.store("foobar", "fizzbuzz")
|
19
|
+
data = YAML.load_file(tmpfile.path)
|
20
|
+
data["foobar"].should_not be_empty
|
21
|
+
end
|
22
|
+
|
23
|
+
it "stores in an encrypted fashion" do
|
24
|
+
subject.store("foobar", "fizzbuzz")
|
25
|
+
data = YAML.load_file(tmpfile.path)
|
26
|
+
data["foobar"].should_not == "fizzbuzz"
|
27
|
+
end
|
28
|
+
|
29
|
+
it "leaves other data in the store file intact" do
|
30
|
+
tmpfile.puts YAML.dump("already" => "here")
|
31
|
+
tmpfile.flush
|
32
|
+
subject.store("foobar", "fizzbuzz")
|
33
|
+
data = YAML.load_file(tmpfile.path)
|
34
|
+
data["already"].should == "here"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context "when the key is already stored" do
|
39
|
+
it "raises if not forced" do
|
40
|
+
subject.store("foobar", "fizzbuzz")
|
41
|
+
lambda {
|
42
|
+
subject.store("foobar", "fizzbuzz")
|
43
|
+
}.should raise_error
|
44
|
+
end
|
45
|
+
|
46
|
+
it "stores if forced" do
|
47
|
+
subject.store("foobar", "fizzbuzz")
|
48
|
+
lambda {
|
49
|
+
subject.store("foobar", "fizzbuzz", :force)
|
50
|
+
}.should_not raise_error
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe SecretStore, "getting a secret" do
|
56
|
+
let(:tmpfile) { Tempfile.new("secret_store") }
|
57
|
+
subject { SecretStore.new("the_pass", tmpfile.path) }
|
58
|
+
|
59
|
+
it "returns the decrypted secret" do
|
60
|
+
subject.store("foobar", "fizzbuzz")
|
61
|
+
subject.get("foobar").should == "fizzbuzz"
|
62
|
+
end
|
63
|
+
|
64
|
+
it "raises IndexError if the key is not found" do
|
65
|
+
lambda{
|
66
|
+
subject.get("not_found")
|
67
|
+
}.should raise_error(IndexError)
|
68
|
+
end
|
69
|
+
|
70
|
+
it "raises OpenSSL::Cipher::CipherError if the password for the store is wrong" do
|
71
|
+
subject.store("foobar", "fizzbuzz")
|
72
|
+
with_wrong_pass = SecretStore.new("wrong_pass", tmpfile.path)
|
73
|
+
lambda {
|
74
|
+
with_wrong_pass.get("foobar")
|
75
|
+
}.should raise_error(OpenSSL::Cipher::CipherError)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe SecretStore, "changing the password" do
|
80
|
+
let(:tmpfile) { Tempfile.new("secret_store") }
|
81
|
+
subject { SecretStore.new("the_pass", tmpfile.path) }
|
82
|
+
|
83
|
+
it "resets the value for each secret key" do
|
84
|
+
tmpfile.puts YAML.dump("foo" => "bar", "fizz" => "buzz")
|
85
|
+
subject.change_password("new_password")
|
86
|
+
data = YAML.load_file(tmpfile.path)
|
87
|
+
data["foo"].should_not == "bar"
|
88
|
+
data["fizz"].should_not == "buzz"
|
89
|
+
end
|
90
|
+
|
91
|
+
it "leaves you with the ability to get secrets using the new password" do
|
92
|
+
subject.store("foo", "bar")
|
93
|
+
subject.change_password("new_pass")
|
94
|
+
|
95
|
+
subject.get("foo").should == "bar"
|
96
|
+
|
97
|
+
with_new_pass = SecretStore.new("new_pass", tmpfile.path)
|
98
|
+
subject.get("foo").should == "bar"
|
99
|
+
end
|
100
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: secret_store
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Brendon Murphy
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-08-11 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: gibberish
|
16
|
+
requirement: &2160632160 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2160632160
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rake
|
27
|
+
requirement: &2160631720 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *2160631720
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rspec
|
38
|
+
requirement: &2160631280 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *2160631280
|
47
|
+
description: Store secrets for your app in a encrypted in a yaml file.
|
48
|
+
email:
|
49
|
+
- xternal1+github@gmail.com
|
50
|
+
executables: []
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- .gitignore
|
55
|
+
- Gemfile
|
56
|
+
- README.md
|
57
|
+
- Rakefile
|
58
|
+
- lib/secret_store.rb
|
59
|
+
- lib/secret_store/version.rb
|
60
|
+
- secret_store.gemspec
|
61
|
+
- spec/secret_store_spec.rb
|
62
|
+
- spec/spec_helper.rb
|
63
|
+
homepage: ''
|
64
|
+
licenses: []
|
65
|
+
post_install_message:
|
66
|
+
rdoc_options: []
|
67
|
+
require_paths:
|
68
|
+
- lib
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ! '>='
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
segments:
|
76
|
+
- 0
|
77
|
+
hash: -1229492496997866402
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
|
+
none: false
|
80
|
+
requirements:
|
81
|
+
- - ! '>='
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
segments:
|
85
|
+
- 0
|
86
|
+
hash: -1229492496997866402
|
87
|
+
requirements: []
|
88
|
+
rubyforge_project: secret_store
|
89
|
+
rubygems_version: 1.8.15
|
90
|
+
signing_key:
|
91
|
+
specification_version: 3
|
92
|
+
summary: Store secrets for your app in a encrypted in a yaml file.
|
93
|
+
test_files: []
|