Skip to content

Commit 661540d

Browse files
authored
Improve robustness around dmg passwords (#2627)
* Handle failing to extract password protected disk images even when a decryption password isn't provided. * Propagate error with more information when hdiutil attach fails. * Wait for detaching disk images for unit tests (fixes not being able to run tests repeatably).
1 parent a5c26b5 commit 661540d

File tree

8 files changed

+74
-28
lines changed

8 files changed

+74
-28
lines changed

Autoupdate/AppInstaller.m

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ - (void)extractAndInstallUpdate SPU_OBJC_DIRECT
236236

237237
[self->_communicator handleMessageWithIdentifier:SPUExtractedArchiveWithProgress data:data];
238238
}
239-
}];
239+
} waitForCleanup:NO];
240240
}
241241
}
242242

@@ -499,6 +499,7 @@ - (void)handleMessageWithIdentifier:(int32_t)identifier data:(NSData *)data
499499
self->_signatures = installationData.signatures;
500500
self->_updateDirectoryPath = cacheInstallationPath;
501501
self->_extractionDirectory = extractionDirectory;
502+
self->_decryptionPassword = installationData.decryptionPassword;
502503
self->_host = [[SUHost alloc] initWithBundle:hostBundle];
503504
self->_verifierInformation = [[SPUVerifierInformation alloc] initWithExpectedVersion:installationData.expectedVersion expectedContentLength:installationData.expectedContentLength];
504505

Autoupdate/SUBinaryDeltaUnarchiver.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ - (instancetype)initWithArchivePath:(NSString *)archivePath extractionDirectory:
8383
return self;
8484
}
8585

86-
- (void)unarchiveWithCompletionBlock:(void (^)(NSError * _Nullable))completionBlock progressBlock:(void (^ _Nullable)(double))progressBlock
86+
- (void)unarchiveWithCompletionBlock:(void (^)(NSError * _Nullable))completionBlock progressBlock:(void (^ _Nullable)(double))progressBlock waitForCleanup:(BOOL)__unused waitForCleanup
8787
{
8888
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
8989
@autoreleasepool {

Autoupdate/SUDiskImageUnarchiver.m

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#import "SUDiskImageUnarchiver.h"
1212
#import "SUUnarchiverNotifier.h"
1313
#import "SULog.h"
14+
#import "SUErrors.h"
1415

1516

1617
#include "AppKitPrevention.h"
@@ -50,11 +51,11 @@ - (instancetype)initWithArchivePath:(NSString *)archivePath extractionDirectory:
5051
return self;
5152
}
5253

53-
- (void)unarchiveWithCompletionBlock:(void (^)(NSError * _Nullable))completionBlock progressBlock:(void (^ _Nullable)(double))progressBlock
54+
- (void)unarchiveWithCompletionBlock:(void (^)(NSError * _Nullable))completionBlock progressBlock:(void (^ _Nullable)(double))progressBlock waitForCleanup:(BOOL)waitForCleanup
5455
{
5556
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
5657
SUUnarchiverNotifier *notifier = [[SUUnarchiverNotifier alloc] initWithCompletionBlock:completionBlock progressBlock:progressBlock];
57-
[self extractDMGWithNotifier:notifier];
58+
[self extractDMGWithNotifier:notifier waitForCleanup:waitForCleanup];
5859
});
5960
}
6061

@@ -78,7 +79,7 @@ - (BOOL)fileManager:(NSFileManager *)fileManager shouldCopyItemAtURL:(NSURL *)sr
7879
}
7980

