secret 0.0.1 → 0.0.2

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.
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!