summaryrefslogtreecommitdiffstats
path: root/security/manager/ssl/KeychainSecret.cpp
blob: 376e2f14c34ccd4fd56daf571f4d2a58fa0e652c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
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;
}