8081
// Called on a non-main thread.
81-
- (void)extractDMGWithNotifier:(SUUnarchiverNotifier *)notifier SPU_OBJC_DIRECT
82+
- (void)extractDMGWithNotifier:(SUUnarchiverNotifier *)notifier waitForCleanup:(BOOL)waitForCleanup SPU_OBJC_DIRECT
8283
{
8384
@autoreleasepool {
8485
BOOL mountedSuccessfully = NO;
@@ -95,25 +96,32 @@ - (void)extractDMGWithNotifier:(SUUnarchiverNotifier *)notifier SPU_OBJC_DIRECT
9596
// Note: this check does not follow symbolic links, which is what we want
9697
while ([[NSURL fileURLWithPath:mountPoint] checkResourceIsReachableAndReturnError:NULL]);
9798

98-
NSData *promptData = [NSData dataWithBytes:"yes\n" length:4];
99+
NSMutableData *inputData = [NSMutableData data];
99100

100-
// Finder doesn't verify disk images anymore beyond the code signing signature (if available)
101-
// Opt out of the old CRC checksum checks
102-
NSMutableArray *arguments = [@[@"attach", _archivePath, @"-mountpoint", mountPoint, @"-noverify", @"-nobrowse", @"-noautoopen"] mutableCopy];
103-
104-
if (_decryptionPassword) {
105-
NSMutableData *passwordData = [[_decryptionPassword dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
101+
// Prepare stdin data for passwords and license agreements
102+
{
103+
// If no password is supplied, we will still be asked a password.
104+
// In that case we respond with an empty password.
105+
NSData *decryptionPasswordData = [_decryptionPassword dataUsingEncoding:NSUTF8StringEncoding];
106+
if (decryptionPasswordData != nil) {
107+
[inputData appendData:decryptionPasswordData];
108+
}
109+
106110
// From the hdiutil docs:
107111
// read a null-terminated passphrase from standard input
108112
//
109-
// Add the null terminator, then the newline
110-
[passwordData appendData:[NSData dataWithBytes:"\0" length:1]];
111-
[passwordData appendData:promptData];
112-
promptData = passwordData;
113+
// Add the null terminator
114+
[inputData appendBytes:"\0" length:1];
113115

114-
[arguments addObject:@"-stdinpass"];
116+
// Append prompt data for license agreements
117+
[inputData appendBytes:"yes\n" length:4];
115118
}
116119

120+
// Finder doesn't verify disk images anymore beyond the code signing signature (if available)
121+
// Opt out of the old CRC checksum checks
122+
// Also always pass -stdinpass so we gracefully handle password protected disk images even if we aren't expecting them
123+
NSArray *arguments = @[@"attach", _archivePath, @"-mountpoint", mountPoint, @"-noverify", @"-nobrowse", @"-noautoopen", @"-stdinpass"];
124+
117125
NSData *output = nil;
118126
NSInteger taskResult = -1;
119127

@@ -153,15 +161,15 @@ - (void)extractDMGWithNotifier:(SUUnarchiverNotifier *)notifier SPU_OBJC_DIRECT
153161
}
154162

155163
if (@available(macOS 10.15, *)) {
156-
if (![inputPipe.fileHandleForWriting writeData:promptData error:&error]) {
164+
if (![inputPipe.fileHandleForWriting writeData:inputData error:&error]) {
157165
goto reportError;
158166
}
159167
}
160168
#if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_15
161169
else
162170
{
163171
@try {
164-
[inputPipe.fileHandleForWriting writeData:promptData];
172+
[inputPipe.fileHandleForWriting writeData:inputData];
165173
} @catch (NSException *) {
166174
goto reportError;
167175
}
@@ -175,12 +183,16 @@ - (void)extractDMGWithNotifier:(SUUnarchiverNotifier *)notifier SPU_OBJC_DIRECT
175183

176184
taskResult = task.terminationStatus;
177185
}
178-
186+
179187
if (taskResult != 0) {
180188
NSString *resultStr = output ? [[NSString alloc] initWithData:output encoding:NSUTF8StringEncoding] : nil;
181189
SULog(SULogLevelError, @"hdiutil failed with code: %ld data: <<%@>>", (long)taskResult, resultStr);
190+
191+
error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUUnarchivingError userInfo:@{NSLocalizedDescriptionKey:[NSString stringWithFormat:@"Extraction failed due to hdiutil returning %ld status: %@", (long)taskResult, resultStr]}];
192+
182193
goto reportError;
183194
}
195+
184196
mountedSuccessfully = YES;
185197

186198
// Mounting can take some time, so increment progress
@@ -271,11 +283,11 @@ - (void)extractDMGWithNotifier:(SUUnarchiverNotifier *)notifier SPU_OBJC_DIRECT
271283

272284
[notifier notifyProgress:1.0];
273285

274-
[notifier notifySuccess];
286+
BOOL success = YES;
275287
goto finally;
276288

277289
reportError:
278-
[notifier notifyFailureWithError:error];
290+
success = NO;
279291

280292
finally:
281293
if (mountedSuccessfully) {
@@ -289,10 +301,18 @@ - (void)extractDMGWithNotifier:(SUUnarchiverNotifier *)notifier SPU_OBJC_DIRECT
289301
if (![task launchAndReturnError:&launchCleanupError]) {
290302
SULog(SULogLevelError, @"Failed to unmount %@", mountPoint);
291303
SULog(SULogLevelError, @"Error: %@", launchCleanupError);
304+
} else if (waitForCleanup) {
305+
[task waitUntilExit];
292306
}
293307
} else {
294308
SULog(SULogLevelError, @"Can't mount DMG %@", _archivePath);
295309
}
310+
311+
if (success) {
312+
[notifier notifySuccess];
313+
} else {
314+
[notifier notifyFailureWithError:error];
315+
}
296316
}
297317
}
298318

Autoupdate/SUFlatPackageUnarchiver.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ - (instancetype)initWithFlatPackagePath:(NSString *)flatPackagePath extractionDi
4444
return self;
4545
}
4646

47-
- (void)unarchiveWithCompletionBlock:(void (^)(NSError * _Nullable))completionBlock progressBlock:(void (^ _Nullable)(double))progressBlock
47+
- (void)unarchiveWithCompletionBlock:(void (^)(NSError * _Nullable))completionBlock progressBlock:(void (^ _Nullable)(double))progressBlock waitForCleanup:(BOOL)__unused waitForCleanup
4848
{
4949
SUUnarchiverNotifier *notifier = [[SUUnarchiverNotifier alloc] initWithCompletionBlock:completionBlock progressBlock:progressBlock];
5050

Autoupdate/SUPipedUnarchiver.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ - (instancetype)initWithArchivePath:(NSString *)archivePath extractionDirectory:
9696
return self;
9797
}
9898

99-
- (void)unarchiveWithCompletionBlock:(void (^)(NSError * _Nullable))completionBlock progressBlock:(void (^ _Nullable)(double))progressBlock
99+
- (void)unarchiveWithCompletionBlock:(void (^)(NSError * _Nullable))completionBlock progressBlock:(void (^ _Nullable)(double))progressBlock waitForCleanup:(BOOL)__unused waitForCleanup
100100
{
101101
NSString *command = nil;
102102
NSArray<NSString *> *arguments = _argumentsConformingToTypeOfPath(_archivePath, YES, &command);

Autoupdate/SUUnarchiverProtocol.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ NS_ASSUME_NONNULL_BEGIN
1414

1515
+ (BOOL)mustValidateBeforeExtractionWithArchivePath:(NSString *)archivePath;
1616

17-
- (void)unarchiveWithCompletionBlock:(void (^)(NSError * _Nullable))completionBlock progressBlock:(void (^ _Nullable)(double))progressBlock;
17+
- (void)unarchiveWithCompletionBlock:(void (^)(NSError * _Nullable))completionBlock progressBlock:(void (^ _Nullable)(double))progressBlock waitForCleanup:(BOOL)waitForCleanup;
1818

1919
- (NSString *)description;
2020

Tests/SUUnarchiverTest.swift

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class SUUnarchiverTest: XCTestCase
5050
unarchiver.unarchive(completionBlock: {(error: Error?) -> Void in
5151
XCTAssertNotNil(error)
5252
testExpectation.fulfill()
53-
}, progressBlock: nil)
53+
}, progressBlock: nil, waitForCleanup: true)
5454
}
5555

