// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/ui/settings/password/password_exporter.h"
#import "base/check.h"
#import "base/files/file_path.h"
#import "base/functional/bind.h"
#import "base/metrics/histogram_macros.h"
#import "base/notreached.h"
#import "base/strings/sys_string_conversions.h"
#import "base/task/thread_pool.h"
#import "base/threading/scoped_blocking_call.h"
#import "components/device_reauth/device_reauth_metrics_util.h"
#import "components/password_manager/core/browser/export/password_csv_writer.h"
#import "components/password_manager/core/browser/password_manager_metrics_util.h"
#import "components/password_manager/core/browser/ui/credential_ui_entry.h"
#import "components/password_manager/core/common/passwords_directory_util_ios.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/common/ui/reauthentication/reauthentication_module.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util_mac.h"
using device_reauth::ReauthResult;
using password_manager::metrics_util::LogPasswordSettingsReauthResult;
namespace {
enum class ReauthenticationStatus {
PENDING,
SUCCESSFUL,
FAILED,
};
} // namespace
@interface PasswordSerializerBridge : NSObject <PasswordSerializerBridge>
@end
@implementation PasswordSerializerBridge
- (void)serializePasswords:
(const std::vector<password_manager::CredentialUIEntry>&)passwords
handler:(void (^)(std::string))serializedPasswordsHandler {
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_BLOCKING},
base::BindOnce(&password_manager::PasswordCSVWriter::SerializePasswords,
std::move(passwords)),
base::BindOnce(serializedPasswordsHandler));
}
@end
@interface PasswordFileWriter : NSObject <FileWriterProtocol>
@end
@implementation PasswordFileWriter
- (void)writeData:(NSData*)data
toURL:(NSURL*)fileURL
handler:(void (^)(WriteToURLStatus))handler {
WriteToURLStatus (^writeToFile)() = ^{
NSError* error = nil;
NSURL* directoryURL = [fileURL URLByDeletingLastPathComponent];
NSFileManager* fileManager = [NSFileManager defaultManager];
base::ScopedBlockingCall scoped_blocking_call(
FROM_HERE, base::BlockingType::WILL_BLOCK);
if (![fileManager createDirectoryAtURL:directoryURL
withIntermediateDirectories:YES
attributes:nil
error:nil]) {
return WriteToURLStatus::UNKNOWN_ERROR;
}
BOOL success = [data
writeToURL:fileURL
options:
(NSDataWritingAtomic |
NSDataWritingFileProtectionCompleteUntilFirstUserAuthentication)
error:&error];
if (!success) {
if (error.code == NSFileWriteOutOfSpaceError) {
return WriteToURLStatus::OUT_OF_DISK_SPACE_ERROR;
} else {
return WriteToURLStatus::UNKNOWN_ERROR;
}
}
return WriteToURLStatus::SUCCESS;
};
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_BLOCKING},
base::BindOnce(writeToFile), base::BindOnce(handler));
}
@end
@interface PasswordExporter () {
// Module containing the reauthentication mechanism used for exporting
// passwords.
__weak id<ReauthenticationProtocol> _weakReauthenticationModule;
// Instance of the view controller initiating the export. Used
// for displaying alerts.
__weak id<PasswordExporterDelegate> _weakDelegate;
// Name of the temporary passwords file. It can be used by the receiving app,
// so it needs to be a localized string.
NSString* _tempPasswordsFileName;
// Bridge object that triggers password serialization and executes a
// handler on the serialized passwords.
id<PasswordSerializerBridge> _passwordSerializerBridge;
// Object that writes data to a file asyncronously and executes a handler
// block when finished.
id<FileWriterProtocol> _passwordFileWriter;
}
// Contains the status of the reauthentication flow.
@property(nonatomic, assign) ReauthenticationStatus reauthenticationStatus;
// Whether the password serializing has finished.
@property(nonatomic, assign) BOOL serializingFinished;
// String containing serialized password forms.
@property(nonatomic, copy) NSString* serializedPasswords;
// The exporter state.
@property(nonatomic, assign) ExportState exportState;
// The number of passwords that are exported. Used for metrics.
@property(nonatomic, assign) int passwordCount;
@end
@implementation PasswordExporter
// Public synthesized properties
@synthesize exportState = _exportState;
// Private synthesized properties
@synthesize reauthenticationStatus = _reauthenticationStatus;
@synthesize serializingFinished = _serializingFinished;
@synthesize serializedPasswords = _serializedPasswords;
@synthesize passwordCount = _passwordCount;
- (instancetype)initWithReauthenticationModule:
(id<ReauthenticationProtocol>)reauthenticationModule
delegate:(id<PasswordExporterDelegate>)
delegate {
DCHECK(delegate);
DCHECK(reauthenticationModule);
self = [super init];
if (self) {
_tempPasswordsFileName =
[l10n_util::GetNSString(IDS_PASSWORD_MANAGER_DEFAULT_EXPORT_FILENAME)
stringByAppendingString:@".csv"];
_passwordSerializerBridge = [[PasswordSerializerBridge alloc] init];
_passwordFileWriter = [[PasswordFileWriter alloc] init];
_weakReauthenticationModule = reauthenticationModule;
_weakDelegate = delegate;
[self resetExportState];
}
return self;
}
- (void)startExportFlow:
(const std::vector<password_manager::CredentialUIEntry>&)passwords {
DCHECK(!passwords.empty());
DCHECK(self.exportState == ExportState::IDLE);
if ([_weakReauthenticationModule canAttemptReauth]) {
self.exportState = ExportState::ONGOING;
[_weakDelegate updateExportPasswordsButton];
[self serializePasswords:std::move(passwords)];
[self startReauthentication];
} else {
[_weakDelegate showSetPasscodeForPasswordExportDialog];
}
}
- (void)cancelExport {
self.exportState = ExportState::CANCELLING;
}
#pragma mark - Private methods
- (void)showExportErrorAlertWithLocalizedReason:(NSString*)errorReason {
[_weakDelegate showExportErrorAlertWithLocalizedReason:errorReason];
}
- (void)serializePasswords:
(const std::vector<password_manager::CredentialUIEntry>&)passwords {
self.passwordCount = passwords.size();
__weak PasswordExporter* weakSelf = self;
void (^onPasswordsSerialized)(std::string) =
^(std::string serializedPasswords) {
PasswordExporter* strongSelf = weakSelf;
if (!strongSelf)
return;
strongSelf.serializedPasswords =
base::SysUTF8ToNSString(serializedPasswords);
strongSelf.serializingFinished = YES;
[strongSelf tryExporting];
};
[_passwordSerializerBridge serializePasswords:std::move(passwords)
handler:onPasswordsSerialized];
}
- (void)startReauthentication {
__weak PasswordExporter* weakSelf = self;
void (^onReauthenticationFinished)(ReauthenticationResult) =
^(ReauthenticationResult result) {
DCHECK(result != ReauthenticationResult::kSkipped);
PasswordExporter* strongSelf = weakSelf;
if (!strongSelf)
return;
if (result == ReauthenticationResult::kSuccess) {
LogPasswordSettingsReauthResult(ReauthResult::kSuccess);
strongSelf.reauthenticationStatus =
ReauthenticationStatus::SUCCESSFUL;
[strongSelf showPreparingPasswordsAlert];
} else {
LogPasswordSettingsReauthResult(ReauthResult::kFailure);
strongSelf.reauthenticationStatus = ReauthenticationStatus::FAILED;
}
[strongSelf tryExporting];
};
[_weakReauthenticationModule
attemptReauthWithLocalizedReason:l10n_util::GetNSString(
IDS_IOS_EXPORT_PASSWORDS)
canReusePreviousAuth:NO
handler:onReauthenticationFinished];
}
- (void)showPreparingPasswordsAlert {
[_weakDelegate showPreparingPasswordsAlert];
}
- (void)tryExporting {
if (!self.serializingFinished)
return;
switch (self.reauthenticationStatus) {
case ReauthenticationStatus::PENDING:
return;
case ReauthenticationStatus::SUCCESSFUL:
[self writePasswordsToFile];
break;
case ReauthenticationStatus::FAILED:
[self resetExportState];
break;
default:
NOTREACHED_IN_MIGRATION();
}
}
- (void)resetExportState {
self.serializingFinished = NO;
self.serializedPasswords = nil;
self.passwordCount = 0;
self.reauthenticationStatus = ReauthenticationStatus::PENDING;
self.exportState = ExportState::IDLE;
[_weakDelegate updateExportPasswordsButton];
}
- (void)writePasswordsToFile {
if (self.exportState == ExportState::CANCELLING) {
[self resetExportState];
return;
}
base::FilePath filePath;
if (!password_manager::GetPasswordsDirectory(&filePath)) {
[self showExportErrorAlertWithLocalizedReason:
l10n_util::GetNSString(
IDS_IOS_EXPORT_PASSWORDS_UNKNOWN_ERROR_ALERT_MESSAGE)];
[self resetExportState];
return;
}
NSString* filePathString =
[NSString stringWithUTF8String:filePath.value().c_str()];
NSURL* uniqueDirectoryURL = [[NSURL fileURLWithPath:filePathString]
URLByAppendingPathComponent:[[NSUUID UUID] UUIDString]
isDirectory:YES];
NSURL* passwordsTempFileURL =
[uniqueDirectoryURL URLByAppendingPathComponent:_tempPasswordsFileName
isDirectory:NO];
__weak PasswordExporter* weakSelf = self;
void (^onFileWritten)(WriteToURLStatus) = ^(WriteToURLStatus status) {
PasswordExporter* strongSelf = weakSelf;
if (!strongSelf) {
return;
}
if (strongSelf.exportState == ExportState::CANCELLING) {
[strongSelf resetExportState];
return;
}
switch (status) {
case WriteToURLStatus::SUCCESS:
[strongSelf showActivityView:passwordsTempFileURL];
break;
case WriteToURLStatus::OUT_OF_DISK_SPACE_ERROR:
[strongSelf
showExportErrorAlertWithLocalizedReason:
l10n_util::GetNSString(
IDS_IOS_EXPORT_PASSWORDS_OUT_OF_SPACE_ALERT_MESSAGE)];
[strongSelf resetExportState];
break;
case WriteToURLStatus::UNKNOWN_ERROR:
[strongSelf
showExportErrorAlertWithLocalizedReason:
l10n_util::GetNSString(
IDS_IOS_EXPORT_PASSWORDS_UNKNOWN_ERROR_ALERT_MESSAGE)];
[strongSelf resetExportState];
break;
default:
NOTREACHED_IN_MIGRATION();
}
};
NSData* serializedPasswordsData =
[self.serializedPasswords dataUsingEncoding:NSUTF8StringEncoding];
// Drop `serializedPasswords` as it is no longer needed.
self.serializedPasswords = nil;
[_passwordFileWriter writeData:serializedPasswordsData
toURL:passwordsTempFileURL
handler:onFileWritten];
}
- (void)deleteTemporaryFile:(NSURL*)passwordsTempFileURL {
NSURL* uniqueDirectoryURL =
[passwordsTempFileURL URLByDeletingLastPathComponent];
base::ThreadPool::PostTask(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::BindOnce(^{
NSFileManager* fileManager = [NSFileManager defaultManager];
base::ScopedBlockingCall scoped_blocking_call(
FROM_HERE, base::BlockingType::WILL_BLOCK);
[fileManager removeItemAtURL:uniqueDirectoryURL error:nil];
}));
}
- (void)showActivityView:(NSURL*)passwordsTempFileURL {
if (self.exportState == ExportState::CANCELLING) {
[self deleteTemporaryFile:passwordsTempFileURL];
[self resetExportState];
return;
}
__weak PasswordExporter* weakSelf = self;
[_weakDelegate
showActivityViewWithActivityItems:@[ passwordsTempFileURL ]
completionHandler:^(
NSString* activityType, BOOL completed,
NSArray* returnedItems, NSError* activityError) {
[weakSelf deleteTemporaryFile:passwordsTempFileURL];
}];
}
#pragma mark - ForTesting
- (void)setPasswordSerializerBridge:
(id<PasswordSerializerBridge>)passwordSerializerBridge {
_passwordSerializerBridge = passwordSerializerBridge;
}
- (void)setPasswordFileWriter:(id<FileWriterProtocol>)passwordFileWriter {
_passwordFileWriter = passwordFileWriter;
}
@end