Class: Sesame::Cave
- Inherits:
-
Object
- Object
- Sesame::Cave
- Defined in:
- lib/sesame/cave.rb
Overview
The Cave class implements a simple password manager.
Instance Attribute Summary collapse
-
#item ⇒ Object
Returns the value of attribute item.
Instance Method Summary collapse
-
#close ⇒ Object
Close the cave, encrypting and saving its contents if dirty.
-
#create!(phrase = nil) ⇒ Object
Create a new cave.
-
#delete(service, user = nil) ⇒ Object
Remove a service and username, then generate and return the passphrase.
-
#dirty? ⇒ Boolean
True if the cave has been modified and needs to be persisted.
-
#exists? ⇒ Boolean
True if the cave file exists; false otherwise.
-
#forget ⇒ Object
Remove the lock file.
-
#get(service, user = nil, index = nil) ⇒ Object
Generate and return the passphrase for a service and username.
-
#index ⇒ Object
Return the store.
-
#initialize(path, pow = 30) ⇒ Cave
constructor
Initialize with the path where the cave file will be stored.
-
#insert(service, user, index = nil) ⇒ Object
Insert a new service and username, then generate and return the passphrase.
-
#lock ⇒ Object
Lock the cave by encrypting and saving the secret to a lock file, and then closing the cave (which may mean saving it, if it was dirty).
-
#locked? ⇒ Boolean
True if the lock file exists; false otherwise.
-
#open(phrase) ⇒ Object
Open an existing cave, using the supplied phrase to decrypt its contents.
-
#open? ⇒ Boolean
True if the cave file has been loaded into memory and decrypted.
-
#path ⇒ Object
Return the full path of the cave file.
-
#unique?(service) ⇒ Boolean
True if a particular service has exactly one username.
-
#unlock(phrase) ⇒ Object
Unlock the cave, by loading and decrypting the secret using the supplied phrase, and then using that to load and decrypt the cave itself.
-
#update(service, user = nil, index = nil) ⇒ Object
Update the nonce for a service and username, then generate and return the passphrase.
Constructor Details
#initialize(path, pow = 30) ⇒ Cave
Initialize with the path where the cave file will be stored. Optionally specify the complexity of passphrase generation (only change this to lower values to make tests run more quickly).
18 19 20 21 22 23 24 25 26 27 |
# File 'lib/sesame/cave.rb', line 18 def initialize(path, pow = 30) @words = Dict.load raise Fail, 'Unexpected dictionary length' unless @words.length == 2048 @cave = File.(File.join(path, 'sesame.cave')) @lock = File.join(Dir.tmpdir, 'sesame.lock') @store = nil @dirty = false @item = nil @pow = pow end |
Instance Attribute Details
#item ⇒ Object
Returns the value of attribute item.
13 14 15 |
# File 'lib/sesame/cave.rb', line 13 def item @item end |
Instance Method Details
#close ⇒ Object
Close the cave, encrypting and saving its contents if dirty.
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/sesame/cave.rb', line 110 def close raise Fail, 'Cannot close store; it\'s not open' unless open? return unless dirty? # encrypt and save the store box = RbNaCl::SimpleBox.from_secret_key(@secret) data = @store.to_json encrypted_data = box.encrypt(data) File.open(@cave, 'wb') { |file| file.write(encrypted_data) } rescue RbNaCl::CryptoError => e raise Fail, e.messsage ensure @item = nil @store = nil @secret = nil @dirty = false end |
#create!(phrase = nil) ⇒ Object
Create a new cave. If the optional phrase is not supplied, then a random phrase will be returned (this is preferable; users should not select their own passphrase, because humans can’t random).
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
# File 'lib/sesame/cave.rb', line 57 def create!(phrase = nil) raise Fail, 'Cannot create; store already exists' if exists? || locked? || open? @store = {} insert('sesame', 'cave') @dirty = true # generate an 88-bit random number as a hex string number = if phrase.nil? RbNaCl::Util.bin2hex(RbNaCl::Random.random_bytes(11)) else words = phrase.downcase.split(' ') raise Fail, 'There must be exactly eight words' unless words.length == 8 raise Fail, 'Unrecognised word used' unless words.all? { |word| @words.include?(word) } Bases.val(words).in_base(@words).to_base(16) end number.prepend('0') while number.length < 22 @secret = _create_secret(number) # convert the hex string to a word string (base 2048) words = Bases.val(number).in_base(16).to_base(@words, array: true) # make sure it's always 8 words long (i.e. zero-pad the phrase) words.unshift(@words[0]) while words.length < 8 # sanity check the conversion sanity = Bases.val(words).in_base(@words).to_base(16) sanity.prepend('0') while sanity.length < 22 raise Fail, 'Base conversion failure' unless sanity == number # return the phrase to the user words.join(' ') rescue RbNaCl::CryptoError => e raise Fail, e. end |
#delete(service, user = nil) ⇒ Object
Remove a service and username, then generate and return the passphrase.
261 262 263 264 265 266 267 268 269 270 |
# File 'lib/sesame/cave.rb', line 261 def delete(service, user = nil) raise Fail, 'Cannot delete service details; store not open' unless open? raise Fail, 'Cannot delete the sesame service' if service.casecmp('sesame').zero? item = _find(service, user) user = item[:user] @store[service].delete(user) @store.delete(service) if @store[service].count.zero? @dirty = true _generate_phrase(item) end |
#dirty? ⇒ Boolean
True if the cave has been modified and needs to be persisted.
50 51 52 |
# File 'lib/sesame/cave.rb', line 50 def dirty? @dirty end |
#exists? ⇒ Boolean
True if the cave file exists; false otherwise.
35 36 37 |
# File 'lib/sesame/cave.rb', line 35 def exists? File.exist?(@cave) end |
#forget ⇒ Object
Remove the lock file.
199 200 201 |
# File 'lib/sesame/cave.rb', line 199 def forget File.delete(@lock) end |
#get(service, user = nil, index = nil) ⇒ Object
Generate and return the passphrase for a service and username.
221 222 223 224 225 226 227 |
# File 'lib/sesame/cave.rb', line 221 def get(service, user = nil, index = nil) raise Fail, 'Cannot get service details; store not open' unless open? raise Fail, 'Cannot get the sesame service' if service.casecmp('sesame').zero? item = _find(service, user) item[:index] = index unless index.nil? _generate_phrase(item) end |
#index ⇒ Object
Return the store. Note that this doesn’t expose any super-sensitive data; the store is just a hash of service name, usernames for each service, and a nonce for each username. These are combined with the secret (which is itself derived from the users passphrase) to create the password for each service and username.
208 209 210 211 |
# File 'lib/sesame/cave.rb', line 208 def index raise Fail, 'Cannot list the store; it\'s not open' unless open? @store.sort.to_h end |
#insert(service, user, index = nil) ⇒ Object
Insert a new service and username, then generate and return the passphrase.
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 |
# File 'lib/sesame/cave.rb', line 230 def insert(service, user, index = nil) raise Fail, 'Cannot insert service details; store not open' unless open? if @store.length.positive? raise Fail, 'Cannot insert the sesame service' if service.casecmp('sesame').zero? end raise Fail, 'Service cannot be empty' if service.strip.length.zero? raise Fail, 'User cannot be empty' if user.nil? || user.strip.length.zero? raise Fail, 'User already exists for that service' unless @store[service].nil? || @store[service][user].nil? @store[service] ||= {} @store[service][user] = index || 0 @dirty = true return if service == 'sesame' item = _find(service, user) _generate_phrase(item) end |
#lock ⇒ Object
Lock the cave by encrypting and saving the secret to a lock file, and then closing the cave (which may mean saving it, if it was dirty).
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/sesame/cave.rb', line 129 def lock raise Fail, 'Cannot lock cave; it\'s not open' unless open? # create a 16-bit checksum of the secret key item = _find('sesame', 'cave') data = @secret.dup data << item[:index] checksum = Digest::CRC16.checksum(data).to_s(16) checksum.prepend('0') while checksum.length < 4 # convert it to a short sequence of short words words = Bases.val(checksum).in_base(16).to_base((0...16).to_a, array: true) words.map! { |num, _| @words[num.to_i] } words.unshift(@words[0]) while words.length < 4 # create a key from it key = _create_secret(checksum) # encrypt and save the secret box = RbNaCl::SimpleBox.from_secret_key(key) encrypted_data = box.encrypt(@secret) File.open(@lock, 'wb') { |file| file.write(encrypted_data) } # return the phrase to the user words.join(' ') rescue RbNaCl::CryptoError => e raise Fail, e.messsage ensure close end |
#locked? ⇒ Boolean
True if the lock file exists; false otherwise.
40 41 42 |
# File 'lib/sesame/cave.rb', line 40 def locked? File.exist?(@lock) end |
#open(phrase) ⇒ Object
Open an existing cave, using the supplied phrase to decrypt its contents.
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/sesame/cave.rb', line 89 def open(phrase) raise Fail, 'Cannot open store' if !exists? || locked? || open? # make sure the phrase is 8 words long and that the words are valid words = phrase.downcase.split(' ') raise Fail, 'There must be exactly eight words' unless words.length == 8 raise Fail, 'Unrecognised word used' unless words.all? { |word| @words.include?(word) } # convert the phrase to a hex string number = Bases.val(words).in_base(@words).to_base(16) number.prepend('0') while number.length < 22 @secret = _create_secret(number) # load the store and decrypt it box = RbNaCl::SimpleBox.from_secret_key(@secret) encrypted_data = File.open(@cave, 'rb', &:read) data = box.decrypt(encrypted_data) @store = JSON.parse(data) @dirty = false rescue RbNaCl::CryptoError => e raise Fail, e. end |
#open? ⇒ Boolean
True if the cave file has been loaded into memory and decrypted.
45 46 47 |
# File 'lib/sesame/cave.rb', line 45 def open? !@store.nil? end |
#path ⇒ Object
Return the full path of the cave file.
30 31 32 |
# File 'lib/sesame/cave.rb', line 30 def path @cave end |
#unique?(service) ⇒ Boolean
True if a particular service has exactly one username.
214 215 216 217 218 |
# File 'lib/sesame/cave.rb', line 214 def unique?(service) raise Fail, 'Cannot test service uniqueness; store not open' unless open? raise Fail, 'No such service' if @store[service].nil? @store[service].count < 2 end |
#unlock(phrase) ⇒ Object
Unlock the cave, by loading and decrypting the secret using the supplied phrase, and then using that to load and decrypt the cave itself.
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/sesame/cave.rb', line 157 def unlock(phrase) raise Fail, 'Cannot unlock store; it\'s not locked' unless locked? raise Fail, 'Cannot unlock store; it\'s already open' if open? # make sure the phrase is 4 words long and that the words are valid words = phrase.downcase.split(' ') if words.length == 1 && phrase.length == 4 words = [] phrase.each_char do |char| words << @words[0..15].find { |word| word[0] == char } end end raise Fail, 'There must be exactly four words' unless words.length == 4 raise Fail, 'Unrecognised word used' unless words.all? { |word| @words[0..15].include?(word) } # convert the phrase to a hex string words.map! { |word, _| @words.index(word) } checksum = Bases.val(words).in_base((0...16).to_a).to_base(16) checksum.prepend('0') while checksum.length < 4 key = _create_secret(checksum) # load the secret and decrypt it box = RbNaCl::SimpleBox.from_secret_key(key) encrypted_data = File.open(@lock, 'rb', &:read) @secret = box.decrypt(encrypted_data) # load the store and decrypt it box = RbNaCl::SimpleBox.from_secret_key(@secret) encrypted_data = File.open(@cave, 'rb', &:read) data = box.decrypt(encrypted_data) @store = JSON.parse(data) item = _find('sesame', 'cave') data = @secret.dup data << item[:index] sanity = Digest::CRC16.checksum(data).to_s(16) sanity.prepend('0') while sanity.length < 4 raise 'Checksum failure' unless sanity == checksum @dirty = false rescue RbNaCl::CryptoError => e raise Fail, e. ensure @item = nil forget if locked? end |
#update(service, user = nil, index = nil) ⇒ Object
Update the nonce for a service and username, then generate and return the passphrase.
248 249 250 251 252 253 254 255 256 257 258 |
# File 'lib/sesame/cave.rb', line 248 def update(service, user = nil, index = nil) raise Fail, 'Cannot update service details; store not open' unless open? item = _find(service, user) index = item[:index] + 1 if index.nil? index = 0 if index.negative? user = item[:user] @store[service][user] = index @dirty = true item = _find(service, user) _generate_phrase(item) unless service == 'sesame' end |