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.



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

def initialize(path)
  @path = path
end

Instance Attribute Details

#pathObject (readonly)

Returns the value of attribute path.



88
89
90
# File 'lib/aspera/keychain/macos_security.rb', line 88

def path
  @path
end

Class Method Details

.by_name(name) ⇒ Object



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

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

.defaultObject



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

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

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



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

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 '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_line = [SECURITY_UTILITY, command]
  options&.each do |k, v|
    Aspera.assert(supported.key?(k)){"unknown option: #{k}"}
    next if v.nil?
    command_line.push("-#{supported[k]}")
    command_line.push(v.shellescape) unless v.empty?
  end
  command_line.push(last_opt) unless last_opt.nil?
  Log.log.debug{"executing>>#{command_line.join(' ')}"}
  stdout, stderr, status = Open3.capture3(*command_line)
  Log.log.debug{"status=#{status}, stderr=#{stderr}"}
  Log.log.trace1{"stdout=#{stdout}"}
  raise "#{SECURITY_UTILITY} failed: #{status.exitstatus} : #{stderr}" unless status.success?
  return stdout
end

.key_chains(output) ⇒ Object



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

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

.list(options = {}) ⇒ Object



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

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

.loginObject



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

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

Instance Method Details

#decode_hex_blob(string) ⇒ Object



94
95
96
# File 'lib/aspera/keychain/macos_security.rb', line 94

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

#password(operation, pass_type, options) ⇒ Object



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
123
124
125
126
# File 'lib/aspera/keychain/macos_security.rb', line 98

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