1/*
2 * Copyright (C) 2018 Igalia S.L. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "DeviceIdHashSaltStorage.h"
28
29#include "PersistencyUtils.h"
30
31#include <WebCore/SharedBuffer.h>
32#include <wtf/CryptographicallyRandomNumber.h>
33#include <wtf/FileSystem.h>
34#include <wtf/HexNumber.h>
35#include <wtf/RunLoop.h>
36#include <wtf/text/StringBuilder.h>
37#include <wtf/text/StringHash.h>
38
39namespace WebKit {
40using namespace WebCore;
41
42static constexpr unsigned deviceIdHashSaltStorageVersion { 1 };
43static constexpr unsigned hashSaltSize { 48 };
44static constexpr unsigned randomDataSize { hashSaltSize / 16 };
45
46Ref<DeviceIdHashSaltStorage> DeviceIdHashSaltStorage::create(const String& deviceIdHashSaltStorageDirectory)
47{
48 auto deviceIdHashSaltStorage = adoptRef(*new DeviceIdHashSaltStorage(deviceIdHashSaltStorageDirectory));
49 return deviceIdHashSaltStorage;
50}
51
52void DeviceIdHashSaltStorage::completePendingHandler(CompletionHandler<void(HashSet<SecurityOriginData>&&)>&& completionHandler)
53{
54 ASSERT(RunLoop::isMain());
55
56 HashSet<SecurityOriginData> origins;
57
58 for (auto& hashSaltForOrigin : m_deviceIdHashSaltForOrigins) {
59 origins.add(hashSaltForOrigin.value->documentOrigin);
60 origins.add(hashSaltForOrigin.value->parentOrigin);
61 }
62
63 RunLoop::main().dispatch([origins = WTFMove(origins), completionHandler = WTFMove(completionHandler)]() mutable {
64 completionHandler(WTFMove(origins));
65 });
66}
67
68DeviceIdHashSaltStorage::DeviceIdHashSaltStorage(const String& deviceIdHashSaltStorageDirectory)
69 : m_queue(WorkQueue::create("com.apple.WebKit.DeviceIdHashSaltStorage"))
70 , m_deviceIdHashSaltStorageDirectory(!deviceIdHashSaltStorageDirectory.isEmpty() ? FileSystem::pathByAppendingComponent(deviceIdHashSaltStorageDirectory, String::number(deviceIdHashSaltStorageVersion)) : String())
71{
72 if (m_deviceIdHashSaltStorageDirectory.isEmpty()) {
73 m_isLoaded = true;
74 return;
75 }
76
77 loadStorageFromDisk([this, protectedThis = makeRef(*this)] (auto&& deviceIdHashSaltForOrigins) {
78 ASSERT(RunLoop::isMain());
79 m_deviceIdHashSaltForOrigins = WTFMove(deviceIdHashSaltForOrigins);
80 m_isLoaded = true;
81
82 auto pendingCompletionHandlers = WTFMove(m_pendingCompletionHandlers);
83 for (auto& completionHandler : pendingCompletionHandlers)
84 completionHandler();
85 });
86}
87
88DeviceIdHashSaltStorage::~DeviceIdHashSaltStorage()
89{
90 auto pendingCompletionHandlers = WTFMove(m_pendingCompletionHandlers);
91 for (auto& completionHandler : pendingCompletionHandlers)
92 completionHandler();
93}
94
95static WTF::Optional<SecurityOriginData> getSecurityOriginData(const char* name, KeyedDecoder* decoder)
96{
97 String origin;
98
99 if (!decoder->decodeString(name, origin))
100 return WTF::nullopt;
101
102 auto securityOriginData = SecurityOriginData::fromDatabaseIdentifier(origin);
103 if (!securityOriginData)
104 return WTF::nullopt;
105
106 return securityOriginData;
107}
108
109void DeviceIdHashSaltStorage::loadStorageFromDisk(CompletionHandler<void(HashMap<String, std::unique_ptr<HashSaltForOrigin>>&&)>&& completionHandler)
110{
111 m_queue->dispatch([this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)]() mutable {
112 ASSERT(!RunLoop::isMain());
113
114 FileSystem::makeAllDirectories(m_deviceIdHashSaltStorageDirectory);
115
116 auto originPaths = FileSystem::listDirectory(m_deviceIdHashSaltStorageDirectory, "*");
117
118 HashMap<String, std::unique_ptr<HashSaltForOrigin>> deviceIdHashSaltForOrigins;
119 for (const auto& originPath : originPaths) {
120 URL url;
121 url.setProtocol("file"_s);
122 url.setPath(originPath);
123
124 String deviceIdHashSalt = url.lastPathComponent();
125
126 if (hashSaltSize != deviceIdHashSalt.length()) {
127 RELEASE_LOG_ERROR(DiskPersistency, "DeviceIdHashSaltStorage: The length of the hash salt (%d) is different to the length of the hash salts defined in WebKit (%d)", deviceIdHashSalt.length(), hashSaltSize);
128 continue;
129 }
130
131 long long fileSize = 0;
132 if (!FileSystem::getFileSize(originPath, fileSize)) {
133 RELEASE_LOG_ERROR(DiskPersistency, "DeviceIdHashSaltStorage: Impossible to get the file size of: '%s'", originPath.utf8().data());
134 continue;
135 }
136
137 auto decoder = createForFile(originPath);
138
139 if (!decoder) {
140 RELEASE_LOG_ERROR(DiskPersistency, "DeviceIdHashSaltStorage: Impossible to access the file to restore the hash salt: '%s'", originPath.utf8().data());
141 continue;
142 }
143
144 auto hashSaltForOrigin = getDataFromDecoder(decoder.get(), WTFMove(deviceIdHashSalt));
145
146 auto origins = makeString(hashSaltForOrigin->documentOrigin.toString(), hashSaltForOrigin->parentOrigin.toString());
147 auto deviceIdHashSaltForOrigin = deviceIdHashSaltForOrigins.ensure(origins, [hashSaltForOrigin = WTFMove(hashSaltForOrigin)] () mutable {
148 return WTFMove(hashSaltForOrigin);
149 });
150
151 if (!deviceIdHashSaltForOrigin.isNewEntry)
152 RELEASE_LOG_ERROR(DiskPersistency, "DeviceIdHashSaltStorage: There are two files with different hash salts for the same origin: '%s'", originPath.utf8().data());
153 }
154
155 RunLoop::main().dispatch([deviceIdHashSaltForOrigins = WTFMove(deviceIdHashSaltForOrigins), completionHandler = WTFMove(completionHandler)]() mutable {
156 completionHandler(WTFMove(deviceIdHashSaltForOrigins));
157 });
158 });
159}
160
161std::unique_ptr<DeviceIdHashSaltStorage::HashSaltForOrigin> DeviceIdHashSaltStorage::getDataFromDecoder(KeyedDecoder* decoder, String&& deviceIdHashSalt) const
162{
163 auto securityOriginData = getSecurityOriginData("origin", decoder);
164 if (!securityOriginData) {
165 RELEASE_LOG_ERROR(DiskPersistency, "DeviceIdHashSaltStorage: The security origin data in the file is not correct: '%s'", deviceIdHashSalt.utf8().data());
166 return nullptr;
167 }
168
169 auto parentSecurityOriginData = getSecurityOriginData("parentOrigin", decoder);
170 if (!parentSecurityOriginData) {
171 RELEASE_LOG_ERROR(DiskPersistency, "DeviceIdHashSaltStorage: The parent security origin data in the file is not correct: '%s'", deviceIdHashSalt.utf8().data());
172 return nullptr;
173 }
174
175 double lastTimeUsed;
176 if (!decoder->decodeDouble("lastTimeUsed", lastTimeUsed)) {
177 RELEASE_LOG_ERROR(DiskPersistency, "DeviceIdHashSaltStorage: The last time used was not correctly restored for: '%s'", deviceIdHashSalt.utf8().data());
178 return nullptr;
179 }
180
181 auto hashSaltForOrigin = std::make_unique<HashSaltForOrigin>(WTFMove(securityOriginData.value()), WTFMove(parentSecurityOriginData.value()), WTFMove(deviceIdHashSalt));
182
183 hashSaltForOrigin->lastTimeUsed = WallTime::fromRawSeconds(lastTimeUsed);
184
185 return hashSaltForOrigin;
186}
187
188std::unique_ptr<KeyedEncoder> DeviceIdHashSaltStorage::createEncoderFromData(const HashSaltForOrigin& hashSaltForOrigin) const
189{
190 auto encoder = KeyedEncoder::encoder();
191 encoder->encodeString("origin", hashSaltForOrigin.documentOrigin.databaseIdentifier());
192 encoder->encodeString("parentOrigin", hashSaltForOrigin.parentOrigin.databaseIdentifier());
193 encoder->encodeDouble("lastTimeUsed", hashSaltForOrigin.lastTimeUsed.secondsSinceEpoch().value());
194 return encoder;
195}
196
197void DeviceIdHashSaltStorage::storeHashSaltToDisk(const HashSaltForOrigin& hashSaltForOrigin)
198{
199 if (m_deviceIdHashSaltStorageDirectory.isEmpty())
200 return;
201
202 m_queue->dispatch([this, protectedThis = makeRef(*this), hashSaltForOrigin = hashSaltForOrigin.isolatedCopy()]() mutable {
203 auto encoder = createEncoderFromData(hashSaltForOrigin);
204 writeToDisk(WTFMove(encoder), FileSystem::pathByAppendingComponent(m_deviceIdHashSaltStorageDirectory, hashSaltForOrigin.deviceIdHashSalt));
205 });
206}
207
208void DeviceIdHashSaltStorage::completeDeviceIdHashSaltForOriginCall(SecurityOriginData&& documentOrigin, SecurityOriginData&& parentOrigin, CompletionHandler<void(String&&)>&& completionHandler)
209{
210 auto origins = makeString(documentOrigin.toString(), parentOrigin.toString());
211 auto& deviceIdHashSalt = m_deviceIdHashSaltForOrigins.ensure(origins, [documentOrigin = WTFMove(documentOrigin), parentOrigin = WTFMove(parentOrigin)] () mutable {
212 uint64_t randomData[randomDataSize];
213 cryptographicallyRandomValues(reinterpret_cast<unsigned char*>(randomData), sizeof(randomData));
214
215 StringBuilder builder;
216 builder.reserveCapacity(hashSaltSize);
217 for (unsigned i = 0; i < randomDataSize; i++)
218 appendUnsignedAsHex(randomData[i], builder);
219
220 String deviceIdHashSalt = builder.toString();
221
222 auto newHashSaltForOrigin = std::make_unique<HashSaltForOrigin>(WTFMove(documentOrigin), WTFMove(parentOrigin), WTFMove(deviceIdHashSalt));
223
224 return newHashSaltForOrigin;
225 }).iterator->value;
226
227 deviceIdHashSalt->lastTimeUsed = WallTime::now();
228
229 storeHashSaltToDisk(*deviceIdHashSalt.get());
230
231 completionHandler(String(deviceIdHashSalt->deviceIdHashSalt));
232}
233
234void DeviceIdHashSaltStorage::deviceIdHashSaltForOrigin(const SecurityOrigin& documentOrigin, const SecurityOrigin& parentOrigin, CompletionHandler<void(String&&)>&& completionHandler)
235{
236 ASSERT(RunLoop::isMain());
237
238 if (!m_isLoaded) {
239 m_pendingCompletionHandlers.append([this, documentOrigin = documentOrigin.data().isolatedCopy(), parentOrigin = parentOrigin.data().isolatedCopy(), completionHandler = WTFMove(completionHandler)]() mutable {
240 completeDeviceIdHashSaltForOriginCall(WTFMove(documentOrigin), WTFMove(parentOrigin), WTFMove(completionHandler));
241 });
242 return;
243 }
244
245 completeDeviceIdHashSaltForOriginCall(SecurityOriginData(documentOrigin.data()), SecurityOriginData(parentOrigin.data()), WTFMove(completionHandler));
246}
247
248void DeviceIdHashSaltStorage::getDeviceIdHashSaltOrigins(CompletionHandler<void(HashSet<SecurityOriginData>&&)>&& completionHandler)
249{
250 ASSERT(RunLoop::isMain());
251
252 if (!m_isLoaded) {
253 m_pendingCompletionHandlers.append([this, completionHandler = WTFMove(completionHandler)]() mutable {
254 completePendingHandler(WTFMove(completionHandler));
255 });
256 return;
257 }
258
259 completePendingHandler(WTFMove(completionHandler));
260}
261
262void DeviceIdHashSaltStorage::deleteHashSaltFromDisk(const HashSaltForOrigin& hashSaltForOrigin)
263{
264 m_queue->dispatch([this, protectedThis = makeRef(*this), deviceIdHashSalt = hashSaltForOrigin.deviceIdHashSalt.isolatedCopy()]() mutable {
265 ASSERT(!RunLoop::isMain());
266
267 String fileFullPath = FileSystem::pathByAppendingComponent(m_deviceIdHashSaltStorageDirectory, deviceIdHashSalt.utf8().data());
268 FileSystem::deleteFile(fileFullPath);
269 });
270}
271
272void DeviceIdHashSaltStorage::deleteDeviceIdHashSaltForOrigins(const Vector<SecurityOriginData>& origins, CompletionHandler<void()>&& completionHandler)
273{
274 ASSERT(RunLoop::isMain());
275
276 m_deviceIdHashSaltForOrigins.removeIf([this, &origins](auto& keyAndValue) {
277 bool needsRemoval = origins.contains(keyAndValue.value->documentOrigin) || origins.contains(keyAndValue.value->parentOrigin);
278 if (m_deviceIdHashSaltStorageDirectory.isEmpty())
279 return needsRemoval;
280 if (needsRemoval)
281 this->deleteHashSaltFromDisk(*keyAndValue.value.get());
282 return needsRemoval;
283 });
284
285 RunLoop::main().dispatch(WTFMove(completionHandler));
286}
287
288void DeviceIdHashSaltStorage::deleteDeviceIdHashSaltOriginsModifiedSince(WallTime time, CompletionHandler<void()>&& completionHandler)
289{
290 ASSERT(RunLoop::isMain());
291
292 m_deviceIdHashSaltForOrigins.removeIf([this, time](auto& keyAndValue) {
293 bool needsRemoval = keyAndValue.value->lastTimeUsed > time;
294 if (m_deviceIdHashSaltStorageDirectory.isEmpty())
295 return needsRemoval;
296 if (needsRemoval)
297 this->deleteHashSaltFromDisk(*keyAndValue.value.get());
298 return needsRemoval;
299 });
300
301 RunLoop::main().dispatch(WTFMove(completionHandler));
302}
303
304} // namespace WebKit
305