diff --git a/Rakefile b/Rakefile index 8e2175c..5c12864 100644 --- a/Rakefile +++ b/Rakefile @@ -1,5 +1,15 @@ +require 'rubygems/package_task' require 'rspec/core/rake_task' RSpec::Core::RakeTask.new('spec') -task :default => :spec \ No newline at end of file +task :default => :spec + +# Read the spec file +spec = Gem::Specification.load('keychain.gemspec') + +# Setup gem package task +Gem::PackageTask.new(spec) do |pkg| + pkg.package_dir = 'pkg' + pkg.need_tar = false +end diff --git a/keychain.gemspec b/keychain.gemspec index 2df6f38..585438a 100644 --- a/keychain.gemspec +++ b/keychain.gemspec @@ -21,12 +21,11 @@ Gem::Specification.new do |s| s.summary = %q{Ruby wrapper for OS X's keychain} s.required_ruby_version = '>= 1.9.2' - s.add_runtime_dependency "ffi" + s.add_runtime_dependency "ffi", ">= 1.9.18" s.add_runtime_dependency "corefoundation", "~>0.2.0" - s.add_development_dependency "rspec", '~> 3.3', '>= 3.3.0' - s.add_development_dependency "rake", '~> 10.4', '>= 10.4.2' - s.add_development_dependency "yard", '~> 0.8.7' - s.add_development_dependency "redcarpet", '~>3.2', '>= 3.2.3' - + s.add_development_dependency "rspec", '>= 3.6.0' + s.add_development_dependency "rake", '>= 12.0.0' + s.add_development_dependency "yard", '>= 0.9.9' + s.add_development_dependency "redcarpet", '>= 3.4.0' end diff --git a/lib/keychain.rb b/lib/keychain.rb index cd3ad49..fe0be7b 100644 --- a/lib/keychain.rb +++ b/lib/keychain.rb @@ -1,7 +1,9 @@ require 'ffi' +require 'set' require 'corefoundation' require 'keychain/sec' require 'keychain/access' +require 'keychain/acl' require 'keychain/trusted_application' require 'keychain/keychain' require 'keychain/error' diff --git a/lib/keychain/access.rb b/lib/keychain/access.rb index a7d4446..88e1e67 100644 --- a/lib/keychain/access.rb +++ b/lib/keychain/access.rb @@ -1,5 +1,49 @@ +module Sec + attach_function 'SecAccessCreate', [:pointer, :pointer, :pointer], :osstatus + attach_function 'SecKeychainItemCopyAccess', [:pointer, :pointer], :osstatus + attach_function 'SecAccessCopyACLList', [:pointer, :pointer], :osstatus + attach_function 'SecAccessCopyMatchingACLList', [:pointer, :pointer], :pointer + attach_function 'SecKeychainItemSetAccess', [:pointer, :pointer], :osstatus +end + module Keychain + module AccessMixin + def access + access_buffer = FFI::MemoryPointer.new(:pointer) + status = Sec.SecKeychainItemCopyAccess(self, access_buffer) + Sec.check_osstatus status + Access.new(access_buffer.read_pointer) + end + + def access=(value) + status = Sec.SecKeychainItemSetAccess(self, value.to_cf) + Sec.check_osstatus status + end + end + class Access < Sec::Base register_type 'SecAccess' + + def self.create(description, trusted_apps = []) + access_buffer = FFI::MemoryPointer.new(:pointer) + status = Sec.SecAccessCreate(description.to_cf, trusted_apps.to_cf, access_buffer) + Sec.check_osstatus status + self.new(access_buffer.read_pointer) + end + + def acls + acl_list_ref = FFI::MemoryPointer.new(:pointer) + status = Sec.SecAccessCopyACLList(self, acl_list_ref) + Sec.check_osstatus status + array_ref = CF::Base.typecast(acl_list_ref.read_pointer).release_on_gc + array_ref.to_ruby + end + + def matching_acls(authorization_tag) + access_buffer = FFI::MemoryPointer.new(:pointer) + authorization_tag_cf = CF::Base.typecast(authorization_tag) + acl_list_ref = Sec.SecAccessCopyMatchingACLList(self, authorization_tag_cf) + acl_list_ref.null? ? Array.new : CF::Base.typecast(acl_list_ref) + end end end \ No newline at end of file diff --git a/lib/keychain/acl.rb b/lib/keychain/acl.rb new file mode 100644 index 0000000..8c76fff --- /dev/null +++ b/lib/keychain/acl.rb @@ -0,0 +1,74 @@ +module Sec + attach_function 'SecACLCopyContents', [:pointer, :pointer, :pointer, :pointer], :osstatus + attach_function 'SecACLRemove', [:pointer], :osstatus + attach_function 'SecACLCopyAuthorizations', [:pointer], :pointer + attach_function 'SecACLSetContents', [:pointer, :pointer, :pointer, :pointer], :osstatus + attach_variable 'kSecACLAuthorizationAny', :pointer + attach_variable 'kSecACLAuthorizationLogin', :pointer + attach_variable 'kSecACLAuthorizationGenKey', :pointer + attach_variable 'kSecACLAuthorizationDelete', :pointer + attach_variable 'kSecACLAuthorizationExportWrapped', :pointer + attach_variable 'kSecACLAuthorizationExportClear', :pointer + attach_variable 'kSecACLAuthorizationImportWrapped', :pointer + attach_variable 'kSecACLAuthorizationImportClear', :pointer + attach_variable 'kSecACLAuthorizationSign', :pointer + attach_variable 'kSecACLAuthorizationEncrypt', :pointer + attach_variable 'kSecACLAuthorizationDecrypt', :pointer + attach_variable 'kSecACLAuthorizationMAC', :pointer + attach_variable 'kSecACLAuthorizationDerive', :pointer +end + +module Keychain + class Acl < Sec::Base + register_type 'SecACL' + + attr_reader :applications, :description, :prompt + + def initialize(ptr) + super(ptr) + + applications_ref = FFI::MemoryPointer.new(:pointer) + description_ref = FFI::MemoryPointer.new(:pointer) + prompt_ref = FFI::MemoryPointer.new(:pointer) + status = Sec.SecACLCopyContents(self, applications_ref, description_ref, prompt_ref) + Sec.check_osstatus(status) + + unless applications_ref.read_pointer.null? + applications_cf = CF::Base.typecast(applications_ref.read_pointer).release_on_gc + @applications = applications_cf.to_ruby + applications_cf.release + end + + unless description_ref.read_pointer.null? + description_cf = CF::Base.typecast(description_ref.read_pointer).release_on_gc + @description = description_cf.to_ruby + end + + unless prompt_ref.read_pointer.null? + prompt_cf = CF::Base.typecast(prompt_ref.read_pointer).release_on_gc + @prompt = prompt_cf.to_ruby + end + end + + def authorizations + authorizations_ref = Sec.SecACLCopyAuthorizations(self) + authorizations_cf = CF::Base.typecast(authorizations_ref) + result = authorizations_cf.to_ruby + authorizations_cf.release + result + end + + def delete + status = Sec.SecACLRemove(self) + Sec.check_osstatus(status) + end + + def applications=(apps) + apps_cf = apps ? apps.to_cf : nil + description_cf = apps ? self.description.to_cf : nil + prompt_cf = apps ? self.prompt.to_cf : nil + status = Sec.SecACLSetContents(self, apps_cf, description_cf, prompt_cf) + Sec.check_osstatus(status) + end + end +end \ No newline at end of file diff --git a/lib/keychain/certificate.rb b/lib/keychain/certificate.rb index 2ab864a..c73fc27 100644 --- a/lib/keychain/certificate.rb +++ b/lib/keychain/certificate.rb @@ -5,6 +5,7 @@ module Sec attach_function 'SecCertificateCopyPublicKey', [:pointer, :pointer], :osstatus attach_function 'SecCertificateCopyData', [:pointer], :pointer + attach_function 'SecCertificateCopyValues', [:pointer, :pointer, :pointer], :pointer attach_variable 'kSecAttrCertificateType', :pointer attach_variable 'kSecAttrCertificateEncoding', :pointer @@ -13,6 +14,124 @@ module Sec attach_variable 'kSecAttrSerialNumber', :pointer attach_variable 'kSecAttrSubjectKeyID', :pointer attach_variable 'kSecAttrPublicKeyHash', :pointer + + # OIDS + attach_variable 'kSecOIDADC_CERT_POLICY', :pointer + attach_variable 'kSecOIDAPPLE_CERT_POLICY', :pointer + attach_variable 'kSecOIDAPPLE_EKU_CODE_SIGNING', :pointer + attach_variable 'kSecOIDAPPLE_EKU_CODE_SIGNING_DEV', :pointer + attach_variable 'kSecOIDAPPLE_EKU_ICHAT_ENCRYPTION', :pointer + attach_variable 'kSecOIDAPPLE_EKU_ICHAT_SIGNING', :pointer + attach_variable 'kSecOIDAPPLE_EKU_RESOURCE_SIGNING', :pointer + attach_variable 'kSecOIDAPPLE_EKU_SYSTEM_IDENTITY', :pointer + attach_variable 'kSecOIDAPPLE_EXTENSION', :pointer + attach_variable 'kSecOIDAPPLE_EXTENSION_ADC_APPLE_SIGNING', :pointer + attach_variable 'kSecOIDAPPLE_EXTENSION_ADC_DEV_SIGNING', :pointer + attach_variable 'kSecOIDAPPLE_EXTENSION_APPLE_SIGNING', :pointer + attach_variable 'kSecOIDAPPLE_EXTENSION_CODE_SIGNING', :pointer + attach_variable 'kSecOIDAuthorityInfoAccess', :pointer + attach_variable 'kSecOIDAuthorityKeyIdentifier', :pointer + attach_variable 'kSecOIDBasicConstraints', :pointer + attach_variable 'kSecOIDBiometricInfo', :pointer + attach_variable 'kSecOIDCSSMKeyStruct', :pointer + attach_variable 'kSecOIDCertIssuer', :pointer + attach_variable 'kSecOIDCertificatePolicies', :pointer + attach_variable 'kSecOIDClientAuth', :pointer + attach_variable 'kSecOIDCollectiveStateProvinceName', :pointer + attach_variable 'kSecOIDCollectiveStreetAddress', :pointer + attach_variable 'kSecOIDCommonName', :pointer + attach_variable 'kSecOIDCountryName', :pointer + attach_variable 'kSecOIDCrlDistributionPoints', :pointer + attach_variable 'kSecOIDCrlNumber', :pointer + attach_variable 'kSecOIDCrlReason', :pointer + attach_variable 'kSecOIDDOTMAC_CERT_EMAIL_ENCRYPT', :pointer + attach_variable 'kSecOIDDOTMAC_CERT_EMAIL_SIGN', :pointer + attach_variable 'kSecOIDDOTMAC_CERT_EXTENSION', :pointer + attach_variable 'kSecOIDDOTMAC_CERT_IDENTITY', :pointer + attach_variable 'kSecOIDDOTMAC_CERT_POLICY', :pointer + attach_variable 'kSecOIDDeltaCrlIndicator', :pointer + attach_variable 'kSecOIDDescription', :pointer + attach_variable 'kSecOIDEKU_IPSec', :pointer + attach_variable 'kSecOIDEmailAddress', :pointer + attach_variable 'kSecOIDEmailProtection', :pointer + attach_variable 'kSecOIDExtendedKeyUsage', :pointer + attach_variable 'kSecOIDExtendedKeyUsageAny', :pointer + attach_variable 'kSecOIDExtendedUseCodeSigning', :pointer + attach_variable 'kSecOIDGivenName', :pointer + attach_variable 'kSecOIDHoldInstructionCode', :pointer + attach_variable 'kSecOIDInvalidityDate', :pointer + attach_variable 'kSecOIDIssuerAltName', :pointer + attach_variable 'kSecOIDIssuingDistributionPoint', :pointer + attach_variable 'kSecOIDIssuingDistributionPoints', :pointer + attach_variable 'kSecOIDKERBv5_PKINIT_KP_CLIENT_AUTH', :pointer + attach_variable 'kSecOIDKERBv5_PKINIT_KP_KDC', :pointer + attach_variable 'kSecOIDKeyUsage', :pointer + attach_variable 'kSecOIDLocalityName', :pointer + attach_variable 'kSecOIDMS_NTPrincipalName', :pointer + attach_variable 'kSecOIDMicrosoftSGC', :pointer + attach_variable 'kSecOIDNameConstraints', :pointer + attach_variable 'kSecOIDNetscapeCertSequence', :pointer + attach_variable 'kSecOIDNetscapeCertType', :pointer + attach_variable 'kSecOIDNetscapeSGC', :pointer + attach_variable 'kSecOIDOCSPSigning', :pointer + attach_variable 'kSecOIDOrganizationName', :pointer + attach_variable 'kSecOIDOrganizationalUnitName', :pointer + attach_variable 'kSecOIDPolicyConstraints', :pointer + attach_variable 'kSecOIDPolicyMappings', :pointer + attach_variable 'kSecOIDPrivateKeyUsagePeriod', :pointer + attach_variable 'kSecOIDQC_Statements', :pointer + attach_variable 'kSecOIDSerialNumber', :pointer + attach_variable 'kSecOIDServerAuth', :pointer + attach_variable 'kSecOIDStateProvinceName', :pointer + attach_variable 'kSecOIDStreetAddress', :pointer + attach_variable 'kSecOIDSubjectAltName', :pointer + attach_variable 'kSecOIDSubjectDirectoryAttributes', :pointer + attach_variable 'kSecOIDSubjectEmailAddress', :pointer + attach_variable 'kSecOIDSubjectInfoAccess', :pointer + attach_variable 'kSecOIDSubjectKeyIdentifier', :pointer + attach_variable 'kSecOIDSubjectPicture', :pointer + attach_variable 'kSecOIDSubjectSignatureBitmap', :pointer + attach_variable 'kSecOIDSurname', :pointer + attach_variable 'kSecOIDTimeStamping', :pointer + attach_variable 'kSecOIDTitle', :pointer + attach_variable 'kSecOIDUseExemptions', :pointer + attach_variable 'kSecOIDX509V1CertificateIssuerUniqueId', :pointer + attach_variable 'kSecOIDX509V1CertificateSubjectUniqueId', :pointer + attach_variable 'kSecOIDX509V1IssuerName', :pointer + attach_variable 'kSecOIDX509V1IssuerNameCStruct', :pointer + attach_variable 'kSecOIDX509V1IssuerNameLDAP', :pointer + attach_variable 'kSecOIDX509V1IssuerNameStd', :pointer + attach_variable 'kSecOIDX509V1SerialNumber', :pointer + attach_variable 'kSecOIDX509V1Signature', :pointer + attach_variable 'kSecOIDX509V1SignatureAlgorithm', :pointer + attach_variable 'kSecOIDX509V1SignatureAlgorithmParameters', :pointer + attach_variable 'kSecOIDX509V1SignatureAlgorithmTBS', :pointer + attach_variable 'kSecOIDX509V1SignatureCStruct', :pointer + attach_variable 'kSecOIDX509V1SignatureStruct', :pointer + attach_variable 'kSecOIDX509V1SubjectName', :pointer + attach_variable 'kSecOIDX509V1SubjectNameCStruct', :pointer + attach_variable 'kSecOIDX509V1SubjectNameLDAP', :pointer + attach_variable 'kSecOIDX509V1SubjectNameStd', :pointer + attach_variable 'kSecOIDX509V1SubjectPublicKey', :pointer + attach_variable 'kSecOIDX509V1SubjectPublicKeyAlgorithm', :pointer + attach_variable 'kSecOIDX509V1SubjectPublicKeyAlgorithmParameters', :pointer + attach_variable 'kSecOIDX509V1SubjectPublicKeyCStruct', :pointer + attach_variable 'kSecOIDX509V1ValidityNotAfter', :pointer + attach_variable 'kSecOIDX509V1ValidityNotBefore', :pointer + attach_variable 'kSecOIDX509V1Version', :pointer + attach_variable 'kSecOIDX509V3Certificate', :pointer + attach_variable 'kSecOIDX509V3CertificateCStruct', :pointer + attach_variable 'kSecOIDX509V3CertificateExtensionCStruct', :pointer + attach_variable 'kSecOIDX509V3CertificateExtensionCritical', :pointer + attach_variable 'kSecOIDX509V3CertificateExtensionId', :pointer + attach_variable 'kSecOIDX509V3CertificateExtensionStruct', :pointer + attach_variable 'kSecOIDX509V3CertificateExtensionType', :pointer + attach_variable 'kSecOIDX509V3CertificateExtensionValue', :pointer + attach_variable 'kSecOIDX509V3CertificateExtensionsCStruct', :pointer + attach_variable 'kSecOIDX509V3CertificateExtensionsStruct', :pointer + attach_variable 'kSecOIDX509V3CertificateNumberOfExtensions', :pointer + attach_variable 'kSecOIDX509V3SignedCertificate', :pointer + attach_variable 'kSecOIDX509V3SignedCertificateCStruct', :pointer end module Keychain @@ -20,15 +139,136 @@ class Certificate < Sec::Base register_type 'SecCertificate' ATTR_MAP = {CF::Base.typecast(Sec::kSecAttrAccessGroup) => :access_group, + CF::Base.typecast(Sec::kSecAttrAccessible) => :accessible, CF::Base.typecast(Sec::kSecAttrCertificateType) => :certificate_type, CF::Base.typecast(Sec::kSecAttrCertificateEncoding) => :certificate_encoding, CF::Base.typecast(Sec::kSecAttrLabel) => :label, CF::Base.typecast(Sec::kSecAttrSubject) => :subject, CF::Base.typecast(Sec::kSecAttrIssuer) => :issuer, CF::Base.typecast(Sec::kSecAttrSerialNumber) => :serial_number, + CF::Base.typecast(Sec::kSecAttrSynchronizable) => :synchronizable, CF::Base.typecast(Sec::kSecAttrSubjectKeyID) => :subject_key_id, CF::Base.typecast(Sec::kSecAttrPublicKeyHash) => :public_key_hash} + ATTR_UPDATABLE = Set.new(ATTR_MAP.values) + + OID_MAP = {CF::Base.typecast(Sec::kSecOIDADC_CERT_POLICY) => 'ADC_CERT_POLICY', + CF::Base.typecast(Sec::kSecOIDAPPLE_CERT_POLICY) => 'APPLE_CERT_POLICY', + CF::Base.typecast(Sec::kSecOIDAPPLE_EKU_CODE_SIGNING) => 'APPLE_EKU_CODE_SIGNING', + CF::Base.typecast(Sec::kSecOIDAPPLE_EKU_CODE_SIGNING_DEV) => 'APPLE_EKU_CODE_SIGNING_DEV', + CF::Base.typecast(Sec::kSecOIDAPPLE_EKU_ICHAT_ENCRYPTION) => 'APPLE_EKU_ICHAT_ENCRYPTION', + CF::Base.typecast(Sec::kSecOIDAPPLE_EKU_ICHAT_SIGNING) => 'APPLE_EKU_ICHAT_SIGNING', + CF::Base.typecast(Sec::kSecOIDAPPLE_EKU_RESOURCE_SIGNING) => 'APPLE_EKU_RESOURCE_SIGNING', + CF::Base.typecast(Sec::kSecOIDAPPLE_EKU_SYSTEM_IDENTITY) => 'APPLE_EKU_SYSTEM_IDENTITY', + CF::Base.typecast(Sec::kSecOIDAPPLE_EXTENSION) => 'APPLE_EXTENSION', + CF::Base.typecast(Sec::kSecOIDAPPLE_EXTENSION_ADC_APPLE_SIGNING) => 'APPLE_EXTENSION_ADC_APPLE_SIGNING', + CF::Base.typecast(Sec::kSecOIDAPPLE_EXTENSION_ADC_DEV_SIGNING) => 'APPLE_EXTENSION_ADC_DEV_SIGNING', + CF::Base.typecast(Sec::kSecOIDAPPLE_EXTENSION_APPLE_SIGNING) => 'APPLE_EXTENSION_APPLE_SIGNING', + CF::Base.typecast(Sec::kSecOIDAPPLE_EXTENSION_CODE_SIGNING) => 'APPLE_EXTENSION_CODE_SIGNING', + CF::Base.typecast(Sec::kSecOIDAuthorityInfoAccess) => 'AuthorityInfoAccess', + CF::Base.typecast(Sec::kSecOIDAuthorityKeyIdentifier) => 'AuthorityKeyIdentifier', + CF::Base.typecast(Sec::kSecOIDBasicConstraints) => 'BasicConstraints', + CF::Base.typecast(Sec::kSecOIDBiometricInfo) => 'BiometricInfo', + CF::Base.typecast(Sec::kSecOIDCSSMKeyStruct) => 'CSSMKeyStruct', + CF::Base.typecast(Sec::kSecOIDCertIssuer) => 'CertIssuer', + CF::Base.typecast(Sec::kSecOIDCertificatePolicies) => 'CertificatePolicies', + CF::Base.typecast(Sec::kSecOIDClientAuth) => 'ClientAuth', + CF::Base.typecast(Sec::kSecOIDCollectiveStateProvinceName) => 'CollectiveStateProvinceName', + CF::Base.typecast(Sec::kSecOIDCollectiveStreetAddress) => 'CollectiveStreetAddress', + CF::Base.typecast(Sec::kSecOIDCommonName) => 'CommonName', + CF::Base.typecast(Sec::kSecOIDCountryName) => 'CountryName', + CF::Base.typecast(Sec::kSecOIDCrlDistributionPoints) => 'CrlDistributionPoints', + CF::Base.typecast(Sec::kSecOIDCrlNumber) => 'CrlNumber', + CF::Base.typecast(Sec::kSecOIDCrlReason) => 'CrlReason', + CF::Base.typecast(Sec::kSecOIDDOTMAC_CERT_EMAIL_ENCRYPT) => 'DOTMAC_CERT_EMAIL_ENCRYPT', + CF::Base.typecast(Sec::kSecOIDDOTMAC_CERT_EMAIL_SIGN) => 'DOTMAC_CERT_EMAIL_SIGN', + CF::Base.typecast(Sec::kSecOIDDOTMAC_CERT_EXTENSION) => 'DOTMAC_CERT_EXTENSION', + CF::Base.typecast(Sec::kSecOIDDOTMAC_CERT_IDENTITY) => 'DOTMAC_CERT_IDENTITY', + CF::Base.typecast(Sec::kSecOIDDOTMAC_CERT_POLICY) => 'DOTMAC_CERT_POLICY', + CF::Base.typecast(Sec::kSecOIDDeltaCrlIndicator) => 'DeltaCrlIndicator', + CF::Base.typecast(Sec::kSecOIDDescription) => 'Description', + CF::Base.typecast(Sec::kSecOIDEKU_IPSec) => 'EKU_IPSec', + CF::Base.typecast(Sec::kSecOIDEmailAddress) => 'EmailAddress', + CF::Base.typecast(Sec::kSecOIDEmailProtection) => 'EmailProtection', + CF::Base.typecast(Sec::kSecOIDExtendedKeyUsage) => 'ExtendedKeyUsage', + CF::Base.typecast(Sec::kSecOIDExtendedKeyUsageAny) => 'ExtendedKeyUsageAny', + CF::Base.typecast(Sec::kSecOIDExtendedUseCodeSigning) => 'ExtendedUseCodeSigning', + CF::Base.typecast(Sec::kSecOIDGivenName) => 'GivenName', + CF::Base.typecast(Sec::kSecOIDHoldInstructionCode) => 'HoldInstructionCode', + CF::Base.typecast(Sec::kSecOIDInvalidityDate) => 'InvalidityDate', + CF::Base.typecast(Sec::kSecOIDIssuerAltName) => 'IssuerAltName', + CF::Base.typecast(Sec::kSecOIDIssuingDistributionPoint) => 'IssuingDistributionPoint', + CF::Base.typecast(Sec::kSecOIDIssuingDistributionPoints) => 'IssuingDistributionPoints', + CF::Base.typecast(Sec::kSecOIDKERBv5_PKINIT_KP_CLIENT_AUTH) => 'KERBv5_PKINIT_KP_CLIENT_AUTH', + CF::Base.typecast(Sec::kSecOIDKERBv5_PKINIT_KP_KDC) => 'KERBv5_PKINIT_KP_KDC', + CF::Base.typecast(Sec::kSecOIDKeyUsage) => 'KeyUsage', + CF::Base.typecast(Sec::kSecOIDLocalityName) => 'LocalityName', + CF::Base.typecast(Sec::kSecOIDMS_NTPrincipalName) => 'MS_NTPrincipalName', + CF::Base.typecast(Sec::kSecOIDMicrosoftSGC) => 'MicrosoftSGC', + CF::Base.typecast(Sec::kSecOIDNameConstraints) => 'NameConstraints', + CF::Base.typecast(Sec::kSecOIDNetscapeCertSequence) => 'NetscapeCertSequence', + CF::Base.typecast(Sec::kSecOIDNetscapeCertType) => 'NetscapeCertType', + CF::Base.typecast(Sec::kSecOIDNetscapeSGC) => 'NetscapeSGC', + CF::Base.typecast(Sec::kSecOIDOCSPSigning) => 'OCSPSigning', + CF::Base.typecast(Sec::kSecOIDOrganizationName) => 'OrganizationName', + CF::Base.typecast(Sec::kSecOIDOrganizationalUnitName) => 'OrganizationalUnitName', + CF::Base.typecast(Sec::kSecOIDPolicyConstraints) => 'PolicyConstraints', + CF::Base.typecast(Sec::kSecOIDPolicyMappings) => 'PolicyMappings', + CF::Base.typecast(Sec::kSecOIDPrivateKeyUsagePeriod) => 'PrivateKeyUsagePeriod', + CF::Base.typecast(Sec::kSecOIDQC_Statements) => 'QC_Statements', + CF::Base.typecast(Sec::kSecOIDSerialNumber) => 'SerialNumber', + CF::Base.typecast(Sec::kSecOIDServerAuth) => 'ServerAuth', + CF::Base.typecast(Sec::kSecOIDStateProvinceName) => 'StateProvinceName', + CF::Base.typecast(Sec::kSecOIDStreetAddress) => 'StreetAddress', + CF::Base.typecast(Sec::kSecOIDSubjectAltName) => 'SubjectAltName', + CF::Base.typecast(Sec::kSecOIDSubjectDirectoryAttributes) => 'SubjectDirectoryAttributes', + CF::Base.typecast(Sec::kSecOIDSubjectEmailAddress) => 'SubjectEmailAddress', + CF::Base.typecast(Sec::kSecOIDSubjectInfoAccess) => 'SubjectInfoAccess', + CF::Base.typecast(Sec::kSecOIDSubjectKeyIdentifier) => 'SubjectKeyIdentifier', + CF::Base.typecast(Sec::kSecOIDSubjectPicture) => 'SubjectPicture', + CF::Base.typecast(Sec::kSecOIDSubjectSignatureBitmap) => 'SubjectSignatureBitmap', + CF::Base.typecast(Sec::kSecOIDSurname) => 'Surname', + CF::Base.typecast(Sec::kSecOIDTimeStamping) => 'TimeStamping', + CF::Base.typecast(Sec::kSecOIDTitle) => 'Title', + CF::Base.typecast(Sec::kSecOIDUseExemptions) => 'UseExemptions', + CF::Base.typecast(Sec::kSecOIDX509V1CertificateIssuerUniqueId) => 'X509V1CertificateIssuerUniqueId', + CF::Base.typecast(Sec::kSecOIDX509V1CertificateSubjectUniqueId) => 'X509V1CertificateSubjectUniqueId', + CF::Base.typecast(Sec::kSecOIDX509V1IssuerName) => 'X509V1IssuerName', + CF::Base.typecast(Sec::kSecOIDX509V1IssuerNameCStruct) => 'X509V1IssuerNameCStruct', + CF::Base.typecast(Sec::kSecOIDX509V1IssuerNameLDAP) => 'X509V1IssuerNameLDAP', + CF::Base.typecast(Sec::kSecOIDX509V1IssuerNameStd) => 'X509V1IssuerNameStd', + CF::Base.typecast(Sec::kSecOIDX509V1SerialNumber) => 'X509V1SerialNumber', + CF::Base.typecast(Sec::kSecOIDX509V1Signature) => 'X509V1Signature', + CF::Base.typecast(Sec::kSecOIDX509V1SignatureAlgorithm) => 'X509V1SignatureAlgorithm', + CF::Base.typecast(Sec::kSecOIDX509V1SignatureAlgorithmParameters) => 'X509V1SignatureAlgorithmParameters', + CF::Base.typecast(Sec::kSecOIDX509V1SignatureAlgorithmTBS) => 'X509V1SignatureAlgorithmTBS', + CF::Base.typecast(Sec::kSecOIDX509V1SignatureCStruct) => 'X509V1SignatureCStruct', + CF::Base.typecast(Sec::kSecOIDX509V1SignatureStruct) => 'X509V1SignatureStruct', + CF::Base.typecast(Sec::kSecOIDX509V1SubjectName) => 'X509V1SubjectName', + CF::Base.typecast(Sec::kSecOIDX509V1SubjectNameCStruct) => 'X509V1SubjectNameCStruct', + CF::Base.typecast(Sec::kSecOIDX509V1SubjectNameLDAP) => 'X509V1SubjectNameLDAP', + CF::Base.typecast(Sec::kSecOIDX509V1SubjectNameStd) => 'X509V1SubjectNameStd', + CF::Base.typecast(Sec::kSecOIDX509V1SubjectPublicKey) => 'X509V1SubjectPublicKey', + CF::Base.typecast(Sec::kSecOIDX509V1SubjectPublicKeyAlgorithm) => 'X509V1SubjectPublicKeyAlgorithm', + CF::Base.typecast(Sec::kSecOIDX509V1SubjectPublicKeyAlgorithmParameters) => 'X509V1SubjectPublicKeyAlgorithmParameters', + CF::Base.typecast(Sec::kSecOIDX509V1SubjectPublicKeyCStruct) => 'X509V1SubjectPublicKeyCStruct', + CF::Base.typecast(Sec::kSecOIDX509V1ValidityNotAfter) => 'X509V1ValidityNotAfter', + CF::Base.typecast(Sec::kSecOIDX509V1ValidityNotBefore) => 'X509V1ValidityNotBefore', + CF::Base.typecast(Sec::kSecOIDX509V1Version) => 'X509V1Version', + CF::Base.typecast(Sec::kSecOIDX509V3Certificate) => 'X509V3Certificate', + CF::Base.typecast(Sec::kSecOIDX509V3CertificateCStruct) => 'X509V3CertificateCStruct', + CF::Base.typecast(Sec::kSecOIDX509V3CertificateExtensionCStruct) => 'X509V3CertificateExtensionCStruct', + CF::Base.typecast(Sec::kSecOIDX509V3CertificateExtensionCritical) => 'X509V3CertificateExtensionCritical', + CF::Base.typecast(Sec::kSecOIDX509V3CertificateExtensionId) => 'X509V3CertificateExtensionId', + CF::Base.typecast(Sec::kSecOIDX509V3CertificateExtensionStruct) => 'X509V3CertificateExtensionStruct', + CF::Base.typecast(Sec::kSecOIDX509V3CertificateExtensionType) => 'X509V3CertificateExtensionType', + CF::Base.typecast(Sec::kSecOIDX509V3CertificateExtensionValue) => 'X509V3CertificateExtensionValue', + CF::Base.typecast(Sec::kSecOIDX509V3CertificateExtensionsCStruct) => 'X509V3CertificateExtensionsCStruct', + CF::Base.typecast(Sec::kSecOIDX509V3CertificateExtensionsStruct) => 'X509V3CertificateExtensionsStruct', + CF::Base.typecast(Sec::kSecOIDX509V3CertificateNumberOfExtensions) => 'X509V3CertificateNumberOfExtensions', + CF::Base.typecast(Sec::kSecOIDX509V3SignedCertificate) => 'X509V3SignedCertificate', + CF::Base.typecast(Sec::kSecOIDX509V3SignedCertificateCStruct) => 'X509V3SignedCertificateCStruct'} + ATTR_MAP[CF::Base.typecast(Sec::kSecAttrAccessible)] = :accessible if defined?(Sec::kSecAttrAccessible) ATTR_MAP[CF::Base.typecast(Sec::kSecAttrAccessControl)] = :access_control if defined?(Sec::kSecAttrAccessControl) @@ -55,5 +295,63 @@ def x509 data.release result end + + def contents + @contents ||= begin + data_ptr = Sec.SecCertificateCopyValues(self, nil, nil) + data = CF::Dictionary.new(data_ptr) + + result = data.inject(Hash.new) do |hash, pair| + # Get human readable key + key = pair.first + human_key = OID_MAP[key] || key.to_ruby + + value = pair.last + + # Map values to ruby hash + human_value = value.to_ruby + + # Store current result + hash[human_key] = human_value + + # Now fix labels + fix_label(human_value, value) + value['value'].each_with_index do |oid_value, index| + fix_label(human_value['value'][index], oid_value) + end + + hash + end + + data.release + result + end + end + + def fix_label(ruby_dictionary, cf_dictionary) + label = cf_dictionary['label'] + return unless label + ruby_dictionary['label'] = OID_MAP[label] || label.to_ruby + end + + def valid? + self.start < Time.now && self.finish > Time.now + end + + def start + hash = self.contents['X509V1ValidityNotBefore'] + to_time(hash['value']) + end + + def finish + hash = self.contents['X509V1ValidityNotAfter'] + to_time(hash['value']) + end + + def to_time(value) + # Returned times are NSDates which use 00:00:00 UTC on 1 January 2001 as the reference time + reference_time = Time.new(2001, 1, 1, 0, 0, 0, 0) + Time.at(value + reference_time.to_i) + end end end \ No newline at end of file diff --git a/lib/keychain/identity.rb b/lib/keychain/identity.rb index 5b9d8dc..198f097 100644 --- a/lib/keychain/identity.rb +++ b/lib/keychain/identity.rb @@ -13,6 +13,7 @@ class Identity < Sec::Base register_type 'SecIdentity' ATTR_MAP = Certificate::ATTR_MAP.merge(Key::ATTR_MAP) + ATTR_UPDATABLE = Certificate::ATTR_UPDATABLE.merge(Key::ATTR_UPDATABLE) INVERSE_ATTR_MAP = ATTR_MAP.invert define_attributes(ATTR_MAP) @@ -47,7 +48,7 @@ def pkcs12(passphrase='') Sec.check_osstatus(status) data = CF::Data.new(data_ptr.read_pointer) - result = OpenSSL::PKCS12.new(data.to_s) + result = OpenSSL::PKCS12.new(data.to_s, passphrase) data.release result end diff --git a/lib/keychain/item.rb b/lib/keychain/item.rb index 161287d..d0be644 100644 --- a/lib/keychain/item.rb +++ b/lib/keychain/item.rb @@ -1,8 +1,5 @@ - module Sec attach_function 'SecKeychainItemDelete', [:pointer], :osstatus - attach_function 'SecItemAdd', [:pointer, :pointer], :osstatus - attach_function 'SecItemUpdate', [:pointer, :pointer], :osstatus attach_function 'SecKeychainItemCopyKeychain', [:pointer, :pointer], :osstatus end @@ -34,6 +31,8 @@ class Item < Sec::Base CF::Base.typecast(Sec::kSecAttrType) => :type, CF::Base.typecast(Sec::kSecClass) => :klass} + ATTR_UPDATABLE = Set.new(ATTR_MAP.values - [:created_at, :updated_at]) + INVERSE_ATTR_MAP = ATTR_MAP.invert define_attributes(ATTR_MAP) @@ -59,14 +58,6 @@ def self.new(attrs_or_pointer) end end - # Removes the item from the associated keychain - # - def delete - status = Sec.SecKeychainItemDelete(self) - Sec.check_osstatus(status) - self - end - # Set a new password for the item # @note The new password is not saved into the keychain until you call {Keychain::Item#save!} # @param [String] value The new value for the password @@ -129,19 +120,6 @@ def create(options) cf_dict = CF::Base.typecast(result.read_pointer) end - def update - status = Sec.SecItemUpdate({Sec::Query::SEARCH_LIST => [self.keychain], - Sec::Query::ITEM_LIST => [self], - Sec::Query::CLASS => klass}.to_cf, build_new_attributes); - Sec.check_osstatus(status) - - result = FFI::MemoryPointer.new :pointer - query = build_refresh_query - status = Sec.SecItemCopyMatching(query, result); - Sec.check_osstatus(status) - cf_dict = CF::Base.typecast(result.read_pointer) - end - def build_create_query options query = CF::Dictionary.mutable query[Sec::Value::DATA] = CF::Data.from_string(@unsaved_password) if @unsaved_password @@ -151,24 +129,8 @@ def build_create_query options query end - def build_refresh_query - query = CF::Dictionary.mutable - query[Sec::Query::SEARCH_LIST] = CF::Array.immutable([self.keychain]) - query[Sec::Query::ITEM_LIST] = CF::Array.immutable([self]) - query[Sec::Query::RETURN_ATTRIBUTES] = CF::Boolean::TRUE - query[Sec::Query::RETURN_REF] = CF::Boolean::TRUE - query[Sec::Query::CLASS] = klass.to_cf - query - end - def build_new_attributes - new_attributes = CF::Dictionary.mutable - @attributes.each do |k,v| - next if k == :created_at || k == :updated_at - next if k == :klass && persisted? - k = self.class::INVERSE_ATTR_MAP[k] - new_attributes[k] = v.to_cf - end + new_attributes = super new_attributes[Sec::Value::DATA] = CF::Data.from_string(@unsaved_password) if @unsaved_password new_attributes end diff --git a/lib/keychain/key.rb b/lib/keychain/key.rb index 719d590..452ca05 100644 --- a/lib/keychain/key.rb +++ b/lib/keychain/key.rb @@ -1,4 +1,6 @@ module Sec + attach_function 'SecKeyCopyPublicKey', [:pointer], :pointer + begin attach_variable 'kSecAttrAccessible', :pointer rescue FFI::NotFoundError #Only available in 10.9 @@ -9,14 +11,22 @@ module Sec rescue FFI::NotFoundError #Only available in 10.10 end - attach_variable 'kSecAttrAccessGroup', :pointer + # Cryptographic Key Attribute Keys attach_variable 'kSecAttrKeyClass', :pointer attach_variable 'kSecAttrApplicationLabel', :pointer - attach_variable 'kSecAttrIsPermanent', :pointer attach_variable 'kSecAttrApplicationTag', :pointer attach_variable 'kSecAttrKeyType', :pointer + attach_variable 'kSecAttrPRF', :pointer + attach_variable 'kSecAttrRounds', :pointer + attach_variable 'kSecAttrSalt', :pointer attach_variable 'kSecAttrKeySizeInBits', :pointer attach_variable 'kSecAttrEffectiveKeySize', :pointer + attach_variable 'kSecAttrTokenID', :pointer + + # Cryptographic Key Usage Attribute Keys + attach_variable 'kSecAttrIsPermanent', :pointer + attach_variable 'kSecAttrIsSensitive', :pointer + attach_variable 'kSecAttrIsExtractable', :pointer attach_variable 'kSecAttrCanEncrypt', :pointer attach_variable 'kSecAttrCanDecrypt', :pointer attach_variable 'kSecAttrCanDerive', :pointer @@ -25,6 +35,11 @@ module Sec attach_variable 'kSecAttrCanWrap', :pointer attach_variable 'kSecAttrCanUnwrap', :pointer + # kSecAttrKeyClass values + attach_variable 'kSecAttrKeyClassPublic', :pointer + attach_variable 'kSecAttrKeyClassPrivate', :pointer + attach_variable 'kSecAttrKeyClassSymmetric', :pointer + enum :SecItemImportExportFlags, [:kSecItemPemArmour, 1] enum :SecExternalFormat, [:kSecFormatUnknown, 0, @@ -70,16 +85,27 @@ class SecItemImportExportKeyParameters < FFI::Struct module Keychain class Key < Sec::Base register_type 'SecKey' + include AccessMixin + + KEY_CLASS_PUBLIC = CF::Base.typecast(Sec::kSecAttrKeyClassPublic) + KEY_CLASS_PRIVATE = CF::Base.typecast(Sec::kSecAttrKeyClassPrivate) + KEY_CLASS_SYMMETRIC = CF::Base.typecast(Sec::kSecAttrKeyClassSymmetric) - ATTR_MAP = {CF::Base.typecast(Sec::kSecAttrAccessGroup) => :access_group, - CF::Base.typecast(Sec::kSecAttrKeyClass) => :key_class, + ATTR_MAP = {CF::Base.typecast(Sec::kSecAttrKeyClass) => :klass, CF::Base.typecast(Sec::kSecAttrLabel) => :label, CF::Base.typecast(Sec::kSecAttrApplicationLabel) => :application_label, - CF::Base.typecast(Sec::kSecAttrIsPermanent) => :is_permanent, CF::Base.typecast(Sec::kSecAttrApplicationTag) => :application_tag, CF::Base.typecast(Sec::kSecAttrKeyType) => :key_type, - CF::Base.typecast(Sec::kSecAttrKeySizeInBits) => :size_in_bites, + CF::Base.typecast(Sec::kSecAttrPRF) => :prf, + CF::Base.typecast(Sec::kSecAttrRounds) => :rounds, + CF::Base.typecast(Sec::kSecAttrSalt) => :salt, + CF::Base.typecast(Sec::kSecAttrKeySizeInBits) => :size_in_bits, CF::Base.typecast(Sec::kSecAttrEffectiveKeySize) => :effective_key_size, + CF::Base.typecast(Sec::kSecAttrTokenID) => :token_id, + + CF::Base.typecast(Sec::kSecAttrIsPermanent) => :is_permanent, + CF::Base.typecast(Sec::kSecAttrIsSensitive) => :is_sensitive, + CF::Base.typecast(Sec::kSecAttrIsExtractable) => :is_extractable, CF::Base.typecast(Sec::kSecAttrCanEncrypt) => :can_encrypt, CF::Base.typecast(Sec::kSecAttrCanDecrypt) => :can_decrypt, CF::Base.typecast(Sec::kSecAttrCanDerive) => :can_derive, @@ -88,16 +114,40 @@ class Key < Sec::Base CF::Base.typecast(Sec::kSecAttrCanWrap) => :can_wrap, CF::Base.typecast(Sec::kSecAttrCanUnwrap) => :can_unwrap} + ATTR_UPDATABLE = Set.new([:klass, + :label, + :application_tag, + :key_type, + :prf, + :rounds, + :salt, + :token_id, + :is_sensitive, + :is_extractable]) + ATTR_MAP[CF::Base.typecast(Sec::kSecAttrAccessible)] = :accessible if defined?(Sec::kSecAttrAccessible) ATTR_MAP[CF::Base.typecast(Sec::kSecAttrAccessControl)] = :access_control if defined?(Sec::kSecAttrAccessControl) INVERSE_ATTR_MAP = ATTR_MAP.invert define_attributes(ATTR_MAP) + def persisted? + true + end + def klass Sec::Classes::KEY.to_ruby end + def public_key + if self.key_class == KEY_CLASS_PUBLIC.to_ruby + self + else + pointer = Sec.SecKeyCopyPublicKey(self) + self.class.new(pointer) + end + end + def export(passphrase = nil, format = :kSecFormatUnknown) flags = Sec::SecItemImportExportKeyParameters.new flags[:version] = Sec::SEC_KEY_IMPORT_EXPORT_PARAMS_VERSION diff --git a/lib/keychain/keychain.rb b/lib/keychain/keychain.rb index 13b7cbf..d91befe 100644 --- a/lib/keychain/keychain.rb +++ b/lib/keychain/keychain.rb @@ -7,7 +7,6 @@ module Sec :kSecItemTypeCertificate, :kSecItemTypeAggregate] - attach_function 'SecAccessCreate', [:pointer, :pointer, :pointer], :osstatus attach_function 'SecTrustedApplicationCreateFromPath', [:string, :pointer], :osstatus attach_function 'SecKeychainCopyDefault', [:pointer], :osstatus @@ -16,8 +15,11 @@ module Sec attach_function 'SecKeychainGetPath', [:keychainref, :pointer, :pointer], :osstatus attach_function 'SecKeychainCreate', [:string, :uint32, :pointer, :char, :pointer, :pointer], :osstatus + + attach_function 'SecItemAdd', [:pointer, :pointer], :osstatus attach_function 'SecItemCopyMatching', [:pointer, :pointer], :osstatus - + attach_function 'SecItemUpdate', [:pointer, :pointer], :osstatus + attach_function 'SecKeychainSetSearchList', [:pointer], :osstatus attach_function 'SecKeychainCopySearchList', [:pointer], :osstatus @@ -113,6 +115,27 @@ def generic_passwords Scope.new(Sec::Classes::GENERIC, self) end + # Returns a scope for the certificates contained in this keychain + # + # @return [Keychain::Scope] a new scope object + def certificates + Scope.new(Sec::Classes::CERTIFICATE, self) + end + + # Returns a scope for the identities (certificates and private keys) contained in this keychain + # + # @return [Keychain::Scope] a new scope object + def identities + Scope.new(Sec::Classes::IDENTITY, self) + end + + # Returns a scope for public and private keys contained in this keychain + # + # @return [Keychain::Scope] a new scope object + def keys + Scope.new(Sec::Classes::KEY, self) + end + # Imports item from string or file to this keychain # # @param [IO, String] input IO object or String with raw data to import @@ -120,26 +143,20 @@ def generic_passwords # permitted to access imported items # @return [Array ] List of imported keychain objects, # each of which may be a SecCertificate, SecKey, or SecIdentity instance - def import(input, app_list=[]) + def import(input, app_paths = [nil]) input = input.read if input.is_a? IO - # Create array of TrustedApplication objects - trusted_apps = get_trusted_apps(app_list) - - # Create an Access object - access_buffer = FFI::MemoryPointer.new(:pointer) - status = Sec.SecAccessCreate(path.to_cf, trusted_apps, access_buffer) - Sec.check_osstatus status - access = CF::Base.typecast(access_buffer.read_pointer) + apps = app_paths.map do |path| + TrustedApplication.create_from_path(path) + end key_params = Sec::SecItemImportExportKeyParameters.new - key_params[:accessRef] = access + key_params[:accessRef] = Access.create(path, apps) # Import item to the keychain cf_data = CF::Data.from_string(input).release_on_gc cf_array = FFI::MemoryPointer.new(:pointer) status = Sec.SecItemImport(cf_data, nil, :kSecFormatUnknown, :kSecItemTypeUnknown, :kSecItemPemArmour, key_params, self, cf_array) - access.release Sec.check_osstatus status item_array = CF::Base.typecast(cf_array.read_pointer).release_on_gc @@ -172,7 +189,7 @@ def path io_size = FFI::MemoryPointer.new(:uint32) io_size.put_uint32(0, out_buffer.size) - status = Sec.SecKeychainGetPath(self,io_size, out_buffer) + status = Sec.SecKeychainGetPath(self, io_size, out_buffer) Sec.check_osstatus(status) out_buffer.read_string(io_size.get_uint32(0)).force_encoding(Encoding::UTF_8) @@ -242,17 +259,6 @@ def get_settings settings end - def get_trusted_apps apps - trusted_app_array = apps.map do |path| - trusted_app_buffer = FFI::MemoryPointer.new(:pointer) - status = Sec.SecTrustedApplicationCreateFromPath( - path.encode(Encoding::UTF_8), trusted_app_buffer) - Sec.check_osstatus(status) - CF::Base.typecast(trusted_app_buffer.read_pointer).release_on_gc - end - trusted_app_array.to_cf - end - def put_settings settings status = Sec.SecKeychainSetSettings(self, settings) Sec.check_osstatus status diff --git a/lib/keychain/scope.rb b/lib/keychain/scope.rb index 820e4d3..1d238cc 100644 --- a/lib/keychain/scope.rb +++ b/lib/keychain/scope.rb @@ -1,148 +1,198 @@ # A scope that represents the search for a keychain item -# -# -class Keychain::Scope - def initialize(kind, keychain=nil) - @kind = kind - @limit = nil - @keychains = [keychain].compact - @conditions = {} - end - # Adds conditions to the scope. Conditions are merged with any previously defined conditions. - # - # 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 - # Keychain::Protocols - # - # @param [Hash] conditions options to create the item with - # @option conditions [String] :account The account (user name) - # @option conditions [String] :comment A free text comment about the item - # @option conditions [Integer] :creator the item's creator, as the unsigned integer representation of a 4 char code - # @option conditions [String] :generic generic passwords can have a generic data attribute - # @option conditions [String] :invisible whether the item should be invisible - # @option conditions [String] :negative A negative item records that the user decided to never save a password - # @option conditions [String] :label A label for the item (Shown in keychain access) - # @option conditions [String] :path The path the password is associated with (internet passwords only) - # @option conditions [String] :port The path the password is associated with (internet passwords only) - # @option conditions [String] :port The protocol the password is associated with (internet passwords only) - # Should be one of the constants at Keychain::Protocols - # @option conditions [String] :domain the domain the password is associated with (internet passwords only) - # @option conditions [String] :server the host name the password is associated with (internet passwords only) - # @option conditions [String] :service the service the password is associated with (generic passwords only) - # @option conditions [Integer] :type the item's type, as the unsigned integer representation of a 4 char code - # - # @return [Keychain::Scope] Returns self as a convenience. The scope is modified in place - def where(conditions) - @conditions.merge! conditions - self - end +module Sec + attach_variable 'kSecMatchLimitAll', :pointer + attach_variable 'kSecMatchLimitOne', :pointer - # Sets the number of items returned by the scope - # - # @param [Integer] value The maximum number of items to return - # @return [Keychain::Scope] Returns self as a convenience. The scope is modified in place - def limit value - @limit = value - self - end + attach_variable 'kSecMatchPolicy', :pointer + attach_variable 'kSecMatchItemList', :pointer + attach_variable 'kSecMatchSearchList', :pointer + attach_variable 'kSecMatchIssuers', :pointer + attach_variable 'kSecMatchEmailAddressIfPresent', :pointer + attach_variable 'kSecMatchSubjectContains', :pointer + attach_variable 'kSecMatchSubjectStartsWith', :pointer + attach_variable 'kSecMatchSubjectEndsWith', :pointer + attach_variable 'kSecMatchSubjectWholeString', :pointer + attach_variable 'kSecMatchCaseInsensitive', :pointer + attach_variable 'kSecMatchDiacriticInsensitive', :pointer + attach_variable 'kSecMatchWidthInsensitive', :pointer + attach_variable 'kSecMatchTrustedOnly', :pointer + attach_variable 'kSecMatchValidOnDate', :pointer + attach_variable 'kSecMatchLimit', :pointer - # Set the list of keychains to search - # - # @param [Array] keychains The maximum number of items to return - # @return [Keychain::Scope] Returns self as a convenience. The scope is modified in place - def in *keychains - @keychains = keychains.flatten - self + # Query options for use with SecCopyMatching, SecItemUpdate + module Query + #key identifying the class of an item (kSecClass) + CLASS = CF::Base.typecast(Sec.kSecClass) + #key speciying the list of keychains to search (kSecMatchSearchList) + SEARCH_LIST = CF::Base.typecast(Sec.kSecMatchSearchList) + #key indicating the list of specific keychain items to the scope the search to + ITEM_LIST = CF::Base.typecast(Sec.kSecMatchItemList) + #key indicating whether to return attributes (kSecReturnAttributes) + RETURN_ATTRIBUTES = CF::Base.typecast(Sec.kSecReturnAttributes) + #key indicating whether to return the SecKeychainItemRef (kSecReturnRef) + RETURN_REF = CF::Base.typecast(Sec.kSecReturnRef) + #key indicating whether to return the password data (kSecReturnData) + RETURN_DATA = CF::Base.typecast(Sec.kSecReturnData) + #key indicating which keychain to use for the operation (kSecUseKeychain) + KEYCHAIN = CF::Base.typecast(Sec.kSecUseKeychain) end +end - # Returns the first matching item in the scope - # - # @return [Keychain::Item, nil] - def first - query = to_query - execute(query).first - end +module Keychain + class Scope + # Match attributes that can be used to search for keychain items, see #where + ATTR_MAP = {CF::Base.typecast(Sec::kSecMatchPolicy) => :policy, + CF::Base.typecast(Sec::kSecMatchItemList) => :item_list, + CF::Base.typecast(Sec::kSecMatchSearchList) => :search_list, + CF::Base.typecast(Sec::kSecMatchIssuers) => :issuers, + CF::Base.typecast(Sec::kSecMatchEmailAddressIfPresent) => :email_address_if_present, + CF::Base.typecast(Sec::kSecMatchSubjectContains) => :subject_contains, + CF::Base.typecast(Sec::kSecMatchSubjectStartsWith) => :subject_starts_with, + CF::Base.typecast(Sec::kSecMatchSubjectEndsWith) => :subject_ends_with, + CF::Base.typecast(Sec::kSecMatchSubjectWholeString) => :subject_whole_string, + CF::Base.typecast(Sec::kSecMatchCaseInsensitive) => :case_insensitive, + CF::Base.typecast(Sec::kSecMatchDiacriticInsensitive) => :diacritic_insensitive, + CF::Base.typecast(Sec::kSecMatchWidthInsensitive) => :width_insensitive, + CF::Base.typecast(Sec::kSecMatchTrustedOnly) => :trusted_only, + CF::Base.typecast(Sec::kSecMatchValidOnDate) => :valid_on_date, + CF::Base.typecast(Sec::kSecMatchLimit) => :limit} - # Returns an array containing all of the matching items - # - # @return [Array] The matching items. May be empty - def all - query = to_query - query[Sec::Search::LIMIT] = @limit ? @limit.to_cf : Sec::Search::ALL - execute query - end + INVERSE_ATTR_MAP = ATTR_MAP.invert - # Creates a new keychain item - # - # @param [Hash] attributes options to create the item with - # @option attributes [String] :account The account (user name) - # @option attributes [String] :comment A free text comment about the item - # @option attributes [Integer] :creator the item's creator, as the unsigned integer representation of a 4 char code - # @option attributes [String] :generic generic passwords can have a generic data attribute - # @option attributes [String] :invisible whether the item should be invisible - # @option attributes [String] :negative A negative item records that the user decided to never save a password - # @option attributes [String] :label A label for the item (Shown in keychain access) - # @option attributes [String] :path The path the password is associated with (internet passwords only) - # @option attributes [String] :port The path the password is associated with (internet passwords only) - # @option attributes [String] :port The protocol the password is associated with (internet passwords only) - # Should be one of the constants at Keychain::Protocols - # @option attributes [String] :domain the domain the password is associated with (internet passwords only) - # @option attributes [String] :server the host name the password is associated with (internet passwords only) - # @option attributes [String] :service the service the password is associated with (generic passwords only) - # @option attributes [Integer] :type the item's type, as the unsigned integer representation of a 4 char code - # - # @return [Keychain::Item] - def create(attributes) - raise "You must specify a password" unless attributes[:password] - - Keychain::Item.new(attributes.merge(:klass => @kind)).save!(:keychain => @keychains.first) - end + def initialize(kind, keychain=nil) + @kind = kind + @keychains = [keychain].compact + @conditions = {} + end - private + # Adds search conditions to the scope. Conditions are merged with any previously defined conditions. + # + # The list of allowed conditions depends on the type of keychain item. See the ATTR_MAP constant + # in the Certificate, Identity, Item and Key classes for allowed values. + # + # In addition, conditions can also use the values in Scope::ATTR_MAP. Those values provide greater control + # over how the search is performed and how many results are returned. + # + # @return [Keychain::Scope] Returns self as a convenience. The scope is modified in place + def where(conditions) + @conditions.merge! conditions + self + end - def execute query - result = FFI::MemoryPointer.new :pointer - status = Sec.SecItemCopyMatching(query, result) - if status == Sec.enum_value( :errSecItemNotFound) - return [] + # Set the list of keychains to search + # + # @param [Array] keychains The maximum number of items to return + # @return [Keychain::Scope] Returns self as a convenience. The scope is modified in place + def in *keychains + @keychains = keychains.flatten + self end - Sec.check_osstatus(status) - result = CF::Base.typecast(result.read_pointer).release_on_gc - unless result.is_a?(CF::Array) - result = CF::Array.immutable([result]) + + # Returns the first matching item in the scope + # + # @return [Keychain::Item, nil] + def first + where(:limit => CF::Base.typecast(Sec.kSecMatchLimitOne)) unless @conditions.include?(:limit) + execute(to_query).first end - result.collect do |dictionary_of_attributes| - item = dictionary_of_attributes[Sec::Value::REF] - item.update_self_from_dictionary(dictionary_of_attributes) - item + + # Returns an array containing all of the matching items + # + # @return [Array] The matching items. May be empty + def all + where(:limit => CF::Base.typecast(Sec.kSecMatchLimitAll)) unless @conditions.include?(:limit) + execute(to_query) end - end - def to_query - query = CF::Dictionary.mutable - # This is terrible but we need to know the result class to get the list of attributes - inverse_attributes = case @kind - when Sec::Classes::CERTIFICATE - Keychain::Certificate::INVERSE_ATTR_MAP - when Sec::Classes::GENERIC - Keychain::Item::INVERSE_ATTR_MAP - when Sec::Classes::IDENTITY - Keychain::Identity::INVERSE_ATTR_MAP - when Sec::Classes::INTERNET - Keychain::Item::INVERSE_ATTR_MAP - when Sec::Classes::KEY - Keychain::Key::INVERSE_ATTR_MAP - end - - @conditions.each do |k,v| - k = inverse_attributes[k] - query[k] = v.to_cf + # Creates a new keychain item + # + # @param [Hash] attributes options to create the item with + # @option attributes [String] :account The account (user name) + # @option attributes [String] :comment A free text comment about the item + # @option attributes [Integer] :creator the item's creator, as the unsigned integer representation of a 4 char code + # @option attributes [String] :generic generic passwords can have a generic data attribute + # @option attributes [String] :invisible whether the item should be invisible + # @option attributes [String] :negative A negative item records that the user decided to never save a password + # @option attributes [String] :label A label for the item (Shown in keychain access) + # @option attributes [String] :path The path the password is associated with (internet passwords only) + # @option attributes [String] :port The path the password is associated with (internet passwords only) + # @option attributes [String] :port The protocol the password is associated with (internet passwords only) + # Should be one of the constants at Keychain::Protocols + # @option attributes [String] :domain the domain the password is associated with (internet passwords only) + # @option attributes [String] :server the host name the password is associated with (internet passwords only) + # @option attributes [String] :service the service the password is associated with (generic passwords only) + # @option attributes [Integer] :type the item's type, as the unsigned integer representation of a 4 char code + # + # @return [Keychain::Item] + def create(attributes) + raise "You must specify a password" unless attributes[:password] + + Item.new(attributes.merge(:klass => @kind)).save!(:keychain => @keychains.first) end - query[Sec::Query::CLASS] = @kind - query[Sec::Query::SEARCH_LIST] = CF::Array.immutable(@keychains) if @keychains && @keychains.any? - query[Sec::Query::RETURN_ATTRIBUTES] = CF::Boolean::TRUE - query[Sec::Query::RETURN_REF] = CF::Boolean::TRUE - query + private + + def execute query + result = FFI::MemoryPointer.new :pointer + status = Sec.SecItemCopyMatching(query, result) + if status == Sec.enum_value( :errSecItemNotFound) + return [] + end + Sec.check_osstatus(status) + result = CF::Base.typecast(result.read_pointer).release_on_gc + unless result.is_a?(CF::Array) + result = CF::Array.immutable([result]) + end + result.collect do |dictionary_of_attributes| + item = dictionary_of_attributes[Sec::Value::REF] + item.update_self_from_dictionary(dictionary_of_attributes) + item + end + end + + def to_query + query = CF::Dictionary.mutable + + # Specify what type of keychain item we are looking for + query[Sec::Query::CLASS] = @kind + + # Specify the keychains to search + query[Sec::Query::SEARCH_LIST] = CF::Array.immutable(@keychains) if @keychains && @keychains.any? + + # Return attributes for found items + query[Sec::Query::RETURN_ATTRIBUTES] = CF::Boolean::TRUE + + # Return references for found items + query[Sec::Query::RETURN_REF] = CF::Boolean::TRUE + + # Now add user specified values + inverse_attributes = case @kind + when Sec::Classes::CERTIFICATE + Certificate::INVERSE_ATTR_MAP + when Sec::Classes::GENERIC + Item::INVERSE_ATTR_MAP + when Sec::Classes::IDENTITY + Identity::INVERSE_ATTR_MAP + when Sec::Classes::INTERNET + Item::INVERSE_ATTR_MAP + when Sec::Classes::KEY + Key::INVERSE_ATTR_MAP + end + + @conditions.each do |key, value| + key_cf = inverse_attributes[key] || INVERSE_ATTR_MAP[key] + if key_cf.nil? + raise "Unknown search key: #{key}. Type: #{@kind}. Please look at the class's ATTR_MAP constant for accepted keys" + end + if value.nil? + raise "Nil search values are not accepted" + end + + query[key_cf] = value.to_cf + end + + query + end end end diff --git a/lib/keychain/sec.rb b/lib/keychain/sec.rb index 279ffad..38fb4c7 100644 --- a/lib/keychain/sec.rb +++ b/lib/keychain/sec.rb @@ -22,28 +22,36 @@ module Sec attach_variable 'kSecClassIdentity', :pointer attach_variable 'kSecClassKey', :pointer + # General Item Attribute Keys attach_variable 'kSecAttrAccess', :pointer - attach_variable 'kSecAttrAccount', :pointer - attach_variable 'kSecAttrAuthenticationType', :pointer - attach_variable 'kSecAttrComment', :pointer + attach_variable 'kSecAttrAccessControl', :pointer + attach_variable 'kSecAttrAccessible', :pointer + attach_variable 'kSecAttrAccessGroup', :pointer + attach_variable 'kSecAttrSynchronizable', :pointer attach_variable 'kSecAttrCreationDate', :pointer - attach_variable 'kSecAttrCreator', :pointer + attach_variable 'kSecAttrModificationDate', :pointer + attach_variable 'kSecAttrComment', :pointer attach_variable 'kSecAttrDescription', :pointer - attach_variable 'kSecAttrGeneric', :pointer + attach_variable 'kSecAttrCreator', :pointer + attach_variable 'kSecAttrType', :pointer + attach_variable 'kSecAttrLabel', :pointer attach_variable 'kSecAttrIsInvisible', :pointer attach_variable 'kSecAttrIsNegative', :pointer - attach_variable 'kSecAttrLabel', :pointer - attach_variable 'kSecAttrModificationDate', :pointer - attach_variable 'kSecAttrPath', :pointer - attach_variable 'kSecAttrPort', :pointer - attach_variable 'kSecAttrProtocol', :pointer + attach_variable 'kSecAttrSyncViewHint', :pointer + + # Password Attribute Keys + attach_variable 'kSecAttrAccount', :pointer + attach_variable 'kSecAttrService', :pointer + attach_variable 'kSecAttrGeneric', :pointer attach_variable 'kSecAttrSecurityDomain', :pointer attach_variable 'kSecAttrServer', :pointer - attach_variable 'kSecAttrService', :pointer - attach_variable 'kSecAttrType', :pointer + attach_variable 'kSecAttrProtocol', :pointer + attach_variable 'kSecAttrAuthenticationType', :pointer + attach_variable 'kSecAttrPort', :pointer + attach_variable 'kSecAttrPath', :pointer + # Item Search Matching Keys attach_variable 'kSecMatchSearchList', :pointer - attach_variable 'kSecMatchLimit', :pointer attach_variable 'kSecMatchLimitOne', :pointer attach_variable 'kSecMatchLimitAll', :pointer @@ -56,32 +64,13 @@ module Sec attach_variable 'kSecValueData', :pointer attach_variable 'kSecUseKeychain', :pointer - # Query options for use with SecCopyMatching, SecItemUpdate - # - module Query - #key identifying the class of an item (kSecClass) - CLASS = CF::Base.typecast(Sec.kSecClass) - #key speciying the list of keychains to search (kSecMatchSearchList) - SEARCH_LIST = CF::Base.typecast(Sec.kSecMatchSearchList) - #key indicating the list of specific keychain items to the scope the search to - ITEM_LIST = CF::Base.typecast(Sec.kSecMatchItemList) - #key indicating whether to return attributes (kSecReturnAttributes) - RETURN_ATTRIBUTES = CF::Base.typecast(Sec.kSecReturnAttributes) - #key indicating whether to return the SecKeychainItemRef (kSecReturnRef) - RETURN_REF = CF::Base.typecast(Sec.kSecReturnRef) - #key indicating whether to return the password data (kSecReturnData) - RETURN_DATA = CF::Base.typecast(Sec.kSecReturnData) - #key indicating which keychain to use for the operation (kSecUseKeychain) - KEYCHAIN = CF::Base.typecast(Sec.kSecUseKeychain) - end - # defines constants for use as the class of an item module Classes # constant identifying certificates (kSecClassCertificate) CERTIFICATE = CF::Base.typecast(Sec.kSecClassCertificate) # constant identifying generic passwords (kSecClassGenericPassword) GENERIC = CF::Base.typecast(Sec.kSecClassGenericPassword) - # constant identifying generic passwords (kSecClassIdentity) + # constant identifying certificates and associated private keys (kSecClassIdentity) IDENTITY = CF::Base.typecast(Sec.kSecClassIdentity) # constant identifying internet passwords (kSecClassInternetPassword) INTERNET = CF::Base.typecast(Sec.kSecClassInternetPassword) @@ -136,7 +125,7 @@ def initialize(ptr) end def update_self_from_dictionary(cf_dict) - @attributes = cf_dict.inject({}) do |memo, (k,v)| + @attributes = cf_dict.inject(Hash.new) do |memo, (k,v)| if ruby_name = self.class::ATTR_MAP[k] memo[ruby_name] = v.to_ruby end @@ -149,11 +138,18 @@ def update_self_from_dictionary(cf_dict) # @return [Keychain::Keychain] def keychain out = FFI::MemoryPointer.new :pointer - status = Sec.SecKeychainItemCopyKeychain(self,out) + status = Sec.SecKeychainItemCopyKeychain(self, out) Sec.check_osstatus(status) CF::Base.new(out.read_pointer).release_on_gc end + # Removes the item from the associated keychain + def delete + status = Sec.SecKeychainItemDelete(self) + Sec.check_osstatus(status) + self + end + def load_attributes result = FFI::MemoryPointer.new :pointer status = Sec.SecItemCopyMatching({Sec::Query::SEARCH_LIST => [self.keychain], @@ -166,6 +162,40 @@ def load_attributes cf_dict = CF::Base.typecast(result.read_pointer).release_on_gc update_self_from_dictionary(cf_dict) end + + def build_new_attributes + new_attributes = CF::Dictionary.mutable + @attributes.each do |key, value| + next unless self.class::ATTR_UPDATABLE.include?(key) + next if key == :klass && self.persisted? + key_cf = self.class::INVERSE_ATTR_MAP[key] + new_attributes[key_cf] = value.to_cf + end + new_attributes + end + + def update + status = Sec.SecItemUpdate({Sec::Query::SEARCH_LIST => [self.keychain], + Sec::Query::ITEM_LIST => [self], + Sec::Query::CLASS => klass}.to_cf, build_new_attributes) + Sec.check_osstatus(status) + + result = FFI::MemoryPointer.new :pointer + query = build_refresh_query + status = Sec.SecItemCopyMatching(query, result) + Sec.check_osstatus(status) + cf_dict = CF::Base.typecast(result.read_pointer) + end + + def build_refresh_query + query = CF::Dictionary.mutable + query[Sec::Query::SEARCH_LIST] = CF::Array.immutable([self.keychain]) + query[Sec::Query::ITEM_LIST] = CF::Array.immutable([self]) + query[Sec::Query::RETURN_ATTRIBUTES] = CF::Boolean::TRUE + query[Sec::Query::RETURN_REF] = CF::Boolean::TRUE + query[Sec::Query::CLASS] = klass.to_cf + query + end end # If the result is non-zero raises an exception. @@ -192,6 +222,4 @@ def self.check_osstatus result end end end - - end diff --git a/lib/keychain/trusted_application.rb b/lib/keychain/trusted_application.rb index 41525d4..5baad0c 100644 --- a/lib/keychain/trusted_application.rb +++ b/lib/keychain/trusted_application.rb @@ -1,5 +1,13 @@ module Keychain class TrustedApplication < Sec::Base register_type 'SecTrustedApplication' + + def self.create_from_path(path) + trusted_app_buffer = FFI::MemoryPointer.new(:pointer) + path = path ? path.encode(Encoding::UTF_8) : nil + status = Sec.SecTrustedApplicationCreateFromPath(path, trusted_app_buffer) + Sec.check_osstatus(status) + self.new(trusted_app_buffer.read_pointer).release_on_gc + end end end \ No newline at end of file diff --git a/lib/keychain/version.rb b/lib/keychain/version.rb index ca287e0..f81b213 100644 --- a/lib/keychain/version.rb +++ b/lib/keychain/version.rb @@ -1,4 +1,4 @@ module Keychain # The current version string - VERSION = '0.3.1' + VERSION = '0.4.0' end diff --git a/spec/identity_spec.rb b/spec/identity_spec.rb index f31586d..b017248 100644 --- a/spec/identity_spec.rb +++ b/spec/identity_spec.rb @@ -4,12 +4,13 @@ before(:context) do Keychain.user_interaction_allowed = false @keychain = Keychain.open(File.join(File.dirname(__FILE__), 'spec.keychain')) - @keychain.unlock! 'DummyPassword' - + @keychain.unlock!('DummyPassword') end + after(:context) do Keychain.user_interaction_allowed = true end + describe 'query' do it 'should return a identity' do scope = Keychain::Scope.new(Sec::Classes::IDENTITY, @keychain) @@ -33,10 +34,11 @@ end #this fails on travis - not sure 100% why yet - skip 'should be exportable to pkcs12' do + it 'should be exportable to pkcs12' do scope = Keychain::Scope.new(Sec::Classes::IDENTITY, @keychain) identity = scope.all.first - expect(identity.pkcs12).to be_kind_of(OpenSSL::PKCS12) + pkcs12 = identity.pkcs12('passphrase') + expect(pkcs12).to be_kind_of(OpenSSL::PKCS12) end end end \ No newline at end of file diff --git a/spec/keychain_spec.rb b/spec/keychain_spec.rb index 86632e4..2a2f8a8 100644 --- a/spec/keychain_spec.rb +++ b/spec/keychain_spec.rb @@ -17,7 +17,7 @@ describe 'default' do it "should return the login keychain" do - expect(Keychain.default.path).to eq(File.expand_path(File.join(ENV['HOME'], 'Library','Keychains', 'login.keychain'))) + expect(Keychain.default.path).to eq(File.expand_path(File.join(ENV['HOME'], 'Library','Keychains', 'login.keychain-db'))) end end @@ -84,7 +84,7 @@ it 'should return true' do expect(Keychain.default.exists?).to be_truthy end - end + end context 'the keychain does not exist' do it 'should return false' do @@ -148,7 +148,7 @@ @keychain_2.delete @keychain_3.delete end - + describe('create') do it 'should add a password' do item = @keychain_1.send(subject).create(create_arguments) @@ -157,7 +157,7 @@ expect(item.password).to eq('some-password') end - it 'should be findable' do + it 'should be findable' do @keychain_1.send(subject).create(create_arguments) item = @keychain_1.send(subject).where(search_for_created_arguments).first expect(item.password).to eq('some-password') @@ -197,7 +197,7 @@ context 'when the limit is option is set' do it 'should limit the return set' do - expect(Keychain.send(subject).where(search_arguments_with_multiple_results).limit(1).all.length).to eq(1) + expect(Keychain.send(subject).where(search_arguments_with_multiple_results.merge(:limit => 1)).all.length).to eq(1) end end diff --git a/spec/spec.keychain b/spec/spec.keychain index 9208819..5d92727 100644 Binary files a/spec/spec.keychain and b/spec/spec.keychain differ