Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/services/accounts/accounts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ abstract class AccountsOptionsInterface {}
///
/// This is used to store wallet backups and the implementation is platform specific.
abstract class AccountsServiceInterface {
final int _version = 4;
final int _version = 5;

int get version => _version;

Expand Down
28 changes: 28 additions & 0 deletions lib/services/accounts/backup.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,31 @@ class BackupWallet {
String get key => '$address@$alias';
String get value => privateKey;
}

class BackupWalletV5 extends BackupWallet {
final String accountFactoryAddress;

BackupWalletV5({
required super.address,
required super.alias,
required super.privateKey,
required String accountFactoryAddress,
}) : accountFactoryAddress =
EthereumAddress.fromHex(accountFactoryAddress).hexEip55;

// Fixes the 'json' super parameter lint
BackupWalletV5.fromJson(super.json)
: accountFactoryAddress =
EthereumAddress.fromHex(json['accountFactoryAddress']).hexEip55,
super.fromJson();

@override
Map<String, dynamic> toJson() {
final json = super.toJson();
json['accountFactoryAddress'] = accountFactoryAddress;
return json;
}

@override
String get key => '$address@$accountFactoryAddress@$alias';
}
78 changes: 78 additions & 0 deletions lib/services/accounts/native/android.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:citizenwallet/services/config/utils.dart';
import 'package:citizenwallet/services/credentials/credentials.dart';
import 'package:citizenwallet/services/db/backup/db.dart';
import 'package:citizenwallet/utils/encrypt.dart';
Expand All @@ -6,6 +7,7 @@ import 'package:citizenwallet/services/db/backup/accounts.dart';

import 'package:citizenwallet/services/accounts/backup.dart';
import 'package:citizenwallet/services/accounts/accounts.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:web3dart/crypto.dart';
Expand Down Expand Up @@ -62,6 +64,7 @@ class AndroidAccountsService extends AccountsServiceInterface {
}

// write the account data in the accounts table
// TODO: use DBAccountV4, with getAccountFactoryAddressByAlias
final account = DBAccount(
alias: legacyBackup.alias,
address: EthereumAddress.fromHex(legacyBackup.address),
Expand Down Expand Up @@ -90,6 +93,81 @@ class AndroidAccountsService extends AccountsServiceInterface {
}
}
},
5: () async {
// Read all credentials from secure storage
final allValues = await _credentials.readAll();

// Filter keys that match the old format: address@alias
// These are keys that don't start with backupPrefix and contain exactly one '@'
final oldFormatKeys = allValues.keys.where((key) {
if (key.startsWith(backupPrefix) ||
key == versionPrefix ||
key == pinCodeKey ||
key == pinCodeCheckKey) {
return false;
}
// Check if it matches address@alias format (one @ symbol)
final parts = key.split('@');
if (parts.length != 2) {
return false;
}

// Validate that the first part is a valid Ethereum address
try {
EthereumAddress.fromHex(parts[0]);
return true;
} catch (_) {
return false;
}
}).toList();

final toDelete = <String>[];

for (final oldKey in oldFormatKeys) {
final privateKeyValue = allValues[oldKey];
if (privateKeyValue == null) {
continue;
}

// Parse the old key format: address@alias
final parts = oldKey.split('@');
if (parts.length != 2) {
continue;
}

final address = parts[0];
final alias = parts[1];

try {
// Get the account factory address for this alias
final accountFactoryAddress =
getAccountFactoryAddressByAlias(alias);

// Create a BackupWalletV5 with the new format
final backup = BackupWalletV5(
address: address,
alias: alias,
accountFactoryAddress: accountFactoryAddress,
privateKey: privateKeyValue,
);

// Write the credential with the new key format
await _credentials.write(backup.key, backup.value);

// Mark old key for deletion
toDelete.add(oldKey);
} catch (e) {
// If we can't determine the account factory address, skip this key
debugPrint('Error migrating key $oldKey: $e');
continue;
}
}

// Delete all old format keys
for (final key in toDelete) {
await _credentials.delete(key);
}
},
};

// run all migrations
Expand Down
78 changes: 78 additions & 0 deletions lib/services/accounts/native/apple.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import 'package:citizenwallet/services/accounts/backup.dart';
import 'package:citizenwallet/services/accounts/accounts.dart';
import 'package:citizenwallet/services/accounts/options.dart';
import 'package:citizenwallet/services/accounts/utils.dart';
import 'package:citizenwallet/services/config/utils.dart';
import 'package:citizenwallet/services/credentials/credentials.dart';
import 'package:citizenwallet/services/credentials/native/apple.dart';
import 'package:citizenwallet/services/db/backup/accounts.dart';
import 'package:citizenwallet/services/db/backup/db.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:web3dart/credentials.dart';
import 'package:web3dart/crypto.dart';
Expand Down Expand Up @@ -166,6 +168,7 @@ class AppleAccountsService extends AccountsServiceInterface {
}

// write the account data in the accounts table
// TODO: use DBAccountV4, with getAccountFactoryAddressByAlias
final DBAccount account = DBAccount(
alias: legacyBackup.alias,
address: EthereumAddress.fromHex(legacyBackup.address),
Expand Down Expand Up @@ -203,6 +206,81 @@ class AppleAccountsService extends AccountsServiceInterface {
}
}
},
5: () async {
// Read all credentials from Keychain
final allValues = await _credentials.readAll();

// Filter keys that match the old format: address@alias
// These are keys that don't start with backupPrefix and contain exactly one '@'
final oldFormatKeys = allValues.keys.where((key) {
if (key.startsWith(backupPrefix) || key == versionPrefix) {
return false;
}
// Check if it matches address@alias format (one @ symbol)
final parts = key.split('@');
if (parts.length != 2) {
return false;
}

// Validate that the first part is a valid Ethereum address
try {
EthereumAddress.fromHex(parts[0]);
return true;
} catch (_) {
return false;
}
}).toList();

final toDelete = <String>[];

for (final oldKey in oldFormatKeys) {
final privateKeyValue = allValues[oldKey];
if (privateKeyValue == null) {
continue;
}

// Parse the old key format: address@alias
final parts = oldKey.split('@');
if (parts.length != 2) {
continue;
}

final address = parts[0];
final alias = parts[1];

try {
// Get the account factory address for this alias
final accountFactoryAddress =
getAccountFactoryAddressByAlias(alias);

// Create a BackupWalletV5 with the new format
final backup = BackupWalletV5(
address: address,
alias: alias,
accountFactoryAddress: accountFactoryAddress,
privateKey: privateKeyValue,
);

// Write the credential with the new key format
await _credentials.write(backup.key, backup.value);

// Mark old key for deletion
toDelete.add(oldKey);
} catch (e) {
// If we can't determine the account factory address, skip this key
debugPrint('Error migrating key $oldKey: $e');
continue;
}
}

// Delete all old format keys
for (final key in toDelete) {
final saved = await _credentials.containsKey(key);
if (saved) {
await _credentials.delete(key);
}
}
},
};

// run all migrations
Expand Down
Loading
Loading