class UniversaTools::KeyRing

The key ring is useful when it is needed to keep several keys with the same password. As decryption each key using a password takes lot of time, using key ring could save lot of time in server applications.

Also, KeyRing uses individual files storage (it takes a directory to keep its contents in) even a big keyring could safely and effectively be stored in the git or cloud disk. As long as the password is properly concealed from sources (using some sort of secret credentials or smart deploy), it is absolutely safe to keep the keyring itself in the unsafe containers (dropbox, github, google disk, etc.)

Constants

KeyRecord

The record class that hold key, tag and associated information inside the ring

Attributes

keys[RW]

Public Class Methods

new(path, generate: false, override: false, pbkdf2_rounds: 500000, salt: path[0..].force_encoding('binary'), password_proc: -> (prompt) { console_password_input(prompt) } click to toggle source

Create or open key ring at the specified path.

@param [String] path to open from/create at @param [Boolean] generate true to generate if keyring does not exist @param [Boolean] override to delete existing key ring if exists and create new one @param [Integer] pbkdf2_rounds to generate the key @param [String] salt binary string with salt for PBKDF2 key generation @param [Proc] password_proc proc that takes prompt and returns password @param [String] password the password to use. Only one of password or password_proc must be present @param [Boolean] readonly open existing keyring in readonly mode to prevent any modification

# File lib/universa_tools/keyring.rb, line 53
def initialize(path, generate: false, override: false, pbkdf2_rounds: 500000, salt: path[0..].force_encoding('binary'),
               password_proc: -> (prompt) { console_password_input(prompt) }, password: nil, readonly: false)
  @generate, @override, @pbkdf2_rounds, @salt = generate, override, pbkdf2_rounds, salt
  @password_proc, @password = password_proc, password
  @readonly = readonly

  @readonly && (@generate || @override) and raise ArgumentError, "readonly is incompatible with override or generate"
  @key_tags = {}
  @key_addresses = {}
  @keys = []

  @root_path = File.expand_path(path)
  exists = File.exist?(config_file_name)
  case
    when @generate && exists
      if @override
        FileUtils.rm_rf Dir.glob("#@root_path/*")
        generate_new()
      else
        error "Can't generate: keyring already exists"
      end
    when @generate && !exists
      generate_new()
    when exists
      open_keyring()
    else
      raise NotFoundException.new(path)
  end
end

Public Instance Methods

[](tag_or_address) click to toggle source

Find a key by tag or address. @param [String | KeyAddress] tag_or_address to look for. String could be a tag or string representation of

KeyAddress

@return [PrivateKey] or nil

# File lib/universa_tools/keyring.rb, line 116
def [](tag_or_address)
  find(tag_or_address)&.key
end
add_key(key, tag = nil, **key_data) click to toggle source

Add key to the ring. Will not change the ring of the key already exists

# File lib/universa_tools/keyring.rb, line 84
def add_key(key, tag = nil, **key_data)
  will_write!
  raise ArgumentError, "the key tagged #{tag} already exists" if tag && @key_tags[tag]
  if @key_addresses[key.short_address] || @key_addresses[key.long_address]
    raise ArgumentError, "key is already in the ring"
  end
  kr = KeyRecord.new(tag, key, key_data, create_temp_file_name(@root_path, 'data'))
  kr.save(@main_key)
  @keys << kr
  @key_tags[tag] = kr
  @key_addresses[key.short_address] = kr
  @key_addresses[key.long_address] = kr
end
change_password(new_password) click to toggle source
# File lib/universa_tools/keyring.rb, line 160
def change_password new_password
  will_write!
  @main_record = Pbkdf2CryptoRecord.new(hint: 'main password', salt: 42.random_alnums)
  @main_record.encrypt(new_password, @main_key.pack)
  write_config()
end
delete_key(key) click to toggle source

delete the key off the ring @raise [NotFoundException] if such a key is not in the ring

# File lib/universa_tools/keyring.rb, line 150
def delete_key key
  record = @keys.find { |r| r.key == key }
  record or raise NotFoundException
  record.tag && @key_tags.delete(record.tag)
  @key_addresses.delete(record.key.long_address)
  @key_addresses.delete(record.key.short_address)
  @keys.delete record
  FileUtils.rm_f record.file_name
end
info(tag_or_address) click to toggle source

Get the associated data @param [String | KeyAddress] tag_or_address to look for @return [Hash] that could be empty or nil if the key is not found

# File lib/universa_tools/keyring.rb, line 123
def info(tag_or_address)
  find(tag_or_address)&.data
end
matching_records(prefix) click to toggle source

Get oll matching {KeyRecord} instances where the tag starts with the prefix (case-insensitive), or string representation of short or long address starts woth the prefix (case-sensitive)

@param [String] prefix to look for in tags and addresses @return [Array(KeyRecord)] all matching records, could be empty.

# File lib/universa_tools/keyring.rb, line 103
def matching_records(prefix)
  pd = prefix.downcase
  @keys.select { |r|
    r.tag&.downcase&.start_with?(pd) ||
        r.key.long_address.to_s.start_with?(prefix) ||
        r.key.short_address.to_s.start_with?(prefix)
  }
end
system_config() click to toggle source
# File lib/universa_tools/keyring.rb, line 37
def system_config
  @system_config ||= begin
    YAML.load_file File.expand_path("~/.universa/keyring_config.yml") rescue nil
  end
end
tag_by(address: nil, key: nil) click to toggle source

Retreive the tag by the key or its address @param [KeyAddress] address @param [PrivateKey] key @return [String] tag of the key or nil if there is no tag or key not found

# File lib/universa_tools/keyring.rb, line 131
def tag_by(address: nil, key: nil)
  case
    when key
      @keys.find[key]&.tag
    when address
      @key_addresses[address]&.tag
    else
      raise ArgumentError, "no criterion specified"
  end
end
version() click to toggle source

KeyRing version @return [SemanticVersion]

# File lib/universa_tools/keyring.rb, line 144
def version
  @version ||= SemanticVersion.new(@header['version'])
end

Private Instance Methods

backup_file_name() click to toggle source
# File lib/universa_tools/keyring.rb, line 213
def backup_file_name
  @backup_file_name ||= config_file_name + '~'
end
config_file_name() click to toggle source
# File lib/universa_tools/keyring.rb, line 198
def config_file_name
  @config_file_name ||= @root_path + "/uniring.unirecord"
end
console_password_input(prompt) click to toggle source
# File lib/universa_tools/keyring.rb, line 179
def console_password_input(prompt)
  loop do
    puts prompt
    psw = STDIN.noecho { |io| io.gets.chomp }
    puts "reenter password"
    psw1 = STDIN.noecho { |io| io.gets.chomp }
    psw1 == psw and return psw
    puts "password do no match, please try again"
  end
end
delete_file(backup_file_name) click to toggle source

delete file if exists

# File lib/universa_tools/keyring.rb, line 218
def delete_file(backup_file_name)
  will_write!
  FileUtils.rm(backup_file_name) if File.exists?(backup_file_name)
end
find(tag_or_address) click to toggle source
# File lib/universa_tools/keyring.rb, line 169
def find(tag_or_address)
  k = @key_tags[tag_or_address] and return k
  address = tag_or_address.is_a?(Universa::KeyAddress) ? tag_or_address : Universa::KeyAddress.new(tag_or_address)
  @keys.select { |kr| address.isMatchingKey(kr.key.public_key) }.first
rescue Farcall::RemoteError
  raise $! if $!.message !~ /IllegalArgumentException/
  # it is just a missing tag
  nil
end
generate_new() click to toggle source

creates new record if no exist. Does not wipe existing ring. @raise [Exception] on failure.

# File lib/universa_tools/keyring.rb, line 268
def generate_new
  will_write!
  FileUtils.mkdir_p(@root_path)
  FileUtils.chmod(0700, @root_path)

  @main_key = Universa::SymmetricKey.new()
  @main_record = Pbkdf2CryptoRecord.new(hint: 'main password', salt: 42.random_alnums)
  @main_record.encrypt request_password("main password for the new keyring"), @main_key.pack
  @fingerprint = 31.random_alnums.freeze
  @header = {tag: 'uniring', version: '0.1.0', fingerprint: @fingerprint}
  write_config()
end
get_key() click to toggle source
# File lib/universa_tools/keyring.rb, line 194
def get_key
  todo!
end
open_keyring() click to toggle source
# File lib/universa_tools/keyring.rb, line 223
def open_keyring
  read_config()
  Dir.glob("#{@root_path}/*.data") { |path|
    kr = KeyRecord.load(@main_key, path)
    @keys << kr
    @key_addresses[kr.key.short_address] = kr
    @key_addresses[kr.key.long_address] = kr
    @key_tags[kr.tag] = kr if kr.tag
  }
end
read_config() click to toggle source

dead existing ring configuration, needs to unlock after it.

# File lib/universa_tools/keyring.rb, line 235
def read_config
  try_read(config_file_name)
  @main_record or raise IOError("main key not found")
  unpacked = nil
  if (pwd=system_config&.dig("keyrings", @fingerprint, "password")) != nil
    unpacked = @main_record.try_decrypt(pwd)
  end
  unpacked ||= @main_record.decrypt(request_password("password to open repository"))
  @main_key = Universa::SymmetricKey.new(unpacked)
rescue IOError
  # potentially recoverable
  puts error_style("failed to open keyring: #$!")
  try_read(backup_file_name)
  puts "Backup keyring loaded"
end
request_password(text) click to toggle source
# File lib/universa_tools/keyring.rb, line 190
def request_password(text)
  @password || @password_proc.call(text)
end
try_read(name) click to toggle source

Try to read config from main or backup file @raise if it is not possible

# File lib/universa_tools/keyring.rb, line 253
def try_read(name)
  open(config_file_name, 'rb') { |input|
    parser = Boss::Parser.new(input)
    @header = parser.get
    @crypto_records = CryptoRecord.unpack_all(parser.get)
    @main_record = @crypto_records.find { |r| r.is_a?(Pbkdf2CryptoRecord) }
  }
  @fingerprint = @header['fingerprint']&.freeze or begin
    @header['fingerprint'] = @fingerprint = 31.random_alnums.freeze
    write_config()
  end
end
will_write!() click to toggle source
# File lib/universa_tools/keyring.rb, line 281
def will_write!
  @readonly and raise IOError, "keying is readonly"
end
write_config() click to toggle source
# File lib/universa_tools/keyring.rb, line 202
def write_config
  will_write!
  delete_file(backup_file_name)
  FileUtils.mv(config_file_name, backup_file_name) if File.exists?(config_file_name)
  open(config_file_name, 'wb') { |x|
    out = Boss::Formatter.new(x)
    out << @header << CryptoRecord.pack_all([@main_record])
  }
  delete_file(backup_file_name)
end