Class: Keychain::Item

Inherits:
Object
  • Object
show all
Defined in:
lib/mr_keychain/item.rb

Overview

Note:

Only the #password method should ever cause an alert to pop up and require permission, and this should only happen for keychain items that were not created using the same program that is asking for the password.

@ todo Need to add some documentation to explain locally cached attributes and how they need to be #save!'d in order to persist changes and additions.

Represents an entry in the login keychain.

In order to be secure, this class will NEVER cache a password; any time that you change a password, it will be written to the keychain immeadiately.

Accessors (collapse)

Alternate accessors (collapse)

Instance Method Summary (collapse)

Constructor Details

- (Item) initialize(attrs = {})

TODO:

Exploit hash lookup failure blocks to do dynamic attribute lookup

Each Keychain::Item should contain a KSecClass at the very least; the default value is KSecClassInternetPassword.

Parameters:

  • attributes (Hash)


26
27
28
29
# File 'lib/mr_keychain/item.rb', line 26

def initialize attrs = {}
  @tainted    = {}
  @attributes = { KSecClass => KSecClassInternetPassword }.merge(attrs)
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

- (Object) method_missing(meth, *args)

Dynamic get/set for the various attributes that a keychain item can have.

Parameters:

  • meth (Symbol)

    the unique part of an attribute constant (e.g. account for KSecAttrAccount)



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/mr_keychain/item.rb', line 124

def method_missing meth, *args
  method    = meth.to_s
  setter    = method.chomp!('=')
  predicate = method.chomp!('?')
  const     = "KSecAttr#{'Is' if predicate}#{method.camelize!}"
  if Kernel.const_defined?(const)
    const_value = Kernel.const_get(const)
    return (setter ? self.send(:[]=, const_value, *args) : self[const_value])
  end
  const    = "KSecAttrIs#{method}"
  if Kernel.const_defined?(const)
    const_value = Kernel.const_get(const)
    return (setter ? self.send(:[]=, const_value, *args) : self[const_value])
  end
  super
end

Instance Method Details

- (Object) [](key)

Direct access to the attributes hash of the keychain item. You can get a list of all the attributes from Apple's documentation (see the README file).

Examples:

Get the server address

@item[KSecAttrServer]

Get the account name

@item[KSecAttrAccount]

Get the password

@item[KSecAttrPassword]


44
45
46
# File 'lib/mr_keychain/item.rb', line 44

def [] key
  key == KSecAttrPassword ? self.password : @attributes[key]
end

- (Object) []=(key, value)

Direct access to the attributes hash of the keychain item. You can get a list of all the attributes from Apple's documentation (see the README file).

Examples:

Set a comment

@item[KSecAttrComment] = 'my alternative account'

Set the port

@item[KSecAttrPort] = 9001

Set the password

@item[KSecAttrPassword] = 'raspberries'


59
60
61
62
# File 'lib/mr_keychain/item.rb', line 59

def []= key, value
  @tainted[key] = true unless value == @attributes[key]
  @attributes[key] = value
end

- (Boolean) exists?

See if the item currently exists in the keychain.

Returns:

  • (Boolean)


145
146
147
# File 'lib/mr_keychain/item.rb', line 145

def exists?
  Keychain.item_exists?(@attributes)
end

- (Object) item_class

Equivalent to #[KSecClass]



67
# File 'lib/mr_keychain/item.rb', line 67

def item_class; self[KSecClass]; end

- (Object) item_class=(value)

Equivalent to #[KSecClass] =



69
# File 'lib/mr_keychain/item.rb', line 69

def item_class= value; self[KSecClass] = value; end

- (String) password

Note:

Blank passwords should come back as an empty string, but that hasn't been tested thoroughly.

Returns the password for the item.

This method will raise an error if no keychain item is found, which should only happen if the item was deleted since this object was instantiated or you changed some of the key/value pairs used to lookup the object.

Returns:

  • (String)

    UTF8 encoded password string

Raises:



84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/mr_keychain/item.rb', line 84

def password
  search = @attributes.merge(
    KSecMatchLimit => KSecMatchLimitOne,
    KSecReturnData => true
  )
  result = Pointer.new(:id)

  case error_code = SecItemCopyMatching( search, result )
  when ErrSecSuccess then
    result[0].to_str
  else
    raise KeychainException.new( 'Getting password', error_code )
  end
end

- (String) password=(new_password)

Updates the password associated with the keychain item. If the item does not exist in the keychain it will be added first.

Parameters:

  • new_password (String)

    a UTF-8 encoded string

Returns:

  • (String)

    the saved password

Raises:



106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/mr_keychain/item.rb', line 106

def password= new_password
  password_data = { KSecValueData => (new_password || '').to_data }
  error_code    = if exists?
                    SecItemUpdate( @attributes, password_data )
                  else
                    SecItemAdd( @attributes.merge(password_data), nil )
                  end
  case error_code
  when ErrSecSuccess then password_data[KSecValueData].to_str
  else raise KeychainException.new( 'Updating password', error_code )
  end
end

- (Keychain::Item) reload!

Reload the cached item attributes from the keychain. An error will be raised if the item does not exist. If more than one item exists then the first one found will be reloaded.

Returns:

Raises:



156
157
158
159
160
161
162
163
164
165
# File 'lib/mr_keychain/item.rb', line 156

def reload!
  new_attributes = Pointer.new(:id)
  old_attributes = @attributes.merge( KSecReturnAttributes => true,
                                      KSecMatchLimit => KSecMatchLimitOne )
  error_code     = SecItemCopyMatching(old_attributes, new_attributes)
  case error_code
  when ErrSecSuccess then @attributes = new_attributes[0]
  else raise KeychainException.new( 'Reloading keychain item', error_code )
  end
end