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 '=').

Instance Method Summary collapse

Constructor Details

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

Parameters:

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

    Configure the safe as shown in Configurable.


78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/sia/safe.rb', line 78

def initialize(name:, password:, **opt)
  options # Initialize the options with defaults
  @options.merge!(clean_options(opt))
  @options.freeze

  @name = name.to_sym
  @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 Method Details

#close(filename) ⇒ Object

Secure a file in the safe

Parameters:

  • filename (String)

    Relative or absolute path to file to secure.


146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/sia/safe.rb', line 146

def close(filename)
  clearpath = clear_filepath(filename)
  check_file_is_in_safe_dir(clearpath) if options[:portable]
  persist_safe_dir

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


199
200
201
202
203
204
# File 'lib/sia/safe.rb', line 199

def delete
  files.each { |_, d| d[:secure_file].delete if d[:safe] }
  index_path.delete
  salt_path.delete
  safe_dir.delete if safe_dir.empty?
end

#emptyObject

Open all files in the safe


183
184
185
# File 'lib/sia/safe.rb', line 183

def empty
  files.each { |filename, data| open(filename) if data[:safe]  }
end

#fillObject

Close all files in the safe


189
190
191
# File 'lib/sia/safe.rb', line 189

def fill
  files.each { |filename, data| close(filename) unless data[:safe]  }
end

#indexHash

Information about the files in the safe

Returns:

  • (Hash)

115
116
117
118
119
120
121
122
123
124
# File 'lib/sia/safe.rb', line 115

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)

107
108
109
# File 'lib/sia/safe.rb', line 107

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.


167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/sia/safe.rb', line 167

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

#safe_dirPathname

The directory where this safe is stored

Returns:

  • (Pathname)

99
100
101
# File 'lib/sia/safe.rb', line 99

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

#saltObject

The salt in binary encoding


134
135
136
137
138
139
140
# File 'lib/sia/safe.rb', line 134

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


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

def salt_path
  safe_dir / options[:salt_name]
end