blob: cb2309d7c8d31d8369b527126c40210bfa555041 [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:fidl/fidl.dart';
import 'package:ermine_utils/ermine_utils.dart';
import 'package:fidl_fuchsia_identity_account/fidl_async.dart';
import 'package:fidl_fuchsia_io/fidl_async.dart';
import 'package:fidl_fuchsia_recovery/fidl_async.dart';
import 'package:fuchsia_logger/logger.dart';
import 'package:fuchsia_services/services.dart';
import 'package:fuchsia_vfs/vfs.dart';
import 'package:internationalization/strings.dart';
import 'package:mobx/mobx.dart';
import 'package:zircon/zircon.dart';
const kDeprecatedAccountName = 'created_by_user';
const kSystemPickedAccountName = 'picked_by_system';
const kUserPickedAccountName = 'picked_by_user';
const kAccountDataDirectory = 'account_data';
const kAccountCacheDirectory = 'account_cache';
const kCacheSubdirectory = 'cache/';
enum AuthMode { automatic, manual }
/// Defines a service that performs authentication tasks like:
/// - create an account with password
/// - login to an account with password
/// - logout from an account
///
/// Note:
/// - It always picks the first account for login and logout.
/// - Creating an account, when an account already exists, is an undefined
/// behavior. The client of the service should ensure to not call account
/// creation in this case.
class AuthService {
late final PseudoDir hostedDirectories;
final _accountManager = AccountManagerProxy();
AccountProxy? _account;
final _accountIds = <int>[];
final _ready = false.asObservable();
AuthService() {
Incoming.fromSvcPath().connectToService(_accountManager);
}
void dispose() {
_accountManager.ctrl.close();
}
/// Load existing accounts from [AccountManager].
void loadAccounts(AuthMode currentAuthMode) async {
try {
final ids = (await _accountManager.getAccountIds()).toList();
// TODO(http://fxb/85576): Remove once login and OOBE are mandatory.
// Remove any accounts created with a deprecated name or from an auth
// mode that does not match the current build configuration.
final tempIds = <int>[]..addAll(ids);
for (var id in tempIds) {
final metadata = await _accountManager.getAccountMetadata(id);
if (metadata.name != null &&
_shouldRemoveAccountWithName(metadata.name!, currentAuthMode)) {
try {
await _accountManager.removeAccount(id, true);
ids.remove(id);
log.info('Removed account: $id with name: ${metadata.name}');
// ignore: avoid_catches_without_on_clauses
} catch (e) {
// We can only log and continue.
log.shout('Failed during deprecated account removal: $e');
}
}
}
_accountIds.addAll(ids);
runInAction(() => _ready.value = true);
if (ids.length > 1) {
log.shout(
'Multiple (${ids.length}) accounts found, will use the first.');
}
// ignore: avoid_catches_without_on_clauses
} catch (e) {
log.shout('Failed during deprecated account removal: $e');
}
}
bool _shouldRemoveAccountWithName(String name, AuthMode currentAuthMode) {
if (name == kDeprecatedAccountName) {
return true;
}
if (currentAuthMode == AuthMode.automatic) {
// Current auth is automatic, remove account with user picked name.
return name == kUserPickedAccountName;
} else {
// Current auth is manual, remove account with system picked name.
return name == kSystemPickedAccountName;
}
}
/// Calls [FactoryReset] service to factory data reset the device.
void factoryReset() {
final proxy = FactoryResetProxy();
Incoming.fromSvcPath().connectToService(proxy);
proxy
.reset()
.then((status) => log.info('Requested factory reset.'))
.catchError((e) => log.shout('Failed to factory reset device: $e'));
}
/// Returns [true] after [_accountManager.getAccountIds()] completes.
bool get ready => _ready.value;
/// Returns [true] if no accounts exists on device.
bool get hasAccount {
assert(ready, 'Called before list of accounts could be retrieved.');
return _accountIds.isNotEmpty;
}
String errorFromException(Object e) {
if (e is MethodException) {
switch (e.value as Error) {
case Error.failedAuthentication:
return Strings.accountPasswordFailedAuthentication;
case Error.notFound:
return Strings.accountPartitionNotFound;
}
}
return e.toString();
}
/// Creates an account with password and sets up the account data directory.
Future<void> createAccountWithPassword(String password) async {
assert(_account == null, 'An account already exists.');
if (_account != null && _account!.ctrl.isBound) {
// ignore: unawaited_futures
_account!.lock().catchError((_) {});
_account!.ctrl.close();
}
final metadata = AccountMetadata(
name: password.isEmpty
? kSystemPickedAccountName
: kUserPickedAccountName);
_account = AccountProxy();
await _accountManager.deprecatedProvisionNewAccount(
password,
metadata,
_account!.ctrl.request(),
);
final ids = await _accountManager.getAccountIds();
_accountIds
..clear()
..addAll(ids);
log.info('Account creation succeeded.');
await _publishAccountDirectory(_account!);
}
/// Logs in to the first account with [password] and sets up the account data
/// directory.
Future<void> loginWithPassword(String password) async {
assert(_accountIds.isNotEmpty, 'No account exist to login to.');
if (_account != null && _account!.ctrl.isBound) {
// ignore: unawaited_futures
_account!.lock().catchError((_) {});
_account!.ctrl.close();
}
_account = AccountProxy();
await _accountManager.deprecatedGetAccount(
_accountIds.first,
password,
_account!.ctrl.request(),
);
log.info('Login to first account on device succeeded.');
await _publishAccountDirectory(_account!);
}
/// Logs out of an account by locking it.
Future<void> logout() async {
assert(_account != null, 'No account exists to logout from.');
return _account!.lock();
}
Future<void> _publishAccountDirectory(Account account) async {
// Get the data directory for the account.
log.info('Getting data directory for account.');
final dataDirChannel = ChannelPair();
await account.getDataDirectory(InterfaceRequest(dataDirChannel.second));
// Open or create a subdirectory for the cache storage capability.
log.info('Opening cache directory for account.');
final dataDir = RemoteDir(dataDirChannel.first!);
final cacheDirChannel = ChannelPair();
dataDir.open(
openRightReadable |
openRightWritable |
openFlagCreate |
openFlagDirectory,
0,
kCacheSubdirectory,
InterfaceRequest(cacheDirChannel.second));
// Host both directories.
hostedDirectories
..removeNode(kAccountDataDirectory)
..addNode(kAccountDataDirectory, dataDir)
..removeNode(kAccountCacheDirectory)
..addNode(kAccountCacheDirectory, RemoteDir(cacheDirChannel.first!));
log.info('Data and cache directories for account published.');
}
}