1 #include "util/sandbox.h"
2
3 #include <QFileDialog>
4 #include <QFileInfo>
5 #include <QMutexLocker>
6 #include <QObject>
7 #include <QtDebug>
8
9 #include "util/mac.h"
10
11 #ifdef __APPLE__
12 #include <CoreFoundation/CoreFoundation.h>
13 #include <CoreServices/CoreServices.h>
14 #include <Security/SecCode.h>
15 #include <Security/SecRequirement.h>
16 #endif
17
18 const bool sDebug = false;
19
20 #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
21 QRecursiveMutex Sandbox::s_mutex;
22 #else
23 QMutex Sandbox::s_mutex(QMutex::Recursive);
24 #endif
25 bool Sandbox::s_bInSandbox = false;
26 QSharedPointer<ConfigObject<ConfigValue>> Sandbox::s_pSandboxPermissions;
27 QHash<QString, SecurityTokenWeakPointer> Sandbox::s_activeTokens;
28
29 // static
checkSandboxed()30 void Sandbox::checkSandboxed() {
31 #ifdef __APPLE__
32 SecCodeRef secCodeSelf;
33 if (SecCodeCopySelf(kSecCSDefaultFlags, &secCodeSelf) == errSecSuccess) {
34 SecRequirementRef sandboxReq;
35 CFStringRef entitlement = CFSTR("entitlement [\"com.apple.security.app-sandbox\"]");
36 if (SecRequirementCreateWithString(entitlement, kSecCSDefaultFlags,
37 &sandboxReq) == errSecSuccess) {
38 if (SecCodeCheckValidity(secCodeSelf, kSecCSDefaultFlags,
39 sandboxReq) == errSecSuccess) {
40 s_bInSandbox = true;
41 }
42 CFRelease(sandboxReq);
43 }
44 CFRelease(secCodeSelf);
45 }
46 #endif
47 }
48
setPermissionsFilePath(const QString & permissionsFile)49 void Sandbox::setPermissionsFilePath(const QString& permissionsFile) {
50 QMutexLocker locker(&s_mutex);
51 s_pSandboxPermissions = QSharedPointer<ConfigObject<ConfigValue>>(
52 new ConfigObject<ConfigValue>(permissionsFile));
53 }
54
55 // static
shutdown()56 void Sandbox::shutdown() {
57 QMutexLocker locker(&s_mutex);
58 QSharedPointer<ConfigObject<ConfigValue>> pSandboxPermissions = s_pSandboxPermissions;
59 s_pSandboxPermissions.clear();
60 if (pSandboxPermissions) {
61 pSandboxPermissions->save();
62 }
63 }
64
65 // static
askForAccess(const QString & path)66 bool Sandbox::askForAccess(const QString& path) {
67 if (sDebug) {
68 qDebug() << "Sandbox::askForAccess" << path;
69 }
70 if (!enabled()) {
71 // Pretend we have access.
72 return true;
73 }
74
75 QFileInfo info(path);
76 if (!info.exists()) {
77 // We cannot grant access to a not existing file
78 return false;
79 }
80
81 // We always want read/write access because we wouldn't want to have to
82 // re-ask for access in the future if we need to write.
83 if (canAccessFile(info)) {
84 return true;
85 }
86
87 QString title = QObject::tr("Mixxx Needs Access to: %1")
88 .arg(info.fileName());
89
90 QMessageBox::information(nullptr,
91 title,
92 QObject::tr(
93 "Due to Mac Sandboxing, we need your permission to access "
94 "this file:"
95 "\n\n%1\n\n"
96 "After clicking OK, you will see a file picker. "
97 "To give Mixxx permission, you must select '%2' to "
98 "proceed. "
99 "If you do not want to grant Mixxx access click Cancel on "
100 "the file picker. "
101 "We're sorry for this inconvenience.\n\n"
102 "To abort this action, press Cancel on the file dialog.")
103 .arg(path, info.fileName()));
104
105 QString result;
106 QFileInfo resultInfo;
107 while (true) {
108 if (info.isFile()) {
109 result = QFileDialog::getOpenFileName(nullptr, title, path);
110 } else if (info.isDir()) {
111 result = QFileDialog::getExistingDirectory(nullptr, title, path);
112 }
113
114 if (result.isNull()) {
115 if (sDebug) {
116 qDebug() << "Sandbox: User rejected access to" << path;
117 }
118 return false;
119 }
120
121 if (sDebug) {
122 qDebug() << "Sandbox: User selected" << result;
123 }
124 resultInfo = QFileInfo(result);
125 if (resultInfo == info) {
126 break;
127 }
128
129 if (sDebug) {
130 qDebug() << "User selected the wrong file.";
131 }
132 QMessageBox::information(
133 nullptr, title, QObject::tr("You selected the wrong file. To grant Mixxx access, "
134 "please select the file '%1'. If you do not want to "
135 "continue, press Cancel.")
136 .arg(info.fileName()));
137 }
138
139 return createSecurityToken(resultInfo);
140 }
141
142 // static
keyForCanonicalPath(const QString & canonicalPath)143 ConfigKey Sandbox::keyForCanonicalPath(const QString& canonicalPath) {
144 return ConfigKey("[OSXBookmark]",
145 QString(canonicalPath.toLocal8Bit().toBase64()));
146 }
147
148 // static
createSecurityToken(const QString & canonicalPath,bool isDirectory)149 bool Sandbox::createSecurityToken(const QString& canonicalPath,
150 bool isDirectory) {
151 if (sDebug) {
152 qDebug() << "createSecurityToken" << canonicalPath << isDirectory;
153 }
154 if (!enabled()) {
155 return false;
156 }
157 QMutexLocker locker(&s_mutex);
158 if (s_pSandboxPermissions == nullptr) {
159 return false;
160 }
161
162 #ifdef __APPLE__
163 CFURLRef url = CFURLCreateWithFileSystemPath(
164 kCFAllocatorDefault, QStringToCFString(canonicalPath),
165 kCFURLPOSIXPathStyle, isDirectory);
166 if (url) {
167 CFErrorRef error = NULL;
168 CFDataRef bookmark = CFURLCreateBookmarkData(
169 kCFAllocatorDefault, url,
170 kCFURLBookmarkCreationWithSecurityScope, nil, nil, &error);
171 CFRelease(url);
172 if (bookmark) {
173 QByteArray bookmarkBA = QByteArray(
174 reinterpret_cast<const char*>(CFDataGetBytePtr(bookmark)),
175 CFDataGetLength(bookmark));
176
177 QString bookmarkBase64 = QString(bookmarkBA.toBase64());
178
179 s_pSandboxPermissions->set(keyForCanonicalPath(canonicalPath),
180 bookmarkBase64);
181 CFRelease(bookmark);
182 return true;
183 } else {
184 if (sDebug) {
185 qDebug() << "Failed to create security-scoped bookmark for" << canonicalPath;
186 if (error != NULL) {
187 qDebug() << "Error:" << CFStringToQString(CFErrorCopyDescription(error));
188 }
189 }
190 }
191 } else {
192 if (sDebug) {
193 qDebug() << "Failed to create security-scoped bookmark URL for" << canonicalPath;
194 }
195 }
196 #endif
197 return false;
198 }
199
200 // static
openSecurityToken(const QFileInfo & file,bool create)201 SecurityTokenPointer Sandbox::openSecurityToken(const QFileInfo& file, bool create) {
202 const QString& canonicalFilePath = file.canonicalFilePath();
203 if (sDebug) {
204 qDebug() << "openSecurityToken QFileInfo" << canonicalFilePath << create;
205 }
206
207 if (!enabled()) {
208 return SecurityTokenPointer();
209 }
210
211 QMutexLocker locker(&s_mutex);
212 if (s_pSandboxPermissions == nullptr) {
213 return SecurityTokenPointer();
214 }
215
216 QHash<QString, SecurityTokenWeakPointer>::iterator it = s_activeTokens
217 .find(canonicalFilePath);
218 if (it != s_activeTokens.end()) {
219 SecurityTokenPointer pToken(it.value());
220 if (pToken) {
221 if (sDebug) {
222 qDebug() << "openSecurityToken QFileInfo" << canonicalFilePath
223 << "using cached token for" << pToken->m_path;
224 }
225 return pToken;
226 }
227 }
228
229 if (file.isDir()) {
230 return openSecurityToken(QDir(canonicalFilePath), create);
231 }
232
233 // First, check for a bookmark of the key itself.
234 ConfigKey key = keyForCanonicalPath(canonicalFilePath);
235 if (s_pSandboxPermissions->exists(key)) {
236 return openTokenFromBookmark(
237 canonicalFilePath,
238 s_pSandboxPermissions->getValueString(key));
239 }
240
241 // Next, try to open a bookmark for an existing directory but don't create a
242 // bookmark.
243 SecurityTokenPointer pDirToken = openSecurityToken(file.dir(), false);
244 if (!pDirToken.isNull()) {
245 return pDirToken;
246 }
247
248 if (!create) {
249 return SecurityTokenPointer();
250 }
251
252 // Otherwise, try to create a token.
253 bool created = createSecurityToken(file);
254
255 if (created) {
256 return openTokenFromBookmark(
257 canonicalFilePath,
258 s_pSandboxPermissions->getValueString(key));
259 }
260 return SecurityTokenPointer();
261 }
262
263 // static
openSecurityToken(const QDir & dir,bool create)264 SecurityTokenPointer Sandbox::openSecurityToken(const QDir& dir, bool create) {
265 QDir walkDir = dir;
266 QString walkDirCanonicalPath = walkDir.canonicalPath();
267 if (sDebug) {
268 qDebug() << "openSecurityToken QDir" << walkDirCanonicalPath << create;
269 }
270
271 if (!enabled()) {
272 return SecurityTokenPointer();
273 }
274
275 QMutexLocker locker(&s_mutex);
276 if (s_pSandboxPermissions.isNull()) {
277 return SecurityTokenPointer();
278 }
279
280 while (true) {
281 // Look for a valid token in the cache.
282 QHash<QString, SecurityTokenWeakPointer>::iterator it = s_activeTokens
283 .find(walkDirCanonicalPath);
284 if (it != s_activeTokens.end()) {
285 SecurityTokenPointer pToken(it.value());
286 if (pToken) {
287 if (sDebug) {
288 qDebug() << "openSecurityToken QDir" << walkDirCanonicalPath
289 << "using cached token for" << pToken->m_path;
290 }
291 return pToken;
292 }
293 }
294
295 // Next, check if the key exists in the config.
296 ConfigKey key = keyForCanonicalPath(walkDirCanonicalPath);
297 if (s_pSandboxPermissions->exists(key)) {
298 SecurityTokenPointer pToken = openTokenFromBookmark(
299 dir.canonicalPath(),
300 s_pSandboxPermissions->getValueString(key));
301 if (pToken) {
302 return pToken;
303 }
304 }
305
306 // Go one step higher and repeat.
307 if (!walkDir.cdUp()) {
308 // There's nothing higher. Bail.
309 break;
310 }
311 walkDirCanonicalPath = walkDir.canonicalPath();
312 }
313
314 // Last chance: Try to create a token for this directory.
315 if (create && createSecurityToken(dir.canonicalPath(), true)) {
316 ConfigKey key = keyForCanonicalPath(dir.canonicalPath());
317 return openTokenFromBookmark(
318 dir.canonicalPath(),
319 s_pSandboxPermissions->getValueString(key));
320 }
321 return SecurityTokenPointer();
322 }
323
openTokenFromBookmark(const QString & canonicalPath,const QString & bookmarkBase64)324 SecurityTokenPointer Sandbox::openTokenFromBookmark(const QString& canonicalPath,
325 const QString& bookmarkBase64) {
326 #ifdef __APPLE__
327 QByteArray bookmarkBA = QByteArray::fromBase64(bookmarkBase64.toLatin1());
328 if (!bookmarkBA.isEmpty()) {
329 CFDataRef bookmarkData = CFDataCreate(
330 kCFAllocatorDefault, reinterpret_cast<const UInt8*>(bookmarkBA.constData()),
331 bookmarkBA.length());
332 Boolean stale;
333 CFErrorRef error = NULL;
334 CFURLRef url = CFURLCreateByResolvingBookmarkData(
335 kCFAllocatorDefault, bookmarkData,
336 kCFURLBookmarkResolutionWithSecurityScope, NULL, NULL,
337 &stale, &error);
338 if (error != NULL) {
339 if (sDebug) {
340 qDebug() << "Error creating URL from bookmark data:"
341 << CFStringToQString(CFErrorCopyDescription(error));
342 }
343 }
344 CFRelease(bookmarkData);
345 if (url != NULL) {
346 if (!CFURLStartAccessingSecurityScopedResource(url)) {
347 if (sDebug) {
348 qDebug() << "CFURLStartAccessingSecurityScopedResource failed for"
349 << canonicalPath;
350 }
351 } else {
352 SecurityTokenPointer pToken = SecurityTokenPointer(
353 new SandboxSecurityToken(canonicalPath, url));
354 s_activeTokens[canonicalPath] = pToken;
355 return pToken;
356 }
357 } else {
358 if (sDebug) {
359 qDebug() << "Cannot resolve security-scoped bookmark for" << canonicalPath;
360 }
361 }
362 }
363 #else
364 Q_UNUSED(canonicalPath);
365 Q_UNUSED(bookmarkBase64);
366 #endif
367
368 return SecurityTokenPointer();
369 }
370
371 #ifdef __APPLE__
migrateOldSettings()372 QString Sandbox::migrateOldSettings() {
373 // QStandardPaths::DataLocation returns a different location depending on whether the build
374 // is signed (and therefore sandboxed with the hardened runtime), so use the absolute path
375 // that the sandbox uses regardless of whether this build is actually sandboxed.
376 // Otherwise, developers would need to run with --settingsPath every time or symlink
377 // to use the same settings directory with signed and unsigned builds.
378
379 // QDir::homePath returns a path inside the sandbox when running sandboxed
380 QString homePath = QLatin1String("/Users/") + qgetenv("USER");
381 if (qEnvironmentVariableIsEmpty("USER") || qgetenv("USER").contains("/")) {
382 qCritical() << "Cannot find home directory (USER environment variable invalid)";
383 return QString();
384 }
385
386 QDir homeDir(homePath);
387 if (!homeDir.exists()) {
388 qCritical() << "Home directory does not exist" << homePath;
389 return QString();
390 }
391
392 // The parent of the sandboxed path needs to be created before the legacySettingsPath
393 // can be moved there. This is not necessary when running in a sandbox because macOS
394 // automatically creates it.
395 QString sandboxedParentPath = homePath +
396 QLatin1String(
397 "/Library/Containers/org.mixxx.mixxx/Data/Library/"
398 "Application Support");
399 QString sandboxedPath = sandboxedParentPath + QLatin1String("/Mixxx");
400 QDir sandboxedDir(sandboxedPath);
401
402 QString legacySettingsPath = homePath + QLatin1String("/Library/Application Support/Mixxx");
403 // The user has no settings from Mixxx < 2.3.0, so there is no migration to do.
404 if (!QDir(legacySettingsPath).exists()) {
405 return sandboxedPath;
406 }
407
408 // The user already has settings in the sandboxed path, so there is no migration to do.
409 if (sandboxedDir.exists() && !sandboxedDir.isEmpty()) {
410 return sandboxedPath;
411 }
412
413 // Sandbox::askForAccess cannot be used here because it depends on settings being
414 // initialized. There is no need to store the bookmark anyway because this is a
415 // one time process.
416 QString title = QObject::tr("Upgrading old Mixxx settings");
417 QMessageBox::information(nullptr,
418 title,
419 QObject::tr(
420 "Due to macOS sandboxing, Mixxx needs your permission "
421 "to access your music library and settings from Mixxx "
422 "versions before 2.3.0. After clicking OK, you will see a "
423 "file selection dialog. "
424 "\n\n"
425 "To allow Mixxx to use your old library and settings, "
426 "click the Open button in the file selection dialog. "
427 "Mixxx will then move your old settings into the sandbox. "
428 "This only needs to be done once."
429 "\n\n"
430 "If you do not want to grant Mixxx access, click Cancel "
431 "on the file picker. Mixxx will create a new music library "
432 "and use default settings."));
433
434 QString result = QFileDialog::getExistingDirectory(
435 nullptr,
436 title,
437 legacySettingsPath);
438 if (result != legacySettingsPath) {
439 qInfo() << "Sandbox::migrateOldSettings: User declined to migrate old settings from"
440 << legacySettingsPath << "User selected" << result;
441 return sandboxedPath;
442 }
443
444 CFURLRef url = CFURLCreateWithFileSystemPath(
445 kCFAllocatorDefault, QStringToCFString(legacySettingsPath), kCFURLPOSIXPathStyle, true);
446 if (url) {
447 CFErrorRef error = NULL;
448 if (s_bInSandbox) {
449 // Request permissions to the old unsandboxed sandboxed settings path
450 // and move the directory into the sandbox
451 CFDataRef bookmark = CFURLCreateBookmarkData(
452 kCFAllocatorDefault,
453 url,
454 kCFURLBookmarkCreationWithSecurityScope,
455 nil,
456 nil,
457 &error);
458 CFRelease(url);
459 if (bookmark) {
460 QFile oldSettings(legacySettingsPath);
461 if (oldSettings.rename(sandboxedPath)) {
462 qInfo() << "Sandbox::migrateOldSettings: Successfully "
463 "migrated old settings from"
464 << legacySettingsPath << "to new path" << sandboxedPath;
465 } else {
466 qWarning() << "Sandbox::migrateOldSettings: Failed to migrate "
467 "old settings from"
468 << legacySettingsPath
469 << "to new path" << sandboxedPath;
470 }
471 CFRelease(bookmark);
472 } else {
473 qWarning() << "Sandbox::migrateOldSettings: Failed to access old "
474 "settings path"
475 << legacySettingsPath
476 << "Cannot migrate to new path" << sandboxedPath;
477 }
478 } else {
479 // Move old unsandboxed settings directory into the sandbox
480
481 // Ensure the parent directory of the destination path exists, otherwise
482 // moving to the new path will fail.
483 QDir sandboxedParentDir(sandboxedParentPath);
484 if (!sandboxedParentDir.exists()) {
485 if (!sandboxedParentDir.mkpath(sandboxedParentPath)) {
486 qWarning() << "Could not create sandboxed application data directory"
487 << sandboxedParentPath;
488 }
489 }
490
491 QFile oldSettings(legacySettingsPath);
492 if (oldSettings.rename(sandboxedPath)) {
493 qInfo() << "Sandbox::migrateOldSettings: Successfully "
494 "migrated old settings from"
495 << legacySettingsPath << "to new path"
496 << sandboxedPath;
497 } else {
498 qWarning() << "Sandbox::migrateOldSettings: Failed to migrate old settings from"
499 << legacySettingsPath << "to new path" << sandboxedPath;
500 }
501 }
502 }
503 return sandboxedPath;
504 }
505 #endif
506
507 #ifdef __APPLE__
SandboxSecurityToken(const QString & path,CFURLRef url)508 SandboxSecurityToken::SandboxSecurityToken(const QString& path, CFURLRef url)
509 : m_path(path),
510 m_url(url) {
511 if (m_url) {
512 if (sDebug) {
513 qDebug() << "SandboxSecurityToken successfully opened for" << path;
514 }
515 }
516 }
517 #endif
518
~SandboxSecurityToken()519 SandboxSecurityToken::~SandboxSecurityToken() {
520 #ifdef __APPLE__
521 if (sDebug) {
522 qDebug() << "~SandboxSecurityToken" << m_path;
523 }
524 if (m_url) {
525 CFURLStopAccessingSecurityScopedResource(m_url);
526 CFRelease(m_url);
527 m_url = 0;
528 }
529 #endif
530 }
531