diff options
Diffstat (limited to 'security/manager/ssl/KeychainSecret.cpp')
-rw-r--r-- | security/manager/ssl/KeychainSecret.cpp | 186 |
1 files changed, 186 insertions, 0 deletions
diff --git a/security/manager/ssl/KeychainSecret.cpp b/security/manager/ssl/KeychainSecret.cpp new file mode 100644 index 0000000000..376e2f14c3 --- /dev/null +++ b/security/manager/ssl/KeychainSecret.cpp @@ -0,0 +1,186 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "KeychainSecret.h" + +#include <Security/Security.h> + +#include "mozilla/Logging.h" + +// This is the implementation of KeychainSecret, an instantiation of OSKeyStore +// for OS X. It uses the system keychain, hence the name. + +using namespace mozilla; + +LazyLogModule gKeychainSecretLog("keychainsecret"); + +KeychainSecret::KeychainSecret() {} + +KeychainSecret::~KeychainSecret() {} + +nsresult KeychainSecret::Lock() { + // https://developer.apple.com/documentation/security/1402180-seckeychainlock + // Passing `nullptr` locks the default keychain. + OSStatus rv = SecKeychainLock(nullptr); + if (rv != errSecSuccess) { + MOZ_LOG(gKeychainSecretLog, LogLevel::Debug, + ("SecKeychainLock failed: %d", rv)); + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +nsresult KeychainSecret::Unlock() { + // https://developer.apple.com/documentation/security/1400341-seckeychainunlock + // This attempts to unlock the default keychain. Using `false` indicates we + // aren't passing in a password (if the keychain is locked, a dialog will + // appear for the user). + OSStatus rv = SecKeychainUnlock(nullptr, 0, nullptr, false); + if (rv != errSecSuccess) { + MOZ_LOG(gKeychainSecretLog, LogLevel::Debug, + ("SecKeychainUnlock failed: %d", rv)); + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +ScopedCFType<CFStringRef> MozillaStringToCFString(const nsACString& stringIn) { + // https://developer.apple.com/documentation/corefoundation/1543419-cfstringcreatewithbytes + ScopedCFType<CFStringRef> stringOut(CFStringCreateWithBytes( + nullptr, reinterpret_cast<const UInt8*>(stringIn.BeginReading()), + stringIn.Length(), kCFStringEncodingUTF8, false)); + return stringOut; +} + +nsresult KeychainSecret::StoreSecret(const nsACString& aSecret, + const nsACString& aLabel) { + // This creates a CFDictionary of the form: + // { class: generic password, + // account: the given label, + // value: the given secret } + // "account" is the way we differentiate different secrets. + // By default, secrets stored by the application (Firefox) in this way are not + // accessible to other applications, so we shouldn't need to worry about + // unauthorized access or namespace collisions. This will be the case as long + // as we never set the kSecAttrAccessGroup attribute on the CFDictionary. The + // platform enforces this restriction using the application-identifier + // entitlement that each application bundle should have. See + // https://developer.apple.com/documentation/security/1401659-secitemadd?language=objc#discussion + + // The keychain does not overwrite secrets by default (unlike other backends + // like libsecret and credential manager). To be consistent, we first delete + // any previously-stored secrets that use the given label. + nsresult rv = DeleteSecret(aLabel); + if (NS_FAILED(rv)) { + MOZ_LOG(gKeychainSecretLog, LogLevel::Debug, + ("DeleteSecret before StoreSecret failed")); + return rv; + } + const CFStringRef keys[] = {kSecClass, kSecAttrAccount, kSecValueData}; + ScopedCFType<CFStringRef> label(MozillaStringToCFString(aLabel)); + if (!label) { + MOZ_LOG(gKeychainSecretLog, LogLevel::Debug, + ("MozillaStringToCFString failed")); + return NS_ERROR_FAILURE; + } + ScopedCFType<CFDataRef> secret(CFDataCreate( + nullptr, reinterpret_cast<const UInt8*>(aSecret.BeginReading()), + aSecret.Length())); + if (!secret) { + MOZ_LOG(gKeychainSecretLog, LogLevel::Debug, ("CFDataCreate failed")); + return NS_ERROR_FAILURE; + } + const void* values[] = {kSecClassGenericPassword, label.get(), secret.get()}; + static_assert(ArrayLength(keys) == ArrayLength(values), + "mismatched SecItemAdd key/value array sizes"); + ScopedCFType<CFDictionaryRef> addDictionary(CFDictionaryCreate( + nullptr, (const void**)&keys, (const void**)&values, ArrayLength(keys), + &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)); + // https://developer.apple.com/documentation/security/1401659-secitemadd + OSStatus osrv = SecItemAdd(addDictionary.get(), nullptr); + if (osrv != errSecSuccess) { + MOZ_LOG(gKeychainSecretLog, LogLevel::Debug, + ("SecItemAdd failed: %d", osrv)); + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +nsresult KeychainSecret::DeleteSecret(const nsACString& aLabel) { + // To delete a secret, we create a CFDictionary of the form: + // { class: generic password, + // account: the given label } + // and then call SecItemDelete. + const CFStringRef keys[] = {kSecClass, kSecAttrAccount}; + ScopedCFType<CFStringRef> label(MozillaStringToCFString(aLabel)); + if (!label) { + MOZ_LOG(gKeychainSecretLog, LogLevel::Debug, + ("MozillaStringToCFString failed")); + return NS_ERROR_FAILURE; + } + const void* values[] = {kSecClassGenericPassword, label.get()}; + static_assert(ArrayLength(keys) == ArrayLength(values), + "mismatched SecItemDelete key/value array sizes"); + ScopedCFType<CFDictionaryRef> deleteDictionary(CFDictionaryCreate( + nullptr, (const void**)&keys, (const void**)&values, ArrayLength(keys), + &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)); + // https://developer.apple.com/documentation/security/1395547-secitemdelete + OSStatus rv = SecItemDelete(deleteDictionary.get()); + if (rv != errSecSuccess && rv != errSecItemNotFound) { + MOZ_LOG(gKeychainSecretLog, LogLevel::Debug, + ("SecItemDelete failed: %d", rv)); + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +nsresult KeychainSecret::RetrieveSecret(const nsACString& aLabel, + /* out */ nsACString& aSecret) { + // To retrieve a secret, we create a CFDictionary of the form: + // { class: generic password, + // account: the given label, + // match limit: match one, + // return attributes: true, + // return data: true } + // This searches for and returns the attributes and data for the secret + // matching the given label. We then extract the data (i.e. the secret) and + // return it. + const CFStringRef keys[] = {kSecClass, kSecAttrAccount, kSecMatchLimit, + kSecReturnAttributes, kSecReturnData}; + ScopedCFType<CFStringRef> label(MozillaStringToCFString(aLabel)); + if (!label) { + MOZ_LOG(gKeychainSecretLog, LogLevel::Debug, + ("MozillaStringToCFString failed")); + return NS_ERROR_FAILURE; + } + const void* values[] = {kSecClassGenericPassword, label.get(), + kSecMatchLimitOne, kCFBooleanTrue, kCFBooleanTrue}; + static_assert(ArrayLength(keys) == ArrayLength(values), + "mismatched SecItemCopyMatching key/value array sizes"); + ScopedCFType<CFDictionaryRef> searchDictionary(CFDictionaryCreate( + nullptr, (const void**)&keys, (const void**)&values, ArrayLength(keys), + &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)); + CFTypeRef item; + // https://developer.apple.com/documentation/security/1398306-secitemcopymatching + OSStatus rv = SecItemCopyMatching(searchDictionary.get(), &item); + if (rv != errSecSuccess) { + MOZ_LOG(gKeychainSecretLog, LogLevel::Debug, + ("SecItemCopyMatching failed: %d", rv)); + return NS_ERROR_FAILURE; + } + ScopedCFType<CFDictionaryRef> dictionary( + reinterpret_cast<CFDictionaryRef>(item)); + CFDataRef secret = reinterpret_cast<CFDataRef>( + CFDictionaryGetValue(dictionary.get(), kSecValueData)); + if (!secret) { + MOZ_LOG(gKeychainSecretLog, LogLevel::Debug, + ("CFDictionaryGetValue failed")); + return NS_ERROR_FAILURE; + } + aSecret.Assign(reinterpret_cast<const char*>(CFDataGetBytePtr(secret)), + CFDataGetLength(secret)); + return NS_OK; +} |