Class: Sia::Safe
Overview
Keep all the files safe
Encrypt files and store them in a digital safe. Have one safe for everything, or use individual safes for each file to be encrypted.
When creating a safe provide at least a name and a password, and the defaults will take care of the rest.
safe = Sia::Safe.new(name: 'test', password: 'secret')
With a safe in hand, #close an existing file to keep it safe. (Note, any
type of file can be closed, not just .txt
files.)
safe.close('~/secret.txt')
The file will not longer be present at /path/to/the/secret.txt
; instead,
it will now be encrypted in the default Sia directory with a new name.
Restore it by using #open.
safe.open('~/secret.txt')
Notice that #open requires the path (relative or absolute) to the file as it existed before being encrypted, even though there's no file at that location anymore. To see all files available to open in the safe, take a peak in the #index.
pp safe.index
{:files=>
{"/Users/spencer/secret.txt"=>
{:secure_file=>"0nxntvTLteCTZ8cmZdX848gGaYHRAOHqir-1RuJ-n-E",
:last_closed=>2018-04-29 19:58:24 -0600,
:safe=>true}}}
The #fill and #empty methods are also helpful. #fill will close all files that belong to the safe, and #empty will open all the files.
safe.fill
safe.empty
Finally, if the safe has outlived its usefulness, #delete is there to help. #delete will remove a safe as-is, without opening or closing any files. This means that all currently closed files will be lost when using #delete.
safe.delete
FYI, the safe directory for this example has the structure:
~/
└── .sia_safes/
└── test/
├── .sia_index
├── .sia_salt
└── 0nxntvTLteCTZ8cmZdX848gGaYHRAOHqir-1RuJ-n-E
The .sia_safes/
directory holds all the safes, in this case the test
safe. Its name and location can be customized using Configurable. The
test/
directory where the test
safe lives. .sia_index
is an encrypted
file that stores information about the safe. Its name cam be customized:
Configurable. The .sia_salt
file stores the salt used to make a good
symmetric key out of the password. Its name cam be customized:
Configurable. The last file,
0nxntvTLteCTZ8cmZdX848gGaYHRAOHqir-1RuJ-n-E
, is the newly encrypted file.
Its name is a SHA256
digest of the full pathname of the clearfile (in this
case, "/Users/spencer/secret.txt"
) encoded in url-safe base 64 without
padding (ie, not ending '='
).
Constant Summary
Constants included from Configurable
Instance Attribute Summary collapse
-
#name ⇒ Object
readonly
Returns the value of attribute name.
Instance Method Summary collapse
-
#close(filename) ⇒ Object
Secure a file in the safe.
-
#delete ⇒ Object
Delete the safe as-is, without opening or closing files.
-
#empty ⇒ Object
Open all files in the safe.
-
#fill ⇒ Object
Close all files in the safe.
-
#index ⇒ Hash
Information about the files in the safe.
-
#index_path ⇒ Pathname
The absolute path to the encrypted index file.
- #initialize(name:, password:, **opt) ⇒ Safe constructor
-
#open(filename) ⇒ Object
Extract a file from the safe.
-
#persist! ⇒ Object
Persist the safe and its configuration.
-
#safe_dir ⇒ Pathname
The directory where this safe is stored.
-
#salt ⇒ Object
The salt in binary encoding.
-
#salt_path ⇒ Object
The absolute path to the file storing the salt.
Methods included from Configurable
Constructor Details
#initialize(name:, password:, **opt) ⇒ Safe
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/sia/safe.rb', line 82 def initialize(name:, password:, **opt) @name = name.to_sym @persisted_config = PersistedConfig.new(@name) # Initialize the options with defaults (opt) @lock = Lock.new( password.to_s, salt, [:buffer_bytes], [:digest_iterations] ) # Don't let initialization succeed if the password was invalid index end |
Instance Attribute Details
#name ⇒ Object (readonly)
Returns the value of attribute name.
73 74 75 |
# File 'lib/sia/safe.rb', line 73 def name @name end |
Instance Method Details
#close(filename) ⇒ Object
Secure a file in the safe
166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'lib/sia/safe.rb', line 166 def close(filename) clearpath = clear_filepath(filename) check_file_is_in_safe_dir(clearpath) if [:portable] persist! @lock.encrypt(clearpath, secure_filepath(clearpath)) info = files.fetch(clearpath, {}).merge( secure_file: secure_filepath(clearpath), last_closed: Time.now, safe: true ) update_index(:files, files.merge(clearpath => info)) end |
#delete ⇒ Object
Delete the safe as-is, without opening or closing files
All closed files are deleted. Open files are not deleted. The safe dir is deleted if there is nothing besides closed files, the #index_path, and the #salt_path in it.
219 220 221 222 223 224 225 226 227 228 |
# File 'lib/sia/safe.rb', line 219 def delete return unless @persisted_config.exist? files.each { |_, d| d[:secure_file].delete if d[:safe] } index_path.delete salt_path.delete safe_dir.delete if safe_dir.empty? @persisted_config.delete end |
#empty ⇒ Object
Open all files in the safe
203 204 205 |
# File 'lib/sia/safe.rb', line 203 def empty files.each { |filename, data| open(filename) if data[:safe] } end |
#fill ⇒ Object
Close all files in the safe
209 210 211 |
# File 'lib/sia/safe.rb', line 209 def fill files.each { |filename, data| close(filename) unless data[:safe] } end |
#index ⇒ Hash
Information about the files in the safe
135 136 137 138 139 140 141 142 143 144 |
# File 'lib/sia/safe.rb', line 135 def index return {} unless index_path.file? YAML.load(@lock.decrypt_from_file(index_path)) rescue Psych::SyntaxError # A Psych::SyntaxError was raised in my integration test once when an # incorrect password was used. This raises the right error if that ever # happens again. raise Sia::Error::PasswordError, 'Invalid password' end |
#index_path ⇒ Pathname
The absolute path to the encrypted index file
127 128 129 |
# File 'lib/sia/safe.rb', line 127 def index_path safe_dir / [:index_name] end |
#open(filename) ⇒ Object
Extract a file from the safe
187 188 189 190 191 192 193 194 195 196 197 198 199 |
# File 'lib/sia/safe.rb', line 187 def open(filename) clearpath = clear_filepath(filename) check_file_is_in_safe_dir(clearpath) if [:portable] @lock.decrypt(clearpath, secure_filepath(clearpath)) info = files.fetch(clearpath, {}).merge( secure_file: secure_filepath(clearpath), last_opened: Time.now, safe: false ) update_index(:files, files.merge(clearpath => info)) end |
#persist! ⇒ Object
Persist the safe and its configuration
This doesn't have any effect once a file has been closed in the safe.
104 105 106 107 108 109 110 111 112 113 |
# File 'lib/sia/safe.rb', line 104 def persist! return if @persisted_config.exist? safe_dir.mkpath unless safe_dir.directory? salt_path.write(salt) unless salt_path.file? @persisted_config.persist() update_index(:files, files) end |
#safe_dir ⇒ Pathname
The directory where this safe is stored
119 120 121 |
# File 'lib/sia/safe.rb', line 119 def safe_dir [:root_dir] / name.to_s end |
#salt ⇒ Object
The salt in binary encoding
154 155 156 157 158 159 160 |
# File 'lib/sia/safe.rb', line 154 def salt if salt_path.file? salt_path.read else @salt ||= SecureRandom.bytes(Sia::Lock::DIGEST.new.digest_length) end end |
#salt_path ⇒ Object
The absolute path to the file storing the salt
148 149 150 |
# File 'lib/sia/safe.rb', line 148 def salt_path safe_dir / [:salt_name] end |