Class: Aspera::Keychain::MacosSecurity::Keychain

Inherits:
Object
  • Object
show all
Defined in:
lib/aspera/keychain/macos_security.rb

Overview

keychain based on macOS keychain, using ‘security` command line

Constant Summary collapse

SECURITY_UTILITY =
'security'
DOMAINS =
%i[user system common dynamic].freeze
LIST_OPTIONS =
{
  domain: :c
}
ADD_PASS_OPTIONS =
{
  account:  :a,
  creator:  :c,
  type:     :C,
  domain:   :d,
  kind:     :D,
  value:    :G,
  comment:  :j,
  label:    :l,
  path:     :p,
  port:     :P,
  protocol: :r,
  server:   :s,
  service:  :s,
  auth:     :t,
  password: :w,
  getpass:  :g
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path) ⇒ Keychain

Returns a new instance of Keychain.



86
87
88
# File 'lib/aspera/keychain/macos_security.rb', line 86

def initialize(path)
  @path = path
end

Instance Attribute Details

#pathObject (readonly)

Returns the value of attribute path.



84
85
86
# File 'lib/aspera/keychain/macos_security.rb', line 84

def path
  @path
end

Class Method Details

.by_name(name) ⇒ Object



80
81
82
# File 'lib/aspera/keychain/macos_security.rb', line 80

def by_name(name)
  list.find{ |kc| kc.path.end_with?("/#{name}.keychain-db")}
end

.defaultObject



67
68
69
# File 'lib/aspera/keychain/macos_security.rb', line 67

def default
  key_chains(execute('default-keychain')).first
end

.execute(command, options = nil, supported = nil, last_opt = nil) ⇒ Object



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/aspera/keychain/macos_security.rb', line 41

def execute(command, options = nil, supported = nil, last_opt = nil)
  url = options&.delete(:url)
  if !url.nil?
    uri = URI.parse(url)
    Aspera.assert(uri.scheme.eql?('https')){'only https'}
    options[:protocol] = 'htps' # cspell: disable-line
    raise Error, 'host required in URL' if uri.host.nil?
    options[:server] = uri.host
    options[:path] = uri.path unless ['', '/'].include?(uri.path)
    options[:port] = uri.port unless uri.port.eql?(443) && !url.include?(':443/')
  end
  command_args = [command]
  options&.each do |k, v|
    Aspera.assert(supported.key?(k)){"unknown option: #{k}"}
    next if v.nil?
    command_args.push("-#{supported[k]}")
    command_args.push(v.shellescape) unless v.empty?
  end
  command_args.push(last_opt) unless last_opt.nil?
  return Environment.secure_capture(exec: SECURITY_UTILITY, args: command_args)
end

.key_chains(output) ⇒ Object



63
64
65
# File 'lib/aspera/keychain/macos_security.rb', line 63

def key_chains(output)
  output.split("\n").collect{ |line| new(line.strip.gsub(/^"|"$/, ''))}
end

.list(options = {}) ⇒ Object



75
76
77
78
# File 'lib/aspera/keychain/macos_security.rb', line 75

def list(options = {})
  Aspera.assert_values(options[:domain], DOMAINS, type: ParameterError){'domain'} unless options[:domain].nil?
  key_chains(execute('list-keychains', options, LIST_OPTIONS))
end

.loginObject



71
72
73
# File 'lib/aspera/keychain/macos_security.rb', line 71

def 
  key_chains(execute('login-keychain')).first
end

Instance Method Details

#decode_hex_blob(string) ⇒ Object



90
91
92
# File 'lib/aspera/keychain/macos_security.rb', line 90

def decode_hex_blob(string)
  [string].pack('H*').force_encoding('UTF-8')
end

#password(operation, pass_type, options) ⇒ Object



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/aspera/keychain/macos_security.rb', line 94

def password(operation, pass_type, options)
  Aspera.assert_values(operation, %i[add find delete]){'operation'}
  Aspera.assert_values(pass_type, %i[generic internet]){'pass_type'}
  Aspera.assert_type(options, Hash)
  missing = (operation.eql?(:add) ? %i[account service password] : %i[label]) - options.keys
  Aspera.assert(missing.empty?){"missing options: #{missing}"}
  options[:getpass] = '' if operation.eql?(:find)
  output = self.class.execute("#{operation}-#{pass_type}-password", options, ADD_PASS_OPTIONS, @path)
  raise output.gsub(/^.*: /, '') if output.start_with?('security: ')
  return unless operation.eql?(:find)
  attributes = {}
  output.split("\n").each do |line|
    case line
    when /^keychain: "(.+)"/
      # ignore
    when /0x00000007 .+="(.+)"/
      attributes['label'] = Regexp.last_match(1)
    when /"(\w{4})".+="(.+)"/
      attributes[Regexp.last_match(1)] = Regexp.last_match(2)
    when /"(\w{4})"<blob>=0x([[:xdigit:]]+)/
      attributes[Regexp.last_match(1)] = decode_hex_blob(Regexp.last_match(2))
    when /^password: "(.+)"/
      attributes['password'] = Regexp.last_match(1)
    when /^password: 0x([[:xdigit:]]+)/
      attributes['password'] = decode_hex_blob(Regexp.last_match(1))
    end
  end
  return attributes
end