ruby-keychain 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +8 -0
- data/README +90 -0
- data/lib/keychain.rb +72 -0
- data/lib/keychain/error.rb +34 -0
- data/lib/keychain/item.rb +188 -0
- data/lib/keychain/keychain.rb +188 -0
- data/lib/keychain/protocols.rb +43 -0
- data/lib/keychain/scope.rb +131 -0
- data/lib/keychain/sec.rb +165 -0
- data/lib/keychain/version.rb +4 -0
- data/spec/keychain_item_spec.rb +67 -0
- data/spec/keychain_spec.rb +220 -0
- data/spec/spec_helper.rb +11 -0
- metadata +155 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
|
2
|
+
# Procotol constants for use when creating/finding a Keychain::Item of type internet password
|
3
|
+
#
|
4
|
+
#
|
5
|
+
module Keychain::Protocols
|
6
|
+
# @private
|
7
|
+
def self.attach_protocol name
|
8
|
+
Sec.send :attach_variable, name, :pointer
|
9
|
+
constant_name = name.to_s.gsub('kSecAttrProtocol', '').upcase
|
10
|
+
const_set constant_name, CF::Base.typecast(Sec.send(name))
|
11
|
+
end
|
12
|
+
attach_protocol :kSecAttrProtocolFTP
|
13
|
+
attach_protocol :kSecAttrProtocolFTPAccount
|
14
|
+
attach_protocol :kSecAttrProtocolHTTP
|
15
|
+
attach_protocol :kSecAttrProtocolIRC
|
16
|
+
attach_protocol :kSecAttrProtocolNNTP
|
17
|
+
attach_protocol :kSecAttrProtocolPOP3
|
18
|
+
attach_protocol :kSecAttrProtocolSMTP
|
19
|
+
attach_protocol :kSecAttrProtocolSOCKS
|
20
|
+
attach_protocol :kSecAttrProtocolIMAP
|
21
|
+
attach_protocol :kSecAttrProtocolLDAP
|
22
|
+
attach_protocol :kSecAttrProtocolAppleTalk
|
23
|
+
attach_protocol :kSecAttrProtocolAFP
|
24
|
+
attach_protocol :kSecAttrProtocolTelnet
|
25
|
+
attach_protocol :kSecAttrProtocolSSH
|
26
|
+
attach_protocol :kSecAttrProtocolFTPS
|
27
|
+
attach_protocol :kSecAttrProtocolHTTPS
|
28
|
+
attach_protocol :kSecAttrProtocolHTTPProxy
|
29
|
+
attach_protocol :kSecAttrProtocolHTTPSProxy
|
30
|
+
attach_protocol :kSecAttrProtocolFTPProxy
|
31
|
+
attach_protocol :kSecAttrProtocolSMB
|
32
|
+
attach_protocol :kSecAttrProtocolRTSP
|
33
|
+
attach_protocol :kSecAttrProtocolRTSPProxy
|
34
|
+
attach_protocol :kSecAttrProtocolDAAP
|
35
|
+
attach_protocol :kSecAttrProtocolEPPC
|
36
|
+
attach_protocol :kSecAttrProtocolIPP
|
37
|
+
attach_protocol :kSecAttrProtocolNNTPS
|
38
|
+
attach_protocol :kSecAttrProtocolLDAPS
|
39
|
+
attach_protocol :kSecAttrProtocolTelnetS
|
40
|
+
attach_protocol :kSecAttrProtocolIMAPS
|
41
|
+
attach_protocol :kSecAttrProtocolIRCS
|
42
|
+
attach_protocol :kSecAttrProtocolPOP3S
|
43
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# A scope that represents the search for a keychain item
|
2
|
+
#
|
3
|
+
#
|
4
|
+
class Keychain::Scope
|
5
|
+
def initialize(kind, keychain=nil)
|
6
|
+
@kind = kind
|
7
|
+
@limit = nil
|
8
|
+
@keychains = [keychain]
|
9
|
+
@conditions = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
# Adds conditions to the scope. Conditions are merged with any previously defined conditions.
|
13
|
+
#
|
14
|
+
# The set of possible keys for conditions is given by Sec::ATTR_MAP.values. The legal values for the :protocol key are the constants in
|
15
|
+
# Keychain::Protocols
|
16
|
+
#
|
17
|
+
# @param [Hash] conditions options to create the item with
|
18
|
+
# @option conditions [String] :account The account (user name)
|
19
|
+
# @option conditions [String] :comment A free text comment about the item
|
20
|
+
# @option conditions [Integer] :creator the item's creator, as the unsigned integer representation of a 4 char code
|
21
|
+
# @option conditions [String] :generic generic passwords can have a generic data attribute
|
22
|
+
# @option conditions [String] :invisible whether the item should be invisible
|
23
|
+
# @option conditions [String] :negative A negative item records that the user decided to never save a password
|
24
|
+
# @option conditions [String] :label A label for the item (Shown in keychain access)
|
25
|
+
# @option conditions [String] :path The path the password is associated with (internet passwords only)
|
26
|
+
# @option conditions [String] :port The path the password is associated with (internet passwords only)
|
27
|
+
# @option conditions [String] :port The protocol the password is associated with (internet passwords only)
|
28
|
+
# Should be one of the constants at Keychain::Protocols
|
29
|
+
# @option conditions [String] :domain the domain the password is associated with (internet passwords only)
|
30
|
+
# @option conditions [String] :server the host name the password is associated with (internet passwords only)
|
31
|
+
# @option conditions [String] :service the service the password is associated with (generic passwords only)
|
32
|
+
# @option conditions [Integer] :type the item's type, as the unsigned integer representation of a 4 char code
|
33
|
+
#
|
34
|
+
# @return [Keychain::Scope] Returns self as a convenience. The scope is modified in place
|
35
|
+
def where(conditions)
|
36
|
+
@conditions.merge! conditions
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
# Sets the number of items returned by the scope
|
41
|
+
#
|
42
|
+
# @param [Integer] value The maximum number of items to return
|
43
|
+
# @return [Keychain::Scope] Returns self as a convenience. The scope is modified in place
|
44
|
+
def limit value
|
45
|
+
@limit = value
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
# Set the list of keychains to search
|
50
|
+
#
|
51
|
+
# @param [Array<Keychain::Keychain>] keychains The maximum number of items to return
|
52
|
+
# @return [Keychain::Scope] Returns self as a convenience. The scope is modified in place
|
53
|
+
def in *keychains
|
54
|
+
@keychains = keychains.flatten
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns the first matching item in the scope
|
59
|
+
#
|
60
|
+
# @return [Keychain::Item, nil]
|
61
|
+
def first
|
62
|
+
query = to_query
|
63
|
+
execute(query).first
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns an array containing all of the matching items
|
67
|
+
#
|
68
|
+
# @return [Array] The matching items. May be empty
|
69
|
+
def all
|
70
|
+
query = to_query
|
71
|
+
query[Sec::Search::LIMIT] = @limit ? @limit.to_cf : Sec::Search::ALL
|
72
|
+
execute query
|
73
|
+
end
|
74
|
+
|
75
|
+
# Creates a new keychain item
|
76
|
+
#
|
77
|
+
# @param [Hash] attributes options to create the item with
|
78
|
+
# @option attributes [String] :account The account (user name)
|
79
|
+
# @option attributes [String] :comment A free text comment about the item
|
80
|
+
# @option attributes [Integer] :creator the item's creator, as the unsigned integer representation of a 4 char code
|
81
|
+
# @option attributes [String] :generic generic passwords can have a generic data attribute
|
82
|
+
# @option attributes [String] :invisible whether the item should be invisible
|
83
|
+
# @option attributes [String] :negative A negative item records that the user decided to never save a password
|
84
|
+
# @option attributes [String] :label A label for the item (Shown in keychain access)
|
85
|
+
# @option attributes [String] :path The path the password is associated with (internet passwords only)
|
86
|
+
# @option attributes [String] :port The path the password is associated with (internet passwords only)
|
87
|
+
# @option attributes [String] :port The protocol the password is associated with (internet passwords only)
|
88
|
+
# Should be one of the constants at Keychain::Protocols
|
89
|
+
# @option attributes [String] :domain the domain the password is associated with (internet passwords only)
|
90
|
+
# @option attributes [String] :server the host name the password is associated with (internet passwords only)
|
91
|
+
# @option attributes [String] :service the service the password is associated with (generic passwords only)
|
92
|
+
# @option attributes [Integer] :type the item's type, as the unsigned integer representation of a 4 char code
|
93
|
+
#
|
94
|
+
# @return [Keychain::Item]
|
95
|
+
def create(attributes)
|
96
|
+
raise "You must specify a password" unless attributes[:password]
|
97
|
+
|
98
|
+
Keychain::Item.new(attributes.merge(:klass => @kind)).save!(:keychain => @keychains.first)
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def execute query
|
104
|
+
result = FFI::MemoryPointer.new :pointer
|
105
|
+
status = Sec.SecItemCopyMatching(query, result)
|
106
|
+
if status == Sec.enum_value( :errSecItemNotFound)
|
107
|
+
return []
|
108
|
+
end
|
109
|
+
Sec.check_osstatus(status)
|
110
|
+
result = CF::Base.typecast(result.read_pointer).release_on_gc
|
111
|
+
unless result.is_a?(CF::Array)
|
112
|
+
result = CF::Array.immutable([result])
|
113
|
+
end
|
114
|
+
result.collect {|dictionary_of_attributes| Keychain::Item.from_dictionary_of_attributes(dictionary_of_attributes)}
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
def to_query
|
119
|
+
query = CF::Dictionary.mutable
|
120
|
+
@conditions.each do |k,v|
|
121
|
+
k = Sec::INVERSE_ATTR_MAP[k]
|
122
|
+
query[k] = v.to_cf
|
123
|
+
end
|
124
|
+
|
125
|
+
query[Sec::Query::CLASS] = @kind
|
126
|
+
query[Sec::Query::SEARCH_LIST] = CF::Array.immutable(@keychains) if @keychains && @keychains.any?
|
127
|
+
query[Sec::Query::RETURN_ATTRIBUTES] = CF::Boolean::TRUE
|
128
|
+
query[Sec::Query::RETURN_REF] = CF::Boolean::TRUE
|
129
|
+
query
|
130
|
+
end
|
131
|
+
end
|
data/lib/keychain/sec.rb
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
# The module to which FFI attaches constants
|
2
|
+
module Sec
|
3
|
+
extend FFI::Library
|
4
|
+
ffi_lib '/System/Library/Frameworks/Security.framework/Security'
|
5
|
+
|
6
|
+
typedef :int32, :osstatus
|
7
|
+
typedef :pointer, :keychainref
|
8
|
+
|
9
|
+
enum [
|
10
|
+
:errSecItemNotFound, -25300,
|
11
|
+
:errSecDuplicateItem, -25299,
|
12
|
+
:errSecAuthFailed, -25293,
|
13
|
+
:errSecNoSuchKeychain, -25294,
|
14
|
+
:errCancelled, -128
|
15
|
+
]
|
16
|
+
|
17
|
+
attach_variable 'kSecClassInternetPassword', :pointer
|
18
|
+
attach_variable 'kSecClassGenericPassword', :pointer
|
19
|
+
|
20
|
+
attach_variable 'kSecClass', :pointer
|
21
|
+
|
22
|
+
attach_variable 'kSecAttrAccess', :pointer
|
23
|
+
attach_variable 'kSecAttrAccount', :pointer
|
24
|
+
attach_variable 'kSecAttrAuthenticationType', :pointer
|
25
|
+
attach_variable 'kSecAttrComment', :pointer
|
26
|
+
attach_variable 'kSecAttrCreationDate', :pointer
|
27
|
+
attach_variable 'kSecAttrCreator', :pointer
|
28
|
+
attach_variable 'kSecAttrDescription', :pointer
|
29
|
+
attach_variable 'kSecAttrGeneric', :pointer
|
30
|
+
attach_variable 'kSecAttrIsInvisible', :pointer
|
31
|
+
attach_variable 'kSecAttrIsNegative', :pointer
|
32
|
+
attach_variable 'kSecAttrLabel', :pointer
|
33
|
+
attach_variable 'kSecAttrModificationDate', :pointer
|
34
|
+
attach_variable 'kSecAttrPath', :pointer
|
35
|
+
attach_variable 'kSecAttrPort', :pointer
|
36
|
+
attach_variable 'kSecAttrProtocol', :pointer
|
37
|
+
attach_variable 'kSecAttrSecurityDomain', :pointer
|
38
|
+
attach_variable 'kSecAttrServer', :pointer
|
39
|
+
attach_variable 'kSecAttrService', :pointer
|
40
|
+
attach_variable 'kSecAttrType', :pointer
|
41
|
+
|
42
|
+
attach_variable 'kSecMatchSearchList', :pointer
|
43
|
+
|
44
|
+
attach_variable 'kSecMatchLimit', :pointer
|
45
|
+
attach_variable 'kSecMatchLimitOne', :pointer
|
46
|
+
attach_variable 'kSecMatchLimitAll', :pointer
|
47
|
+
attach_variable 'kSecMatchItemList', :pointer
|
48
|
+
attach_variable 'kSecReturnAttributes', :pointer
|
49
|
+
attach_variable 'kSecReturnRef', :pointer
|
50
|
+
attach_variable 'kSecReturnData', :pointer
|
51
|
+
|
52
|
+
attach_variable 'kSecValueRef', :pointer
|
53
|
+
attach_variable 'kSecValueData', :pointer
|
54
|
+
attach_variable 'kSecUseKeychain', :pointer
|
55
|
+
|
56
|
+
|
57
|
+
|
58
|
+
# map of kSecAttr* constants to the corresponding ruby name for the attribute
|
59
|
+
# Used in {Keychain::Item#attributes}}
|
60
|
+
ATTR_MAP = {
|
61
|
+
CF::Base.typecast(kSecAttrAccess) => :access,
|
62
|
+
CF::Base.typecast(kSecAttrAccount) => :account,
|
63
|
+
CF::Base.typecast(kSecAttrAuthenticationType) => :authentication_type,
|
64
|
+
CF::Base.typecast(kSecAttrComment) => :comment,
|
65
|
+
CF::Base.typecast(kSecAttrCreationDate) => :created_at,
|
66
|
+
CF::Base.typecast(kSecAttrCreator) => :creator,
|
67
|
+
CF::Base.typecast(kSecAttrDescription) => :description,
|
68
|
+
CF::Base.typecast(kSecAttrGeneric) => :generic,
|
69
|
+
CF::Base.typecast(kSecAttrIsInvisible) => :invisible,
|
70
|
+
CF::Base.typecast(kSecAttrIsNegative) => :negative,
|
71
|
+
CF::Base.typecast(kSecAttrLabel) => :label,
|
72
|
+
CF::Base.typecast(kSecAttrModificationDate) => :updated_at,
|
73
|
+
CF::Base.typecast(kSecAttrPath) => :path,
|
74
|
+
CF::Base.typecast(kSecAttrPort) => :port,
|
75
|
+
CF::Base.typecast(kSecAttrProtocol) => :protocol,
|
76
|
+
CF::Base.typecast(kSecAttrSecurityDomain) => :security_domain,
|
77
|
+
CF::Base.typecast(kSecAttrServer) => :server,
|
78
|
+
CF::Base.typecast(kSecAttrService) => :service,
|
79
|
+
CF::Base.typecast(kSecAttrType) => :type,
|
80
|
+
CF::Base.typecast(kSecClass) => :klass
|
81
|
+
}
|
82
|
+
|
83
|
+
# Inverse of {ATTR_MAP}
|
84
|
+
INVERSE_ATTR_MAP = ATTR_MAP.invert
|
85
|
+
|
86
|
+
# Query options for use with SecCopyMatching, SecItemUpdate
|
87
|
+
#
|
88
|
+
module Query
|
89
|
+
#key identifying the class of an item (kSecClass)
|
90
|
+
CLASS = CF::Base.typecast(Sec.kSecClass)
|
91
|
+
#key speciying the list of keychains to search (kSecMatchSearchList)
|
92
|
+
SEARCH_LIST = CF::Base.typecast(Sec.kSecMatchSearchList)
|
93
|
+
#key indicating the list of specific keychain items to the scope the search to
|
94
|
+
ITEM_LIST = CF::Base.typecast(Sec.kSecMatchItemList)
|
95
|
+
#key indicating whether to return attributes (kSecReturnAttributes)
|
96
|
+
RETURN_ATTRIBUTES = CF::Base.typecast(Sec.kSecReturnAttributes)
|
97
|
+
#key indicating whether to return the SecKeychainItemRef (kSecReturnRef)
|
98
|
+
RETURN_REF = CF::Base.typecast(Sec.kSecReturnRef)
|
99
|
+
#key indicating whether to return the password data (kSecReturnData)
|
100
|
+
RETURN_DATA = CF::Base.typecast(Sec.kSecReturnData)
|
101
|
+
#key indicating which keychain to use for the operation (kSecUseKeychain)
|
102
|
+
KEYCHAIN = CF::Base.typecast(Sec.kSecUseKeychain)
|
103
|
+
end
|
104
|
+
|
105
|
+
# defines constants for use as the class of an item
|
106
|
+
module Classes
|
107
|
+
# constant identifiying generic passwords (kSecClassGenericPassword)
|
108
|
+
GENERIC = CF::Base.typecast(Sec.kSecClassGenericPassword)
|
109
|
+
# constant identifying internet passwords (kSecClassInternetPassword)
|
110
|
+
INTERNET = CF::Base.typecast(Sec.kSecClassInternetPassword)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Search match types for use with SecCopyMatching
|
114
|
+
module Search
|
115
|
+
#meta value for {Sec::Search::LIMIT} indicating that all items be returned (kSecMatchLimitAll)
|
116
|
+
ALL = CF::Base.typecast(Sec.kSecMatchLimitAll)
|
117
|
+
# hash key indicating the maximum number of items (kSecMatchLimit)
|
118
|
+
LIMIT = CF::Base.typecast(Sec.kSecMatchLimit)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Constants for use with SecItemAdd/SecItemUpdate
|
122
|
+
module Value
|
123
|
+
# The hash key for the SecKeychainItemRef (in the dictionary returned by SecCopyMatching) (kSecValueRef)
|
124
|
+
REF = CF::Base.typecast(Sec.kSecValueRef)
|
125
|
+
# The hash key for the password data (in the dictionary returned by SecCopyMatching) (kSecValueData)
|
126
|
+
DATA = CF::Base.typecast(Sec.kSecValueData)
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
# The base class of all CF types from the security framework
|
131
|
+
#
|
132
|
+
# @abstract
|
133
|
+
class Base < CF::Base
|
134
|
+
#@private
|
135
|
+
def self.register_type(type_name)
|
136
|
+
Sec.attach_function "#{type_name}GetTypeID", [], CF.find_type(:cftypeid)
|
137
|
+
@@type_map[Sec.send("#{type_name}GetTypeID")] = self
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# If the result is non-zero raises an exception.
|
142
|
+
#
|
143
|
+
# The exception will have the result code as well as a human readable description
|
144
|
+
#
|
145
|
+
# @param [Integer] result the status code to check
|
146
|
+
# @raise [Keychain::Error] is the result is non zero
|
147
|
+
def self.check_osstatus result
|
148
|
+
if result != 0
|
149
|
+
case result
|
150
|
+
when Sec.enum_value(:errSecDuplicateItem)
|
151
|
+
raise Keychain::DuplicateItemError.new(result)
|
152
|
+
when Sec.enum_value(:errCancelled)
|
153
|
+
raise Keychain::UserCancelledError.new(result)
|
154
|
+
when Sec.enum_value(:errSecAuthFailed)
|
155
|
+
raise Keychain::AuthFailedError.new(result)
|
156
|
+
when Sec.enum_value(:errSecNoSuchKeychain)
|
157
|
+
raise Keychain::NoSuchKeychainError.new(result)
|
158
|
+
else
|
159
|
+
raise Keychain::Error.new(result)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
|
165
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Keychain::Item do
|
4
|
+
before(:each) do
|
5
|
+
@keychain = Keychain.create(File.join(Dir.tmpdir, "keychain_spec_#{Time.now.to_i}_#{Time.now.usec}_#{rand(1000)}.keychain"), 'pass')
|
6
|
+
@keychain.generic_passwords.create :service => 'some-service', :account => 'some-account', :password => 'some-password'
|
7
|
+
end
|
8
|
+
|
9
|
+
after(:each) do
|
10
|
+
@keychain.delete
|
11
|
+
end
|
12
|
+
|
13
|
+
def find_item
|
14
|
+
@keychain.generic_passwords.where(:service => 'some-service').first
|
15
|
+
end
|
16
|
+
|
17
|
+
subject {find_item}
|
18
|
+
|
19
|
+
describe 'keychain' do
|
20
|
+
it 'should return the keychain containing the item' do
|
21
|
+
subject.keychain.should == @keychain
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe 'password' do
|
26
|
+
it 'should retrieve the password' do
|
27
|
+
subject.password.should == 'some-password'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe 'service' do
|
32
|
+
it 'should retrieve the service' do
|
33
|
+
subject.service.should == 'some-service'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe 'account' do
|
38
|
+
it 'should retrieve the account' do
|
39
|
+
subject.account.should == 'some-account'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe 'created_at' do
|
44
|
+
it 'should retrieve the item creation date' do
|
45
|
+
subject.created_at.should be_within(2).of(Time.now)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe 'save' do
|
50
|
+
it 'should update attributes and password' do
|
51
|
+
subject.password = 'new-password'
|
52
|
+
subject.account = 'new-account'
|
53
|
+
subject.save!
|
54
|
+
|
55
|
+
fresh = find_item
|
56
|
+
fresh.password.should == 'new-password'
|
57
|
+
fresh.account.should == 'new-account'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe 'delete' do
|
62
|
+
it 'should remove the item from the keychain' do
|
63
|
+
subject.delete
|
64
|
+
find_item.should be_nil
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,220 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Keychain do
|
4
|
+
describe 'default' do
|
5
|
+
it "should return the login keychain" do
|
6
|
+
Keychain.default.path.should == File.expand_path(File.join(ENV['HOME'], 'Library','Keychains', 'login.keychain'))
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
describe 'open' do
|
11
|
+
it 'should create a keychain reference to a path' do
|
12
|
+
keychain = Keychain.open(File.join(ENV['HOME'], 'Library','Keychains', 'login.keychain'))
|
13
|
+
keychain.path.should == Keychain.default.path
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe 'new' do
|
18
|
+
it 'should create the keychain' do
|
19
|
+
begin
|
20
|
+
keychain = Keychain.create(File.join(Dir.tmpdir, "other_keychain_spec_#{Time.now.to_i}_#{Time.now.usec}_#{rand(1000)}.keychain"),
|
21
|
+
'password');
|
22
|
+
File.exists?(keychain.path).should be_true
|
23
|
+
ensure
|
24
|
+
keychain.delete
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe 'exists?' do
|
30
|
+
context 'the keychain exists' do
|
31
|
+
it 'should return true' do
|
32
|
+
Keychain.default.exists?.should be_true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'the keychain does not exist' do
|
37
|
+
it 'should return false' do
|
38
|
+
k = Keychain.open('/some/path/that/does/not/exist')
|
39
|
+
k.exists?.should be_false
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe 'settings' do
|
45
|
+
before(:all) do
|
46
|
+
@keychain = Keychain.create(File.join(Dir.tmpdir, "keychain_spec_#{Time.now.to_i}_#{Time.now.usec}_#{rand(1000)}.keychain"), 'pass')
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should read/write lock_on_sleep' do
|
50
|
+
@keychain.lock_on_sleep = true
|
51
|
+
@keychain.lock_on_sleep?.should == true
|
52
|
+
@keychain.lock_on_sleep = false
|
53
|
+
@keychain.lock_on_sleep?.should == false
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'should read/write lock_interval' do
|
57
|
+
@keychain.lock_interval = 12345
|
58
|
+
@keychain.lock_interval.should == 12345
|
59
|
+
end
|
60
|
+
|
61
|
+
after(:all) do
|
62
|
+
@keychain.delete
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe 'locking' do
|
67
|
+
context 'with a locked keychain' do
|
68
|
+
before :each do
|
69
|
+
@keychain = Keychain.create(File.join(Dir.tmpdir, "keychain_spec_#{Time.now.to_i}_#{Time.now.usec}_#{rand(1000)}.keychain"), 'pass')
|
70
|
+
@keychain.lock!
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'should raise on invalid password' do
|
74
|
+
expect {@keychain.unlock! 'badpassword'}.to raise_error(Keychain::AuthFailedError)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'should unlock on valid password' do
|
78
|
+
@keychain.unlock! 'pass'
|
79
|
+
@keychain.should_not be_locked
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
shared_examples_for 'item collection' do
|
85
|
+
|
86
|
+
before(:each) do
|
87
|
+
@keychain_1 = Keychain.create(File.join(Dir.tmpdir, "other_keychain_spec_#{Time.now.to_i}_#{Time.now.usec}_#{rand(1000)}.keychain"), 'pass')
|
88
|
+
@keychain_2 = Keychain.create(File.join(Dir.tmpdir, "keychain_2_#{Time.now.to_i}_#{Time.now.usec}_#{rand(1000)}.keychain"), 'pass')
|
89
|
+
@keychain_3 = Keychain.create(File.join(Dir.tmpdir, "keychain_3_#{Time.now.to_i}_#{Time.now.usec}_#{rand(1000)}.keychain"), 'pass')
|
90
|
+
|
91
|
+
add_fixtures
|
92
|
+
end
|
93
|
+
|
94
|
+
after(:each) do
|
95
|
+
@keychain_1.delete
|
96
|
+
@keychain_2.delete
|
97
|
+
@keychain_3.delete
|
98
|
+
end
|
99
|
+
|
100
|
+
describe('create') do
|
101
|
+
it 'should add a password' do
|
102
|
+
item = @keychain_1.send(subject).create(create_arguments)
|
103
|
+
item.should be_a(Keychain::Item)
|
104
|
+
item.klass.should == expected_kind
|
105
|
+
item.password.should == 'some-password'
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'should be findable' do
|
109
|
+
@keychain_1.send(subject).create(create_arguments)
|
110
|
+
item = @keychain_1.send(subject).where(search_for_created_arguments).first
|
111
|
+
item.password.should == 'some-password'
|
112
|
+
end
|
113
|
+
|
114
|
+
context 'when a duplicate item exists' do
|
115
|
+
before(:each) do
|
116
|
+
@keychain_1.send(subject).create(create_arguments)
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'should raise Keychain::DuplicateItemError' do
|
120
|
+
expect {@keychain_1.send(subject).create(create_arguments)}.to raise_error(Keychain::DuplicateItemError)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
describe('all') do
|
126
|
+
|
127
|
+
context 'when the keychain does not contains a matching item' do
|
128
|
+
it 'should return []' do
|
129
|
+
@keychain_1.send(subject).where(search_arguments_with_no_results).all.should == []
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'should return an array of results' do
|
134
|
+
item = @keychain_1.send(subject).where(search_arguments).all.first
|
135
|
+
item.should be_a(Keychain::Item)
|
136
|
+
item.password.should == 'some-password-1'
|
137
|
+
end
|
138
|
+
|
139
|
+
context 'searching all keychains' do
|
140
|
+
context 'when the keychain does contains matching items' do
|
141
|
+
it 'should return all of them' do
|
142
|
+
Keychain.send(subject).where(search_arguments_with_multiple_results).all.length.should == 3
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
context 'when the limit is option is set' do
|
147
|
+
it 'should limit the return set' do
|
148
|
+
Keychain.send(subject).where(search_arguments_with_multiple_results).limit(1).all.length.should == 1
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
context 'when a subset of keychains is specified' do
|
153
|
+
it 'should return items from those keychains' do
|
154
|
+
Keychain.send(subject).where(search_arguments_with_multiple_results).in(@keychain_1, @keychain_2).all.length.should == 2
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
describe 'first' do
|
160
|
+
context 'when the keychain does not contain a matching item' do
|
161
|
+
it 'should return nil' do
|
162
|
+
item = @keychain_1.send(subject).where(search_arguments_with_no_results).first.should be_nil
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
context 'when the keychain does contain a matching item' do
|
167
|
+
it 'should find it' do
|
168
|
+
item = @keychain_1.send(subject).where(search_arguments).first
|
169
|
+
item.should be_a(Keychain::Item)
|
170
|
+
item.password.should == 'some-password-1'
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
context 'when a different keychain contains a matching item' do
|
175
|
+
before(:each) do
|
176
|
+
item = @keychain_1.send(subject).create(create_arguments)
|
177
|
+
end
|
178
|
+
|
179
|
+
it 'should not find it' do
|
180
|
+
@keychain_2.send(subject).where(search_arguments).first.should be_nil
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
describe 'generic_passwords' do
|
187
|
+
subject { :generic_passwords }
|
188
|
+
let(:create_arguments){{:service => 'aservice', :account => 'anaccount-foo', :password =>'some-password'}}
|
189
|
+
let(:search_for_created_arguments){{:service => 'aservice'}}
|
190
|
+
|
191
|
+
let(:search_arguments){{:service => 'aservice-1'}}
|
192
|
+
let(:search_arguments_with_no_results){{:service => 'doesntexist'}}
|
193
|
+
let(:search_arguments_with_multiple_results){{:account => 'anaccount'}}
|
194
|
+
let(:expected_kind) {'genp'}
|
195
|
+
|
196
|
+
def add_fixtures
|
197
|
+
@keychain_1.generic_passwords.create(:service => 'aservice-1', :account => 'anaccount', :password => 'some-password-1')
|
198
|
+
@keychain_2.generic_passwords.create(:service => 'aservice-2', :account => 'anaccount', :password => 'some-password-2')
|
199
|
+
@keychain_3.generic_passwords.create(:service => 'aservice-2', :account => 'anaccount', :password => 'some-password-3')
|
200
|
+
end
|
201
|
+
it_behaves_like 'item collection'
|
202
|
+
end
|
203
|
+
|
204
|
+
describe 'internet_passwords' do
|
205
|
+
subject { :internet_passwords }
|
206
|
+
let(:create_arguments){{:server => 'dressipi.example.com', :account => 'anaccount-foo', :password =>'some-password', :protocol => Keychain::Protocols::HTTP}}
|
207
|
+
let(:search_for_created_arguments){{:server => 'dressipi.example.com', :protocol => Keychain::Protocols::HTTP}}
|
208
|
+
let(:search_arguments){{:server => 'dressipi-1.example.com', :protocol => Keychain::Protocols::HTTP}}
|
209
|
+
let(:search_arguments_with_no_results){{:server => 'dressipi.example.com'}}
|
210
|
+
let(:search_arguments_with_multiple_results){{:account => 'anaccount'}}
|
211
|
+
let(:expected_kind) {'inet'}
|
212
|
+
|
213
|
+
def add_fixtures
|
214
|
+
@keychain_1.internet_passwords.create(:server => 'dressipi-1.example.com', :account => 'anaccount', :password => 'some-password-1', :protocol => Keychain::Protocols::HTTP)
|
215
|
+
@keychain_2.internet_passwords.create(:server => 'dressipi-2.example.com', :account => 'anaccount', :password => 'some-password-2', :protocol => Keychain::Protocols::HTTP)
|
216
|
+
@keychain_3.internet_passwords.create(:server => 'dressipi-3.example.com', :account => 'anaccount', :password => 'some-password-3', :protocol => Keychain::Protocols::HTTP)
|
217
|
+
end
|
218
|
+
it_behaves_like 'item collection'
|
219
|
+
end
|
220
|
+
end
|