Class: Keychain::Item

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

Overview

Note:

Methods only need a user's explicit authorization if they want the password data, metadata does not need permission. In these cases, the OS should present an alert asking to allow, deny, or always allow the script to access. You need to be careful when using 'always allow' if you are running this code from interactive ruby or the regular interpreter because you could accidentally allow any future script to not require permission to access any password in the keychain.

Represents an entry in the login keychain.

The big assumption that this class makes is that you only ever want to work with a single keychain item; whether it be searching for metadata, getting passwords, or adding a new entry.

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.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(attributes = {}) ⇒ Item

You should initialize objects of this class with the attributes relevant to the item you wish to work with, but you can add or remove attributes via accessors as well.



41
42
43
44
# File 'lib/mr_keychain.rb', line 41

def initialize attributes = {}
  @attributes = { KSecClass => KSecClassInternetPassword }
  @attributes.merge! attributes
end

Instance Attribute Details

#attributesHash



25
26
27
# File 'lib/mr_keychain.rb', line 25

def attributes
  @attributes
end

Instance Method Details

#[](key) ⇒ Object

Direct access to the attributes hash of the keychain item.



28
29
30
# File 'lib/mr_keychain.rb', line 28

def [] key
  @attributes[key]
end

#[]=(key, value) ⇒ Object

Direct access to the attributes hash of the keychain item.



33
34
35
# File 'lib/mr_keychain.rb', line 33

def []= key, value
  @attributes[key] = value
end

#exists?true, false

Note:

This method asks only for the metadata and doesn't need authorization

Returns true if there are any item matching the given attributes.

Raises:



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/mr_keychain.rb', line 50

def exists?
  result = Pointer.new :id
  search = @attributes.merge(
    KSecMatchLimit       => KSecMatchLimitOne,
    KSecReturnAttributes => true
  )

  case (error_code = SecItemCopyMatching(search, result))
  when ErrSecSuccess then
    true
  when ErrSecItemNotFound then
    false
  else
    message = SecCopyErrorMessageString(error_code, nil)
    raise KeychainException, "Error checking keychain item existence: #{message}"
  end
end

#metadataHash

Get all the metadata about a keychain item, they will be keyed according Apple's documentation.

Raises:



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/mr_keychain.rb', line 141

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

  case (error_code = SecItemCopyMatching(search, result))
  when ErrSecSuccess then
    result[0]
  else
    message = SecCopyErrorMessageString(error_code, nil)
    raise KeychainException, "Error getting metadata: #{message}"
  end
end

#metadata!Hash

Update attributes to include all the metadata from the keychain.

Raises:



160
161
162
# File 'lib/mr_keychain.rb', line 160

def metadata!
  @attributes = 
end

#passwordString

Note:

We ask for an NSData object here in order to get the password.

Returns the password for the first match found, raises an error if no keychain item is found.

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

Raises:



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/mr_keychain.rb', line 76

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

  case (error_code = SecItemCopyMatching(search, result))
  when ErrSecSuccess then
    result[0].to_str
  else
    message = SecCopyErrorMessageString(error_code, nil)
    raise KeychainException, "Error getting password: #{message}"
  end
end

#password=(new_password) ⇒ String

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

Raises:



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/mr_keychain.rb', line 98

def password= new_password
  password_data = { KSecValueData => new_password.to_data }
  if exists?
    error_code = SecItemUpdate( @attributes, password_data )
  else
    error_code = SecItemAdd( @attributes.merge(password_data), nil )
  end

  case error_code
  when ErrSecSuccess then
    password
  else
    message = SecCopyErrorMessageString(error_code, nil)
    raise KeychainException, "Error updating password: #{message}"
  end
end

#update!(new_attributes) ⇒ Hash

TODO:

This method does not really fit with the rest of the API.

Note:

This method does not need authorization unless you are updating the password.

Updates attributes of the item in the keychain. If the item does not exist yet then this method will raise an exception.

Use a value of nil to remove an attribute.



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

def update! new_attributes
  result = Pointer.new :id
  query  = @attributes.merge( KSecMatchLimit => KSecMatchLimitOne )

  case (error_code = SecItemUpdate(query, new_attributes))
  when ErrSecSuccess then
    metadata!
  else
    message = SecCopyErrorMessageString(error_code, nil)
    raise KeychainException, "Error updating keychain item: #{message}"
  end
end