Class: Sia::Safe

Inherits:
Object
  • Object
show all
Includes:
Configurable
Defined in:
lib/sia/safe.rb

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

Configurable::DEFAULTS

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Configurable

#options

Constructor Details

#initialize(name:, password:, **opt) ⇒ Safe

Parameters:

  • name (#to_sym)
  • password (#to_s)
  • opt (Hash)

    Configure new safes as shown in Configurable. When instantiating existing safes, configuration here must match the persisted config, or be absent.



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)

  options # Initialize the options with defaults
  assign_options(opt)

  @lock = Lock.new(
    password.to_s,
    salt,
    options[:buffer_bytes],
    options[:digest_iterations]
  )

  # Don't let initialization succeed if the password was invalid
  index
end

Instance Attribute Details

#nameObject (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

Parameters:

  • filename (String)

    Relative or absolute path to file to secure.



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 options[: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

#deleteObject

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

#emptyObject

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

#fillObject

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

#indexHash

Information about the files in the safe

Returns:

  • (Hash)


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_pathPathname

The absolute path to the encrypted index file

Returns:

  • (Pathname)


127
128
129
# File 'lib/sia/safe.rb', line 127

def index_path
  safe_dir / options[:index_name]
end

#open(filename) ⇒ Object

Extract a file from the safe

Parameters:

  • filename (String)

    Relative or absolute path to file to extract. Note: For in-place safes, the closed path may be used. Otherwise, this the path to the file as it existed before being closed.



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 options[: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(options)

  update_index(:files, files)
end

#safe_dirPathname

The directory where this safe is stored

Returns:

  • (Pathname)


119
120
121
# File 'lib/sia/safe.rb', line 119

def safe_dir
  options[:root_dir] / name.to_s
end

#saltObject

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_pathObject

The absolute path to the file storing the salt



148
149
150
# File 'lib/sia/safe.rb', line 148

def salt_path
  safe_dir / options[:salt_name]
end