KeychainSecret.cpp (8122B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- 2 * 3 * This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "KeychainSecret.h" 8 9 #include <Security/Security.h> 10 11 #include "mozilla/Logging.h" 12 #include "mozilla/glean/SecurityManagerSslMetrics.h" 13 #include "nsPrintfCString.h" 14 15 // This is the implementation of KeychainSecret, an instantiation of OSKeyStore 16 // for OS X. It uses the system keychain, hence the name. 17 18 using namespace mozilla; 19 20 LazyLogModule gKeychainSecretLog("keychainsecret"); 21 22 KeychainSecret::KeychainSecret() {} 23 24 KeychainSecret::~KeychainSecret() {} 25 26 ScopedCFType<CFStringRef> MozillaStringToCFString(const nsACString& stringIn) { 27 // https://developer.apple.com/documentation/corefoundation/1543419-cfstringcreatewithbytes 28 ScopedCFType<CFStringRef> stringOut(CFStringCreateWithBytes( 29 nullptr, reinterpret_cast<const UInt8*>(stringIn.BeginReading()), 30 stringIn.Length(), kCFStringEncodingUTF8, false)); 31 return stringOut; 32 } 33 34 nsresult KeychainSecret::StoreSecret(const nsACString& aSecret, 35 const nsACString& aLabel) { 36 // This creates a CFDictionary of the form: 37 // { class: generic password, 38 // account: the given label, 39 // value: the given secret } 40 // "account" is the way we differentiate different secrets. 41 // By default, secrets stored by the application (Firefox) in this way are not 42 // accessible to other applications, so we shouldn't need to worry about 43 // unauthorized access or namespace collisions. This will be the case as long 44 // as we never set the kSecAttrAccessGroup attribute on the CFDictionary. The 45 // platform enforces this restriction using the application-identifier 46 // entitlement that each application bundle should have. See 47 // https://developer.apple.com/documentation/security/1401659-secitemadd?language=objc#discussion 48 49 // The keychain does not overwrite secrets by default (unlike other backends 50 // like libsecret and credential manager). To be consistent, we first delete 51 // any previously-stored secrets that use the given label. 52 nsresult rv = DeleteSecret(aLabel); 53 if (NS_FAILED(rv)) { 54 MOZ_LOG(gKeychainSecretLog, LogLevel::Debug, 55 ("DeleteSecret before StoreSecret failed")); 56 return rv; 57 } 58 const CFStringRef keys[] = {kSecClass, kSecAttrAccount, kSecValueData}; 59 ScopedCFType<CFStringRef> label(MozillaStringToCFString(aLabel)); 60 if (!label) { 61 MOZ_LOG(gKeychainSecretLog, LogLevel::Debug, 62 ("MozillaStringToCFString failed")); 63 return NS_ERROR_FAILURE; 64 } 65 ScopedCFType<CFDataRef> secret(CFDataCreate( 66 nullptr, reinterpret_cast<const UInt8*>(aSecret.BeginReading()), 67 aSecret.Length())); 68 if (!secret) { 69 MOZ_LOG(gKeychainSecretLog, LogLevel::Debug, ("CFDataCreate failed")); 70 return NS_ERROR_FAILURE; 71 } 72 const void* values[] = {kSecClassGenericPassword, label.get(), secret.get()}; 73 static_assert(std::size(keys) == std::size(values), 74 "mismatched SecItemAdd key/value array sizes"); 75 ScopedCFType<CFDictionaryRef> addDictionary(CFDictionaryCreate( 76 nullptr, (const void**)&keys, (const void**)&values, std::size(keys), 77 &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)); 78 // https://developer.apple.com/documentation/security/1401659-secitemadd 79 OSStatus osrv = SecItemAdd(addDictionary.get(), nullptr); 80 if (osrv != errSecSuccess) { 81 MOZ_LOG(gKeychainSecretLog, LogLevel::Debug, 82 ("SecItemAdd failed: %d", osrv)); 83 nsPrintfCString osrvString("%d", osrv); 84 mozilla::glean::oskeystore::ReturnCodesExtra extra = {}; 85 extra.function = Some("StoreSecret_SecItemAdd"_ns); 86 extra.result = Some(osrvString); 87 glean::oskeystore::return_codes.Record(Some(extra)); 88 return NS_ERROR_FAILURE; 89 } 90 return NS_OK; 91 } 92 93 nsresult KeychainSecret::DeleteSecret(const nsACString& aLabel) { 94 // To delete a secret, we create a CFDictionary of the form: 95 // { class: generic password, 96 // account: the given label } 97 // and then call SecItemDelete. 98 const CFStringRef keys[] = {kSecClass, kSecAttrAccount}; 99 ScopedCFType<CFStringRef> label(MozillaStringToCFString(aLabel)); 100 if (!label) { 101 MOZ_LOG(gKeychainSecretLog, LogLevel::Debug, 102 ("MozillaStringToCFString failed")); 103 return NS_ERROR_FAILURE; 104 } 105 const void* values[] = {kSecClassGenericPassword, label.get()}; 106 static_assert(std::size(keys) == std::size(values), 107 "mismatched SecItemDelete key/value array sizes"); 108 ScopedCFType<CFDictionaryRef> deleteDictionary(CFDictionaryCreate( 109 nullptr, (const void**)&keys, (const void**)&values, std::size(keys), 110 &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)); 111 // https://developer.apple.com/documentation/security/1395547-secitemdelete 112 OSStatus osrv = SecItemDelete(deleteDictionary.get()); 113 if (osrv != errSecSuccess && osrv != errSecItemNotFound) { 114 MOZ_LOG(gKeychainSecretLog, LogLevel::Debug, 115 ("SecItemDelete failed: %d", osrv)); 116 mozilla::glean::oskeystore::ReturnCodesExtra extra = {}; 117 extra.function = Some("DeleteSecret_SecItemDelete"_ns); 118 nsPrintfCString osrvString("%d", osrv); 119 extra.result = Some(osrvString); 120 glean::oskeystore::return_codes.Record(Some(extra)); 121 return NS_ERROR_FAILURE; 122 } 123 return NS_OK; 124 } 125 126 nsresult KeychainSecret::RetrieveSecret(const nsACString& aLabel, 127 /* out */ nsACString& aSecret) { 128 // To retrieve a secret, we create a CFDictionary of the form: 129 // { class: generic password, 130 // account: the given label, 131 // match limit: match one, 132 // return attributes: true, 133 // return data: true } 134 // This searches for and returns the attributes and data for the secret 135 // matching the given label. We then extract the data (i.e. the secret) and 136 // return it. 137 const CFStringRef keys[] = {kSecClass, kSecAttrAccount, kSecMatchLimit, 138 kSecReturnAttributes, kSecReturnData}; 139 ScopedCFType<CFStringRef> label(MozillaStringToCFString(aLabel)); 140 if (!label) { 141 MOZ_LOG(gKeychainSecretLog, LogLevel::Debug, 142 ("MozillaStringToCFString failed")); 143 return NS_ERROR_FAILURE; 144 } 145 const void* values[] = {kSecClassGenericPassword, label.get(), 146 kSecMatchLimitOne, kCFBooleanTrue, kCFBooleanTrue}; 147 static_assert(std::size(keys) == std::size(values), 148 "mismatched SecItemCopyMatching key/value array sizes"); 149 ScopedCFType<CFDictionaryRef> searchDictionary(CFDictionaryCreate( 150 nullptr, (const void**)&keys, (const void**)&values, std::size(keys), 151 &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)); 152 CFTypeRef item; 153 // https://developer.apple.com/documentation/security/1398306-secitemcopymatching 154 OSStatus osrv = SecItemCopyMatching(searchDictionary.get(), &item); 155 if (osrv == errSecItemNotFound) { 156 MOZ_LOG(gKeychainSecretLog, LogLevel::Debug, 157 ("Key not found in key store")); 158 return NS_ERROR_NOT_AVAILABLE; 159 } 160 if (osrv != errSecSuccess) { 161 MOZ_LOG(gKeychainSecretLog, LogLevel::Debug, 162 ("SecItemCopyMatching failed: %d", osrv)); 163 mozilla::glean::oskeystore::ReturnCodesExtra extra = {}; 164 extra.function = Some("RetrieveSecret_SecItemCopyMatching"_ns); 165 nsPrintfCString osrvString("%d", osrv); 166 extra.result = Some(osrvString); 167 glean::oskeystore::return_codes.Record(Some(extra)); 168 return NS_ERROR_FAILURE; 169 } 170 ScopedCFType<CFDictionaryRef> dictionary( 171 reinterpret_cast<CFDictionaryRef>(item)); 172 CFDataRef secret = reinterpret_cast<CFDataRef>( 173 CFDictionaryGetValue(dictionary.get(), kSecValueData)); 174 if (!secret) { 175 MOZ_LOG(gKeychainSecretLog, LogLevel::Debug, 176 ("CFDictionaryGetValue failed")); 177 return NS_ERROR_FAILURE; 178 } 179 aSecret.Assign(reinterpret_cast<const char*>(CFDataGetBytePtr(secret)), 180 CFDataGetLength(secret)); 181 return NS_OK; 182 }