secret 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a56b7cd7533a64c852d89901ba1460a2b487d498
4
+ data.tar.gz: 2993017272c967c3b9e7249f6be3eff1a9bc3e22
5
+ SHA512:
6
+ metadata.gz: f263a042a3fb1373b73a7548143b36fc1eb6147cbdbd5ce58e7f6aa34c22bfa40cb014d6934186b220ee08c305976b3b87df8ffb1721091c120243b363297fdd
7
+ data.tar.gz: a80eb4b104b6fde26085b253e9d6065aa145d9a6cf65d8a54041e98310f4dd69e7b29abd4a88b3f1b2fd9f0faab5ee5f7bc126d2d23b2a0cc9b850a3778e3b76
data/.gitignore CHANGED
@@ -1,20 +1,23 @@
1
- *.gem
2
- *.rbc
3
- .bundle
4
- .config
5
- .yardoc
6
- Gemfile.lock
7
- InstalledFiles
8
- _yardoc
9
- coverage
10
- doc/
11
- lib/bundler/man
12
- pkg
13
- rdoc
14
- spec/reports
15
- test/tmp
16
- test/version_tmp
17
- tmp
18
-
19
-
20
- .idea
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+
19
+
20
+ .idea
21
+ *.komodoproject
22
+ .komodotools
23
+ test/secrets/*
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
- source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in secret.gemspec
4
- gemspec
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in secret.gemspec
4
+ gemspec
data/LICENSE.txt CHANGED
@@ -1,22 +1,22 @@
1
- Copyright (c) 2013 Christopher Thornton
2
-
3
- MIT License
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining
6
- a copy of this software and associated documentation files (the
7
- "Software"), to deal in the Software without restriction, including
8
- without limitation the rights to use, copy, modify, merge, publish,
9
- distribute, sublicense, and/or sell copies of the Software, and to
10
- permit persons to whom the Software is furnished to do so, subject to
11
- the following conditions:
12
-
13
- The above copyright notice and this permission notice shall be
14
- included in all copies or substantial portions of the Software.
15
-
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1
+ Copyright (c) 2013 Christopher Thornton
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,52 +1,60 @@
1
- Secret Files
2
- ============
3
- Keeps your files more secure by ensuring saved files are chmoded 0700 by the same user who is running the process.
4
- For example, this could be used by a Rails application to store certificates only readable by the www-data process.
5
-
6
- The secret files gem includes some fun over-engineering with file locking!
7
-
8
- ## Usage
9
- Add to your Gemfile:
10
-
11
- ```ruby
12
- gem 'secret'
13
- ```
14
-
15
- Now you should set up your "default" container in an initializer:
16
-
17
- ```ruby
18
- Secret.configure_default 'path/to/secret/dir'
19
- ```
20
-
21
- Finally, you should now be able to put stuff into the secret directory, or read contents!
22
-
23
- ```ruby
24
- secret = Secret.default
25
-
26
- # Save the key
27
- secret.some_key.stash "ThisIsSomeKey"
28
-
29
- # Get the key
30
- puts secret.some_key.contents
31
-
32
- # Manually seek through the file
33
- secret.some_key.stream do |f|
34
- f.seek 1, IO::SEEK_CUR
35
- end
36
- ```
37
-
38
- ## How Secure is It?
39
- Ths is only *somewhat* secure and will provide protection against:
40
-
41
- * Someone who gains access to your server with non-root access
42
- * Other non-root server processes
43
-
44
- However, this will **not** protect you against:
45
-
46
- * People with root access
47
- * Arbitrary code execution attacks by the owner (i.e. an `eval()` gone wrong)
48
-
49
- ## Other Features
50
- This gem also includes locking support, meaning that it should (hopefully) be resillient against multiple processes
51
- writing to a file. The primary reason for this is because I really wanted to try file locking. However, don't do
52
- anything too tricky and you should be okay.
1
+ Secret Files
2
+ ============
3
+ Keeps your files more secure by ensuring saved files are chmoded 0700 by the same user who is running the process.
4
+ For example, this could be used by a Rails application to store certificates only readable by the www-data process.
5
+
6
+ The secret files gem includes some fun over-engineering with file locking!
7
+
8
+ ## Usage
9
+ Add to your Gemfile:
10
+
11
+ ```ruby
12
+ gem 'secret'
13
+ ```
14
+
15
+ Now you should set up your "default" container in an initializer:
16
+
17
+ ```ruby
18
+ Secret.configure_default 'path/to/secret/dir'
19
+ ```
20
+
21
+ Finally, you should now be able to put stuff into the secret directory, or read contents!
22
+
23
+ ```ruby
24
+ secret = Secret.default
25
+
26
+ # Save the key
27
+ secret.some_key.stash "ThisIsSomeKey"
28
+
29
+ # Get the key
30
+ puts secret.some_key.contents
31
+
32
+ # Manually seek through the file
33
+ secret.some_key.stream do |f|
34
+ f.seek 1, IO::SEEK_CUR
35
+ end
36
+
37
+ # Support for nested directories
38
+ file = secret.file "certs/file.crt"
39
+ file.stash "Contents of CA File"
40
+ puts file.contents
41
+
42
+ # Also support for shorthand syntax
43
+ secret.stash "certs/file.key", "Contents of this key file!"
44
+ puts secret.contents "certs/file.key"
45
+ ```
46
+
47
+ ## How Secure is It?
48
+ Ths is only *somewhat* secure and will provide protection against:
49
+
50
+ * Someone who gains access to your server with non-root access
51
+ * Other non-root server processes
52
+
53
+ However, this will **not** protect you against:
54
+
55
+ * People with root access
56
+ * Arbitrary code execution attacks by the owner (i.e. an `eval()` gone wrong)
57
+
58
+ ## Other Features
59
+ This gem also includes locking support, meaning that it will be resillient against multiple processes
60
+ writing to a file. This will **not** lock multiple threads from the same process.
data/Rakefile CHANGED
@@ -1 +1 @@
1
- require "bundler/gem_tasks"
1
+ require "bundler/gem_tasks"
@@ -1,64 +1,83 @@
1
- module Secret
2
- class Container
3
-
4
- attr_reader :directory, :files
5
-
6
- # Initializes a container. Does significant checking on the directory to ensure it is writeable and it exists.
7
- # @param [String] directory the directory to the container
8
- # @param [Boolean] auto_create if true, will attempt to create the directory if it does not exist.
9
- def initialize(directory, auto_create = true)
10
- @directory = directory
11
-
12
- @files = {}
13
-
14
- # Do some checking about our directory
15
- if ::File.exist?(directory)
16
- raise ArgumentError, "Specified directory '#{directory}' is actually a file!" unless ::File.directory?(directory)
17
-
18
- # Now make our directory if auto_create
19
- else
20
- raise ArgumentError, "Specified directory '#{directory}' does not exist!" unless auto_create
21
- Dir.mkdir(directory, Secret::CHMOD_MODE) # Only give read/write access to this user
22
- end
23
- raise ArgumentError, "Directory '#{directory}' is not writeable!" unless ::File.writable?(directory)
24
- end
25
-
26
- # Gets a file stored in the container.
27
- # @param [Symbol] filename the name of the file.
28
- # @return [Secret::File] a secret file
29
- def file(filename)
30
- fn = filename.to_sym
31
- f = files[fn]
32
- return f unless f.nil?
33
- f = Secret::File.new(self, filename)
34
- files[fn] = f
35
- return f
36
- end
37
-
38
- def method_missing(meth, *args, &block)
39
- super(meth, *args, &block) if args.any? or block_given?
40
- return file(meth)
41
- end
42
-
43
-
44
- # Deletes the cache of objects
45
- def uncache!
46
- @files = {}
47
- end
48
-
49
- # This should be called once in some sort of initializer.
50
- def initialize_once!
51
- destroy_all_locks!
52
- end
53
-
54
- # Viciously destroys all locks that the file and its containers may have. Use carefully!
55
- # @return [Integer] the number of files destroyed.
56
- def destroy_all_locks!
57
- files = Dir[::File.join(directory, '*.lock')]
58
- files.each{|f| ::File.delete(f) }
59
- return files.count
60
- end
61
-
62
-
63
- end
1
+ module Secret
2
+ class Container
3
+
4
+ attr_reader :directory, :files, :chmod_mode
5
+
6
+ # Initializes a container. Does significant checking on the directory to ensure it is writeable and it exists.
7
+ # @param [String] directory the directory to the container
8
+ # @param [Boolean] auto_create if true, will attempt to create the directory if it does not exist.
9
+ def initialize(directory, auto_create = true, chmod = Secret::CHMOD_MODE)
10
+ @directory = directory
11
+ @chmod_mode = chmod
12
+
13
+ @files = {}
14
+
15
+ # Do some checking about our directory
16
+ if ::File.exist?(directory)
17
+ raise ArgumentError, "Specified directory '#{directory}' is actually a file!" unless ::File.directory?(directory)
18
+
19
+ # Now make our directory if auto_create
20
+ else
21
+ raise ArgumentError, "Specified directory '#{directory}' does not exist!" unless auto_create
22
+ FileUtils.mkdir_p(directory, :mode => chmod_mode) # Only give read/write access to this user
23
+ end
24
+ raise ArgumentError, "Directory '#{directory}' is not writeable!" unless ::File.writable?(directory)
25
+ end
26
+
27
+ # Stashes the contents of a file
28
+ def stash(path, contents)
29
+ file(path).stash(contents)
30
+ end
31
+
32
+ def contents(path)
33
+ file(path).contents
34
+ end
35
+
36
+ # Gets a file stored in the container.
37
+ # @param [Symbol] filename the name of the file.
38
+ # @return [Secret::File] a secret file
39
+ def file(filename)
40
+ fn = filename.to_s
41
+ f = files[fn]
42
+ return f unless f.nil?
43
+
44
+ d = ::File.dirname(fn)
45
+ container = d == "." ? self : dir(d)
46
+
47
+ f = Secret::File.new(container, ::File.basename(filename) + Secret::FILE_EXT)
48
+ files[fn] = f
49
+ return f
50
+ end
51
+
52
+ # Another container within the directory
53
+ def dir(name)
54
+ Container.new ::File.join(directory, name), true, chmod_mode
55
+ end
56
+
57
+ def method_missing(meth, *args, &block)
58
+ super(meth, *args, &block) if args.any? or block_given?
59
+ return file(meth)
60
+ end
61
+
62
+
63
+ # Deletes the cache of objects
64
+ def uncache!
65
+ @files = {}
66
+ end
67
+
68
+ # This should be called once in some sort of initializer.
69
+ def initialize_once!
70
+ destroy_all_locks!
71
+ end
72
+
73
+ # Viciously destroys all locks that the file and its containers may have. Use carefully!
74
+ # @return [Integer] the number of files destroyed.
75
+ def destroy_all_locks!
76
+ files = Dir[::File.join(directory, '*.lock')]
77
+ files.each{|f| ::File.delete(f) }
78
+ return files.count
79
+ end
80
+
81
+
82
+ end
64
83
  end
@@ -1,45 +1,45 @@
1
- require 'openssl'
2
-
3
- module Secret
4
- module Encryption
5
-
6
-
7
-
8
- def encrypt_basic(passphrase)
9
- cipher = OpenSSL::Cipher.new('aes-256-cbc')
10
- cipher.encrypt
11
- key = passphrase
12
- iv = cipher.random_iv
13
-
14
- out = StringIO.new("", "wb") do |outf|
15
- StringIO.new(contents, "rb") do |inf|
16
- while inf.read(4096, buf)
17
- outf << cipher.update(buf)
18
- end
19
- outf << cipher.final
20
- end
21
- end
22
-
23
- return out.string
24
- end
25
-
26
- # Checks to see if the file is encrypted
27
- def encrypted?
28
- ::File.exist?(encrypted_meta_filename)
29
- end
30
-
31
-
32
- def stash(content); raise "Not Implemented"; end
33
-
34
- def contents; raise "Not Implemented"; end
35
-
36
-
37
-
38
- protected
39
-
40
- def encrypted_meta_filename
41
- raise NotImplementedError, "Must implement dis!"
42
- end
43
-
44
- end
1
+ require 'openssl'
2
+
3
+ module Secret
4
+ module Encryption
5
+
6
+
7
+
8
+ def encrypt_basic(passphrase)
9
+ cipher = OpenSSL::Cipher.new('aes-256-cbc')
10
+ cipher.encrypt
11
+ key = passphrase
12
+ iv = cipher.random_iv
13
+
14
+ out = StringIO.new("", "wb") do |outf|
15
+ StringIO.new(contents, "rb") do |inf|
16
+ while inf.read(4096, buf)
17
+ outf << cipher.update(buf)
18
+ end
19
+ outf << cipher.final
20
+ end
21
+ end
22
+
23
+ return out.string
24
+ end
25
+
26
+ # Checks to see if the file is encrypted
27
+ def encrypted?
28
+ ::File.exist?(encrypted_meta_filename)
29
+ end
30
+
31
+
32
+ def stash(content); raise "Not Implemented"; end
33
+
34
+ def contents; raise "Not Implemented"; end
35
+
36
+
37
+
38
+ protected
39
+
40
+ def encrypted_meta_filename
41
+ raise NotImplementedError, "Must implement dis!"
42
+ end
43
+
44
+ end
45
45
  end
data/lib/secret/file.rb CHANGED
@@ -1,187 +1,179 @@
1
- module Secret
2
-
3
- # Handles file operations.
4
- #
5
- # Note that locking operations are not perfect!
6
- class File
7
- include Secret::Locking
8
-
9
- # include Secret::Encryption
10
-
11
- attr_reader :container, :identifier
12
-
13
- # Lock timeout in MS. Defaults to 5000 MS
14
- attr_accessor :lock_timeout_ms
15
-
16
- # Whether this current object holds the lock over the file.
17
- # @return [Boolean] TRUE if this object was the one that holds the lock (i.e. created the lock), FALSE if another
18
- # object holds the lock, or the file simply isn't locked.
19
- attr_reader :owns_lock
20
-
21
- # Creates a new secret file. The specified identifier doesn't already need to exist.
22
- # @param [Secret::Container] container the container object
23
- # @param [Symbol] identifier an unique identifier for this container
24
- def initialize(container, identifier)
25
- raise ArgumentError, "Container must be a Secret::Container object" unless container.is_a?(Secret::Container)
26
- @container = container; @identifier = identifier; @owns_lock = false
27
- @lock_timeout_ms = Secret::Locking::DEFAULT_LOCK_WAIT_MS
28
- end
29
-
30
- # Checks whether this file actually exists or not
31
- # @return [Boolean] true if the file exists (i.e. has content), false if otherwise.
32
- def exist?
33
- ::File.exist?(file_path)
34
- end
35
-
36
- # Gets a file stream of this file. If the file doesn't exist, then a blank file will be created. By default,
37
- # this allows you to write to the file. However, please use the {#stash} command, as it accounts for mid-write
38
- # crashes. Don't forget to close the file stream when you're done!
39
- # @param [String] mode the mode for this file. Currently defaults to 'r+', which is read-write, with the
40
- # file pointer at the beginning of the file.
41
- # @return [IO] an IO stream to this file, if not using a block
42
- # @note This completely bypasses any internal locking mechanisms if not using a block!
43
- # @example
44
- # file = container.some_file
45
- #
46
- # # Unsafe way!
47
- # io = file.stream
48
- # io.write "Hello World!"
49
- # io.close
50
- #
51
- # # Safe way, with locking support
52
- # file.stream do |f|
53
- # f.write "Hello World!"
54
- # end
55
- def stream(mode = 'r+', &block)
56
- touch!
57
- return ::File.open(file_path, mode, Secret::CHMOD_MODE) unless block_given?
58
- wait_for_unlock(true, lock_timeout_ms) do
59
- io = ::File.open(file_path, mode, Secret::CHMOD_MODE)
60
- block.call(io)
61
- io.close unless io.closed?
62
- end
63
- end
64
-
65
-
66
- # Gets the contents of the file in a string format. Will return an empty string if the file doesn't exist, or the
67
- # file just so happens to be empty.
68
- # @return [String] the contents of the file
69
- def contents
70
- wait_for_unlock(false, lock_timeout_ms) do
71
- io = stream
72
- str = io.read
73
- io.close
74
- end
75
- return str
76
- end
77
-
78
-
79
- # Creates a new file if it doesn't exist. Doesn't actually change the last updated timestamp.
80
- # @return [Boolean] true if an empty file was created, false if the file already existed.
81
- def touch!
82
- unless exist?
83
- ::File.open(file_path, 'w') {}
84
- secure!
85
- return true
86
- end
87
- return false
88
- end
89
-
90
- # Secures the file by chmoding it to 0700
91
- # @raise [IOError] if the file doesn't exist on the server.
92
- def secure!
93
- raise IOError, "File doesn't exist" unless exist?
94
- ::File.chmod(Secret::CHMOD_MODE, file_path)
95
- end
96
-
97
-
98
- # Stashes some content into the file! This will write a temporary backup file before stashing, in order to prevent
99
- # any partial writes if the server crashes. Once this finishes executing, you can be sure that contents have been
100
- # written.
101
- # @param [String] content the contents to stash. **Must be a string!**
102
- # @raise [ArgumentError] if content is anything other than a String object!
103
- def stash(content)
104
- raise ArgumentError, "Content must be a String (was type of type #{content.class.name})" unless content.is_a?(String)
105
-
106
- # Get an exclusive lock
107
- wait_for_unlock(true, lock_timeout_ms) do
108
- touch!
109
-
110
- # Think of this as a beginning of a transaction.
111
-
112
- # Open a temporary file for writing, and close it immediately
113
- ::File.open(tmp_file_path, "w", Secret::CHMOD_MODE){|f| f.write content }
114
-
115
- # Rename the existing file path
116
- ::File.rename(file_path, backup_file_path)
117
-
118
- # Now rename the temporary file to the correct file
119
- ::File.rename(tmp_file_path, file_path)
120
-
121
- # Delete the backup
122
- ::File.delete(backup_file_path)
123
-
124
- # Committed! Secure it just in case
125
- secure!
126
- end
127
- end
128
-
129
- # Attempts to restore a backup (i.e. if the computer crashed while doing a stash command)
130
- # @return [Boolean] true if the backup was successfully restored, false otherwise
131
- def restore_backup!
132
- return false if locked?
133
- return false unless ::File.exist?(backup_file_path)
134
-
135
- # Ideally we want to get an exclusive lock when doing this!
136
- wait_for_unlock(true, lock_timeout_ms) do
137
- # If the file actually exists, then the backup file probably wasn't deleted, so just
138
- # delete the backup file.
139
- if ::File.exist?(file_path)
140
- ::File.delete(backup_file_path)
141
- return true
142
-
143
- # Otherwise, the temporary file probably wasn't renamed, so restore the backup and delete the temporary file
144
- # if it exists. It's possible the file was corrupted in the middle of writing, so it is better to resort
145
- # to an old and complete file rather than a new and possibly corrupted file.
146
- else
147
- ::File.rename(backup_file_path, file_path)
148
- ::File.delete(tmp_file_path) if ::File.exist?(tmp_file_path)
149
- return true
150
- end
151
- end
152
- end
153
-
154
- protected
155
-
156
- # Gets the path of the lock file
157
- def lock_file_path
158
- return file_path + '.lock'
159
- end
160
-
161
-
162
- private
163
-
164
- # Gets the actual file path of this container. Intentionally made private (security through obscurity)
165
- # @return [String] the absolute path to where this file is
166
- def file_path
167
- @the_file_path = ::File.join(container.directory, identifier.to_s) if @the_file_path.nil?
168
- return @the_file_path
169
- end
170
-
171
- # The path of the temporary file. Used as a temporary container for stashing. This file will then be re-named.
172
- # @return [String] the path to the temporary file
173
- def tmp_file_path
174
- return file_path + ".tmp"
175
- end
176
-
177
- # Gets the path of the backup file
178
- # @return [String] the path of the backup file
179
- def backup_file_path
180
- return file_path + '.bak'
181
- end
182
-
183
-
184
-
185
-
186
- end
1
+ module Secret
2
+
3
+ # Handles file operations. Uses Ruby's internal file locking mechanisms.
4
+ class File
5
+ # include Secret::Encryption
6
+
7
+ attr_reader :container, :identifier
8
+
9
+ # Creates a new secret file. The specified identifier doesn't already need to exist.
10
+ # @param [Secret::Container] container the container object
11
+ # @param [Symbol] identifier an unique identifier for this container
12
+ def initialize(container, identifier)
13
+ raise ArgumentError, "Container must be a Secret::Container object" unless container.is_a?(Secret::Container)
14
+ @container = container; @identifier = identifier
15
+ touch!
16
+ ensure_writeable!
17
+ end
18
+
19
+ # Checks whether this file actually exists or not
20
+ # @return [Boolean] true if the file exists (i.e. has content), false if otherwise.
21
+ def exist?
22
+ ::File.exist?(file_path)
23
+ end
24
+
25
+ # Gets a file stream of this file. If the file doesn't exist, then a blank file will be created. By default,
26
+ # this allows you to write to the file. However, please use the {#stash} command, as it accounts for mid-write
27
+ # crashes. Don't forget to close the file stream when you're done!
28
+ # @param [String] mode the mode for this file. Currently defaults to 'r+', which is read-write, with the
29
+ # file pointer at the beginning of the file.
30
+ # @return [IO] an IO stream to this file, if not using a block
31
+ # @note Uses an exclusive lock on this file
32
+ # @example
33
+ # file = container.some_file
34
+ #
35
+ # # Unsafe way!
36
+ # io = file.stream
37
+ # io.write "Hello World!"
38
+ # io.close
39
+ #
40
+ # # Safe way, with locking support
41
+ # file.stream do |f|
42
+ # f.write "Hello World!"
43
+ # end
44
+ def stream(mode = 'r', &block)
45
+ touch!
46
+ ensure_writeable!
47
+ return ::File.open(file_path, mode, container.chmod_mode) unless block_given?
48
+ ::File.open(file_path, mode, container.chmod_mode) do |f|
49
+ begin
50
+ f.flock(::File::LOCK_EX) # Lock with exclusive mode
51
+ block.call(f)
52
+ ensure
53
+ f.flock(::File::LOCK_UN)
54
+ end
55
+ end
56
+ end
57
+
58
+
59
+ # Gets the contents of the file in a string format. Will return an empty string if the file doesn't exist, or the
60
+ # file just so happens to be empty.
61
+ # @return [String] the contents of the file
62
+ def contents
63
+ str = nil
64
+ stream 'r' do |f|
65
+ str = f.read
66
+ end
67
+ return str
68
+ end
69
+
70
+
71
+ # Creates a new file if it doesn't exist. Doesn't actually change the last updated timestamp.
72
+ # @return [Boolean] true if an empty file was created, false if the file already existed.
73
+ def touch!
74
+ unless exist?
75
+ ::File.open(file_path, 'w', container.chmod_mode) {}
76
+ secure!
77
+ return true
78
+ end
79
+ return false
80
+ end
81
+
82
+ # Secures the file by chmoding it to 0700
83
+ # @raise [IOError] if the file doesn't exist on the server.
84
+ def secure!
85
+ raise IOError, "File doesn't exist" unless exist?
86
+ ::File.chmod(container.chmod_mode, file_path)
87
+ end
88
+
89
+
90
+ # Stashes some content into the file! This will write a temporary backup file before stashing, in order to prevent
91
+ # any partial writes if the server crashes. Once this finishes executing, you can be sure that contents have been
92
+ # written.
93
+ # @param [String] content the contents to stash. **Must be a string!**
94
+ # @raise [ArgumentError] if content is anything other than a String object!
95
+ def stash(content)
96
+ raise ArgumentError, "Content must be a String (was type of type #{content.class.name})" unless content.is_a?(String)
97
+ touch!
98
+ ensure_writeable!
99
+
100
+ # Think of this as a beginning of a transaction.
101
+ ::File.open(file_path, 'a', container.chmod_mode) do |f|
102
+ begin
103
+ f.flock(::File::LOCK_EX)
104
+
105
+ # Open a temporary file for writing, and close it immediately
106
+ ::File.open(tmp_file_path, "w", container.chmod_mode){|f| f.write content }
107
+
108
+ # Rename tmp file to backup file now we know contents are sane
109
+ ::File.rename(tmp_file_path, backup_file_path)
110
+
111
+ # Truncate file contents to zero bytes
112
+ f.truncate 0
113
+
114
+ # Write content
115
+ f.write content
116
+ ensure
117
+ # Now unlock file!
118
+ f.flock(::File::LOCK_UN)
119
+ end
120
+
121
+ # Delete backup file
122
+ ::File.delete(backup_file_path)
123
+ end
124
+
125
+ # Committed! Secure it just in case
126
+ secure!
127
+ end
128
+
129
+ def ensure_writeable!
130
+ unless ::File.writable?(file_path)
131
+ raise FileUnreadableError, "File is not writeable - perhaps it was created by a different process?"
132
+ end
133
+ end
134
+
135
+ # Attempts to restore a backup (i.e. if the computer crashed while doing a stash command)
136
+ # @return [Boolean] true if the backup was successfully restored, false otherwise
137
+ def restore_backup!
138
+ return false unless ::File.exist?(backup_file_path)
139
+
140
+ # We know backup exists, so let's write to the file. We want to truncate file contents.
141
+ # Now copy file contents over from the backup file. We use this method to use locking.
142
+ ::File.open(file_path, 'w', container.chmod_mode) do |f|
143
+ begin
144
+ f.flock ::File::LOCK_EX
145
+ ::File.open(backup_file_path, 'r', container.chmod_mode) do |b|
146
+ f.write b.read
147
+ end
148
+ ensure
149
+ f.flock ::File::LOCK_UN
150
+ end
151
+ end
152
+ return true
153
+
154
+ end
155
+
156
+
157
+ private
158
+
159
+ # Gets the actual file path of this container. Intentionally made private (security through obscurity)
160
+ # @return [String] the absolute path to where this file is
161
+ def file_path
162
+ @the_file_path = ::File.join(container.directory, identifier.to_s) if @the_file_path.nil?
163
+ return @the_file_path
164
+ end
165
+
166
+ # The path of the temporary file. Used as a temporary container for stashing. This file will then be re-named.
167
+ # @return [String] the path to the temporary file
168
+ def tmp_file_path
169
+ return file_path + ".tmp"
170
+ end
171
+
172
+ # Gets the path of the backup file
173
+ # @return [String] the path of the backup file
174
+ def backup_file_path
175
+ return file_path + '.bak'
176
+ end
177
+
178
+ end
187
179
  end
@@ -1,3 +1,3 @@
1
- module Secret
2
- VERSION = "0.0.1"
3
- end
1
+ module Secret
2
+ VERSION = "0.0.2"
3
+ end
data/lib/secret.rb CHANGED
@@ -1,36 +1,34 @@
1
- require "secret/version"
2
- require "secret/locking"
3
- require "secret/file"
4
- require "secret/container"
5
-
6
- module Secret
7
-
8
- # The chmod mode to use for files.
9
- CHMOD_MODE = 0700
10
-
11
- # Gets the UID of the current process
12
- def self.uid
13
- return Process.uid
14
- end
15
-
16
- # Gets the default container
17
- # @return [Secret::Container] the default container
18
- def self.default
19
- raise ArgumentError, "Must call 'Secret.configure_default' before you can access the default container" unless @default
20
- return @default
21
- end
22
-
23
- # Configures the default container once
24
- def self.configure_default(directory, auto_create = true)
25
- unless @default
26
- @default = Secret::Container.new(directory, auto_create)
27
- @default.initialize_once!
28
- return true
29
- else
30
- return false
31
- end
32
- end
33
-
34
- class FileLockedError < Exception; end
35
-
36
- end
1
+ require "secret/version"
2
+ require "secret/file"
3
+ require "secret/container"
4
+
5
+ module Secret
6
+
7
+ # The chmod mode to use for files.
8
+ CHMOD_MODE = 0700
9
+
10
+ # The file extension for secret files
11
+ FILE_EXT = ".sfile"
12
+
13
+ # Gets the default container
14
+ # @return [Secret::Container] the default container
15
+ def self.default
16
+ raise ArgumentError, "Must call 'Secret.configure_default' before you can access the default container" unless @default
17
+ return @default
18
+ end
19
+
20
+ # Configures the default container once
21
+ def self.configure_default(directory, auto_create = true)
22
+ unless @default
23
+ @default = Secret::Container.new(directory, auto_create)
24
+ @default.initialize_once!
25
+ return true
26
+ else
27
+ return false
28
+ end
29
+ end
30
+
31
+ class FileUnreadableError < Exception; end
32
+
33
+
34
+ end
data/secret.gemspec CHANGED
@@ -1,23 +1,23 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'secret/version'
5
-
6
- Gem::Specification.new do |spec|
7
- spec.name = "secret"
8
- spec.version = Secret::VERSION
9
- spec.authors = ["Christopher Thornton"]
10
- spec.email = ["rmdirbin@gmail.com"]
11
- spec.description = %q{Keeps your files more secure by ensuring saved files are chmoded 0700 by the same user who is running the process.}
12
- spec.summary = %q{Keeps files more secure on server environments}
13
- spec.homepage = "https://github.com/cgthornt/secret"
14
- spec.license = "MIT"
15
-
16
- spec.files = `git ls-files`.split($/)
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
20
-
21
- spec.add_development_dependency "bundler", "~> 1.3"
22
- spec.add_development_dependency "rake"
23
- end
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'secret/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "secret"
8
+ spec.version = Secret::VERSION
9
+ spec.authors = ["Christopher Thornton"]
10
+ spec.email = ["rmdirbin@gmail.com"]
11
+ spec.description = %q{Keeps your files more secure by ensuring saved files are chmoded 0700 by the same user who is running the process.}
12
+ spec.summary = %q{Keeps files more secure on server environments}
13
+ spec.homepage = "https://github.com/cgthornt/secret"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ end
data/test/Gemfile CHANGED
@@ -1,3 +1,6 @@
1
- source 'https://rubygems.org'
2
-
3
- gem 'secret', path: '../'
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'secret', path: '../'
4
+
5
+ #gem 'ffi'
6
+ #gem 'win32-process'
data/test/spawn.rb ADDED
@@ -0,0 +1,16 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'secret'
4
+ Secret.configure_default 'secrets'
5
+ container = Secret.default
6
+
7
+ f = "running.txt"
8
+ File.open(f, 'w'){|f| f.write "Running..." }
9
+
10
+ container.test1.stream 'w' do |f|
11
+ puts "Sleeping for 10 seconds..."
12
+ sleep(10)
13
+ f.write "New Contents"
14
+ end
15
+
16
+ File.delete(f)
data/test/tester.rb CHANGED
@@ -1,10 +1,36 @@
1
- require 'rubygems'
2
- require 'bundler/setup'
3
-
4
- require 'secret'
5
-
6
- Secret.configure_default 'secrets'
7
-
8
- container = Secret.default
9
- container.cert_key.stash "Hello World!"
10
- puts container.cert_key.encrypt_basic "password"
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ # require 'win32/process'
4
+
5
+ require 'secret'
6
+
7
+ Secret.configure_default 'secrets'
8
+ container = Secret.default
9
+
10
+ container.stash "key.crt", "The contents of this key!"
11
+
12
+ puts "Contents of key: '#{container.contents 'key.crt'}'"
13
+
14
+
15
+
16
+ exit(0)
17
+ container.something.stash "My Secret Text!"
18
+ container.file(:someting_else).stash "More secret text"
19
+ container.dir('test')
20
+
21
+ puts "Testing some multi-process action"
22
+ container.test1.stash "Original Content"
23
+
24
+ # Windows - testing file locking
25
+ p = Process.create(
26
+ :app_name => 'ruby spawn.rb',
27
+ :creation_flags => Process::DETACHED_PROCESS,
28
+ :process_inherit => false,
29
+ :thread_inherit => true,
30
+ )
31
+
32
+ sleep(1)
33
+ puts p.inspect
34
+
35
+ puts "New contents:"
36
+ puts "'#{container.test1.contents}'"
metadata CHANGED
@@ -1,20 +1,18 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: secret
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
5
- prerelease:
4
+ version: 0.0.2
6
5
  platform: ruby
7
6
  authors:
8
7
  - Christopher Thornton
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2013-05-04 00:00:00.000000000 Z
11
+ date: 2013-05-14 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: bundler
16
15
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
16
  requirements:
19
17
  - - ~>
20
18
  - !ruby/object:Gem::Version
@@ -22,7 +20,6 @@ dependencies:
22
20
  type: :development
23
21
  prerelease: false
24
22
  version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
23
  requirements:
27
24
  - - ~>
28
25
  - !ruby/object:Gem::Version
@@ -30,17 +27,15 @@ dependencies:
30
27
  - !ruby/object:Gem::Dependency
31
28
  name: rake
32
29
  requirement: !ruby/object:Gem::Requirement
33
- none: false
34
30
  requirements:
35
- - - ! '>='
31
+ - - '>='
36
32
  - !ruby/object:Gem::Version
37
33
  version: '0'
38
34
  type: :development
39
35
  prerelease: false
40
36
  version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
37
  requirements:
43
- - - ! '>='
38
+ - - '>='
44
39
  - !ruby/object:Gem::Version
45
40
  version: '0'
46
41
  description: Keeps your files more secure by ensuring saved files are chmoded 0700
@@ -60,44 +55,36 @@ files:
60
55
  - lib/secret/container.rb
61
56
  - lib/secret/encryption.rb
62
57
  - lib/secret/file.rb
63
- - lib/secret/locking.rb
64
58
  - lib/secret/version.rb
65
59
  - secret.gemspec
66
60
  - test/Gemfile
67
- - test/secrets/cert_key
61
+ - test/spawn.rb
68
62
  - test/tester.rb
69
63
  homepage: https://github.com/cgthornt/secret
70
64
  licenses:
71
65
  - MIT
66
+ metadata: {}
72
67
  post_install_message:
73
68
  rdoc_options: []
74
69
  require_paths:
75
70
  - lib
76
71
  required_ruby_version: !ruby/object:Gem::Requirement
77
- none: false
78
72
  requirements:
79
- - - ! '>='
73
+ - - '>='
80
74
  - !ruby/object:Gem::Version
81
75
  version: '0'
82
- segments:
83
- - 0
84
- hash: 296992438860896651
85
76
  required_rubygems_version: !ruby/object:Gem::Requirement
86
- none: false
87
77
  requirements:
88
- - - ! '>='
78
+ - - '>='
89
79
  - !ruby/object:Gem::Version
90
80
  version: '0'
91
- segments:
92
- - 0
93
- hash: 296992438860896651
94
81
  requirements: []
95
82
  rubyforge_project:
96
- rubygems_version: 1.8.25
83
+ rubygems_version: 2.0.0
97
84
  signing_key:
98
- specification_version: 3
85
+ specification_version: 4
99
86
  summary: Keeps files more secure on server environments
100
87
  test_files:
101
88
  - test/Gemfile
102
- - test/secrets/cert_key
89
+ - test/spawn.rb
103
90
  - test/tester.rb
@@ -1,142 +0,0 @@
1
- module Secret
2
-
3
- # Locking utilities!
4
- module Locking
5
-
6
- # By default, wait for locks for 5 seconds
7
- DEFAULT_LOCK_WAIT_MS = 5000
8
-
9
- # Locks the current file. May prevent other object instances (or processes) from reading / writing to this file.
10
- # You may use this to upgrade your lock if you wish.
11
- # @param [Boolean] exclusive if TRUE, holds an exclusive lock meaning that other processes cannot read or write
12
- # to the file. If FALSE, allows other files to read from the file, but not to write.
13
- # @return [Boolean] true if the lock was created successfully, false if it was not (likely due to another process holding a lock,
14
- # or we already hold the lock).
15
- # @note Remember to unlock the file once you are done using it!
16
- def lock!(exclusive = false)
17
- return false if(locked? and !owns_lock?)
18
-
19
- # Write
20
- # The file contents will be of '<exclusive bit>:<process id>'
21
- lock_string = (exclusive ? '0' : '1') + ":#{Process.pid}"
22
- ::File.open(lock_file_path, 'w',Secret::CHMOD_MODE) { |f| f.write lock_string }
23
- @owns_lock = true
24
-
25
- return true
26
- end
27
-
28
- # Checks to see if this file is locked. Has no indication of whether we own the lock or not.
29
- # @param [Boolean] exclusive if TRUE, only checks if it's an exclusive lock
30
- # @return [Boolean] true if the file is locked, false otherwise.
31
- def locked?(exclusive = false)
32
- return false unless ::File.exist?(lock_file_path)
33
- return true unless exclusive # Don't bother checking for non-exlusive
34
- ex = false
35
- begin
36
- # Quickly peek at the file to see if it is exclusive
37
- ::File.open(lock_file_path, 'r', Secret::CHMOD_MODE) do |f|
38
- ex = f.seek(1, IO::SEEK_CUR) == '1'
39
- f.close
40
- end
41
- return ex
42
- rescue Exception => e
43
- return false
44
- end
45
- end
46
-
47
-
48
- # An alias for {#owns_lock}
49
- def owns_lock?
50
- return owns_lock
51
- end
52
-
53
- # Unlocks this file.
54
- # @return [Boolean] true if unlock was successful, false if unlock was unsuccessful (i.e. the file wasn't locked,
55
- # or we don't own the lock)
56
- def unlock!
57
- return false if !locked?
58
- return false if !owns_lock?
59
-
60
- ::File.delete(lock_file_path)
61
- @owns_lock = false
62
- return true
63
- end
64
-
65
- # Forcibly unlocks this file, regardless of whether the lock is owned
66
- # @return [Boolean] true if unlock was successful, false if the file isn't locked.
67
- def force_unlock!
68
- return false if !locked?
69
- ::File.delete(lock_file_path)
70
- @owns_lock = false
71
- return true
72
- end
73
-
74
-
75
- # Waits for the file to become unlocked for 'max_time_ms'. If file is locked for that long, then raises an
76
- # {Secret::::FileLockedError}.
77
- # @param [Boolean] exclusive if TRUE, waits for an exclusive lock or shared lock, and then locks with an
78
- # exclusive lock if a block is given. If FALSE, then only waits for an exclusive lock.
79
- # @param [Integer] max_time_ms the maximum number of MS to wait until raising an error. If negative, waits forever.
80
- # @param [Integer] sleep_ms the number of MS to sleep before polling the lock again
81
- # @param [Proc] block pass a block to then execute block while locked (i.e. {#lock_and_do})
82
- def wait_for_unlock(exclusive = false, max_time_ms = DEFAULT_LOCK_WAIT_MS, sleep_ms = 100, &block)
83
- ms_start = (Time.now.to_f * 1000.0).to_i
84
- wait_exclusive = exclusive or block_given?
85
- sleep_ms = sleep_ms / 1000.0
86
- while locked?(!wait_exclusive)
87
- break if owns_lock?
88
- sleep(sleep_ms)
89
- continue if max_time_ms < 0
90
- ms_new = (Time.now.to_f * 1000.0).to_i
91
- if (ms_new - ms_start) > max_time_ms
92
- raise Secret::FileLockedError, "Timeout of #{max_time_ms} MS exceeded while waiting for lock!"
93
- end
94
- end
95
-
96
- # If we are here, waiting has finished
97
- lock_and_do(exclusive, &block) if block_given?
98
- end
99
-
100
- # Locks and executes a block, then unlocks upon block execution. This will always ensure that the lock
101
- # is released upon block execution, regardless of error or no.
102
- # @param [Boolean] exclusive if TRUE, uses an exclusive lock. If false, uses a shared lock
103
- # @raise [ArgumentError] if a block is not provided
104
- # @raise [FileLockedError] if the file is locked
105
- def lock_and_do(exclusive = false, &block)
106
- raise ArgumentError, 'Block not given' unless block_given?
107
- raise Secret::FileLockedError, 'Cannot lock a file if it is already locked!' if locked?
108
- lock!(exclusive)
109
- begin
110
- block.call
111
- rescue Exception => e
112
- raise e
113
- ensure
114
- unlock!
115
- end
116
- end
117
-
118
-
119
- # Gets information about the current lock:
120
- #
121
- # {:pid => 123123, :exclusive => true }
122
- #
123
- # @return [Hash,nil] information about the current lock, or nil if no lock is present
124
- def lock_info
125
- return nil unless locked?
126
- begin
127
- str = nil
128
- ::File.open(lock_file_path, 'r', Secret::CHMOD_MODE) {|f| str = f.read }
129
- ex,pid = str.split ':'
130
- return {:exclusive => (ex == "1"), :pid => pid.to_i}
131
- rescue Exception => e
132
- return nil
133
- end
134
- end
135
-
136
- protected
137
-
138
- def lock_file_path
139
- raise NotImplementedError, "'lock_file_path' needs to be implemented"
140
- end
141
- end
142
- end
@@ -1 +0,0 @@
1
- Hello World!