5656
// swiftlint:disable function_parameter_count
@@ -65,7 +65,7 @@ class SUUnarchiverTest: XCTestCase
6565
XCTAssertNotNil(error)
6666
}
6767
testExpectation.fulfill()
68-
}, progressBlock: nil)
68+
}, progressBlock: nil, waitForCleanup: true)
6969
}
7070

7171
func testUnarchivingZip()
@@ -132,11 +132,36 @@ class SUUnarchiverTest: XCTestCase
132132
self.unarchiveTestAppWithExtension("enc.nolicense.dmg", password: "testpass")
133133
}
134134

135+
func testUnarchivingEncryptedDmgWithLicenseAndWithIncorrectPassword()
136+
{
137+
self.unarchiveTestAppWithExtension("enc.dmg", password: "moo", expectingSuccess: false)
138+
}
139+
140+
func testUnarchivingEncryptedDmgWithLicenseAndWithoutPassword()
141+
{
142+
self.unarchiveTestAppWithExtension("enc.dmg", expectingSuccess: false)
143+
}
144+
145+
func testUnarchivingEncryptedDmgWithoutLicenseAndWithIncorrectPassword()
146+
{
147+
self.unarchiveTestAppWithExtension("enc.nolicense.dmg", password: "moo", expectingSuccess: false)
148+
}
149+
150+
func testUnarchivingEncryptedDmgWithoutLicenseAndWithoutPassword()
151+
{
152+
self.unarchiveTestAppWithExtension("enc.nolicense.dmg", expectingSuccess: false)
153+
}
154+
135155
func testUnarchivingAPFSDMG()
136156
{
137157
self.unarchiveTestAppWithExtension("dmg", resourceName: "SparkleTestCodeSign_apfs")
138158
}
139159

160+
func testUnarchivingAPFSDMGWithBogusPassword()
161+
{
162+
self.unarchiveTestAppWithExtension("dmg", password: "moo", resourceName: "SparkleTestCodeSign_apfs")
163+
}
164+
140165
func testUnarchivingAPFSAdhocSignedDMGWithAuxFiles()
141166
{
142167
self.unarchiveTestAppWithExtension("dmg", resourceName: "SparkleTestCodeSign_apfs_lzma_aux_files")

generate_appcast/Unarchive.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func unarchive(itemPath: URL, archiveDestDir: URL, callback: @escaping (Error?)
2525
} catch {
2626
callback(error)
2727
}
28-
}, progressBlock: nil)
28+
}, progressBlock: nil, waitForCleanup: true)
2929
} else {
3030
callback(makeError(code: .unarchivingError, "Not a supported archive format: \(itemPath.path)"))
3131
}

0 commit comments

Comments
 (0)