Final metadata encryption keys code & fixes
This commit is contained in:
parent
ff6ea3e0fe
commit
f2ab4c85c9
|
@ -309,7 +309,7 @@ extension SDKSynchronizerClient {
|
|||
}
|
||||
|
||||
var clearedTxs: [TransactionState] = []
|
||||
|
||||
|
||||
let latestBlockHeight = try await SDKSynchronizerClient.latestBlockHeight(synchronizer: synchronizer)
|
||||
|
||||
for clearedTransaction in clearedTransactions {
|
||||
|
|
|
@ -89,7 +89,7 @@ public extension UserMetadata {
|
|||
static func encryptUserMetadata(_ umData: UserMetadata, account: Account) throws -> Data {
|
||||
@Dependency(\.walletStorage) var walletStorage
|
||||
|
||||
guard let encryptionKeys = try? walletStorage.exportUserMetadataEncryptionKeys(),
|
||||
guard let encryptionKeys = try? walletStorage.exportUserMetadataEncryptionKeys(account),
|
||||
let umKey = encryptionKeys.getCached(account: account) else {
|
||||
throw UserMetadataStorage.UMError.missingEncryptionKey
|
||||
}
|
||||
|
@ -131,7 +131,7 @@ public extension UserMetadata {
|
|||
static func userMetadataFrom(encryptedData: Data, account: Account) throws -> UserMetadata? {
|
||||
@Dependency(\.walletStorage) var walletStorage
|
||||
|
||||
guard let encryptionKeys = try? walletStorage.exportUserMetadataEncryptionKeys(),
|
||||
guard let encryptionKeys = try? walletStorage.exportUserMetadataEncryptionKeys(account),
|
||||
let umKey = encryptionKeys.getCached(account: account) else {
|
||||
throw UserMetadataStorage.UMError.missingEncryptionKey
|
||||
}
|
||||
|
@ -151,32 +151,39 @@ public extension UserMetadata {
|
|||
}
|
||||
|
||||
let encryptedSubData = try UserMetadata.subdata(of: encryptedData, in: offset..<encryptedData.count)
|
||||
offset = 0
|
||||
|
||||
// Derive the sub-key for decrypting the user metadata.
|
||||
let salt = encryptedSubData.prefix(upTo: 32)
|
||||
let subKey = umKey.deriveEncryptionKey(salt: salt)
|
||||
offset += 32
|
||||
|
||||
// Deserialize `metadata version`
|
||||
let metadataVersionBytes = try UserMetadata.subdata(of: encryptedSubData, in: offset..<(offset + UserMetadataStorage.Constants.int64Size))
|
||||
offset += UserMetadataStorage.Constants.int64Size
|
||||
|
||||
guard let metadataVersion = UserMetadata.bytesToInt(Array(metadataVersionBytes)) else {
|
||||
return nil
|
||||
}
|
||||
let subKeys = umKey.deriveDecryptionKeys(salt: salt)
|
||||
|
||||
guard metadataVersion == UserMetadata.Constants.version else {
|
||||
throw UserMetadataStorage.UMError.metadataVersionNotSupported
|
||||
}
|
||||
|
||||
// Unseal the encrypted user metadata.
|
||||
let sealed = try ChaChaPoly.SealedBox.init(combined: encryptedSubData.suffix(from: 32 + UserMetadataStorage.Constants.int64Size))
|
||||
let data = try ChaChaPoly.open(sealed, using: subKey)
|
||||
|
||||
// deserialize the json's data
|
||||
if let decodedUM = try? JSONDecoder().decode(UserMetadata.self, from: data) {
|
||||
return decodedUM
|
||||
for subKey in subKeys {
|
||||
offset = 32
|
||||
|
||||
do {
|
||||
// Deserialize `metadata version`
|
||||
let metadataVersionBytes = try UserMetadata.subdata(of: encryptedSubData, in: offset..<(offset + UserMetadataStorage.Constants.int64Size))
|
||||
offset += UserMetadataStorage.Constants.int64Size
|
||||
|
||||
guard let metadataVersion = UserMetadata.bytesToInt(Array(metadataVersionBytes)) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard metadataVersion == UserMetadata.Constants.version else {
|
||||
throw UserMetadataStorage.UMError.metadataVersionNotSupported
|
||||
}
|
||||
|
||||
// Unseal the encrypted user metadata.
|
||||
let sealed = try ChaChaPoly.SealedBox.init(combined: encryptedSubData.suffix(from: 32 + UserMetadataStorage.Constants.int64Size))
|
||||
let data = try ChaChaPoly.open(sealed, using: subKey)
|
||||
|
||||
// deserialize the json's data
|
||||
if let decodedUM = try? JSONDecoder().decode(UserMetadata.self, from: data) {
|
||||
return decodedUM
|
||||
}
|
||||
} catch {
|
||||
// this key failed to decrypt, try another one
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -45,12 +45,12 @@ public class UserMetadataStorage {
|
|||
func filenameForEncryptedFile(account: Account) throws -> String {
|
||||
@Dependency(\.walletStorage) var walletStorage
|
||||
|
||||
guard let encryptionKeys = try? walletStorage.exportUserMetadataEncryptionKeys(),
|
||||
guard let encryptionKeys = try? walletStorage.exportUserMetadataEncryptionKeys(account),
|
||||
let umKey = encryptionKeys.getCached(account: account) else {
|
||||
throw UMError.missingEncryptionKey
|
||||
}
|
||||
|
||||
guard let filename = umKey.fileIdentifier() else {
|
||||
guard let filename = umKey.fileIdentifier(account: account) else {
|
||||
throw UMError.fileIdentifier
|
||||
}
|
||||
|
||||
|
@ -97,7 +97,7 @@ public class UserMetadataStorage {
|
|||
|
||||
let encryptedUMData = try UserMetadata.encryptUserMetadata(metadata, account: account)
|
||||
|
||||
try encryptedUMData.write(to: fileURL, options: .atomic)
|
||||
try encryptedUMData.write(to: fileURL)
|
||||
|
||||
@Dependency(\.remoteStorage) var remoteStorage
|
||||
|
||||
|
@ -132,10 +132,10 @@ public class UserMetadataStorage {
|
|||
// Try to find and get the data from the encrypted file with the latest encryption version
|
||||
let encryptedFileURL = documentsDirectory.appendingPathComponent(try filenameForEncryptedFile(account: account))
|
||||
|
||||
if FileManager.default.fileExists(atPath: encryptedFileURL.path) {
|
||||
if !FileManager.default.fileExists(atPath: encryptedFileURL.path) {
|
||||
throw UMError.localFileDoesntExist
|
||||
}
|
||||
|
||||
|
||||
if let encryptedUMData = try? Data(contentsOf: encryptedFileURL) {
|
||||
return try UserMetadata.userMetadataFrom(encryptedData: encryptedUMData, account: account)
|
||||
}
|
||||
|
@ -201,7 +201,7 @@ public class UserMetadataStorage {
|
|||
// MARK: - Bookmarking
|
||||
|
||||
public func isBookmarked(txId: String) -> Bool {
|
||||
bookmarked[txId] != nil
|
||||
bookmarked[txId]?.isBookmarked ?? false
|
||||
}
|
||||
|
||||
public func toggleBookmarkFor(txId: String) {
|
||||
|
|
|
@ -19,9 +19,13 @@ public struct WalletStorage {
|
|||
public enum Constants {
|
||||
public static let zcashStoredWallet = "zcashStoredWallet"
|
||||
public static let zcashStoredAdressBookEncryptionKeys = "zcashStoredAdressBookEncryptionKeys"
|
||||
public static let zcashStoredUserMetadataEncryptionKeys = "zcashStoredUserMetadataEncryptionKeys"
|
||||
public static let zcashStoredUserMetadataEncryptionKeys = "zcashStoredMetadataEncryptionKeys"
|
||||
/// Versioning of the stored data
|
||||
public static let zcashKeychainVersion = 1
|
||||
|
||||
public static func accountMetadataFilename(account: Account) -> String {
|
||||
Constants.zcashStoredUserMetadataEncryptionKeys + "_\(account.name?.lowercased() ?? "")"
|
||||
}
|
||||
}
|
||||
|
||||
public enum KeychainError: Error, Equatable {
|
||||
|
@ -150,6 +154,9 @@ public struct WalletStorage {
|
|||
|
||||
public func resetZashi() throws {
|
||||
try deleteData(forKey: Constants.zcashStoredWallet)
|
||||
try? deleteData(forKey: Constants.zcashStoredAdressBookEncryptionKeys)
|
||||
try? deleteData(forKey: "\(Constants.zcashStoredUserMetadataEncryptionKeys)_zashi")
|
||||
try? deleteData(forKey: "\(Constants.zcashStoredUserMetadataEncryptionKeys)_keystone")
|
||||
}
|
||||
|
||||
public func importAddressBookEncryptionKeys(_ keys: AddressBookEncryptionKeys) throws {
|
||||
|
@ -188,13 +195,13 @@ public struct WalletStorage {
|
|||
return wallet
|
||||
}
|
||||
|
||||
public func importUserMetadataEncryptionKeys(_ keys: UserMetadataEncryptionKeys) throws {
|
||||
public func importUserMetadataEncryptionKeys(_ keys: UserMetadataEncryptionKeys, account: Account) throws {
|
||||
do {
|
||||
guard let data = try encode(object: keys) else {
|
||||
throw KeychainError.encoding
|
||||
}
|
||||
|
||||
try setData(data, forKey: Constants.zcashStoredUserMetadataEncryptionKeys)
|
||||
try setData(data, forKey: Constants.accountMetadataFilename(account: account))
|
||||
} catch KeychainError.duplicate {
|
||||
throw WalletStorageError.alreadyImported
|
||||
} catch {
|
||||
|
@ -202,11 +209,11 @@ public struct WalletStorage {
|
|||
}
|
||||
}
|
||||
|
||||
public func exportUserMetadataEncryptionKeys() throws -> UserMetadataEncryptionKeys {
|
||||
public func exportUserMetadataEncryptionKeys(account: Account) throws -> UserMetadataEncryptionKeys {
|
||||
let reqData: Data?
|
||||
|
||||
do {
|
||||
reqData = try data(forKey: Constants.zcashStoredUserMetadataEncryptionKeys)
|
||||
reqData = try data(forKey: Constants.accountMetadataFilename(account: account))
|
||||
} catch KeychainError.noDataFound {
|
||||
throw WalletStorageError.uninitializedUserMetadataEncryptionKeys
|
||||
} catch {
|
||||
|
|
|
@ -78,6 +78,6 @@ public struct WalletStorageClient {
|
|||
public var importAddressBookEncryptionKeys: (AddressBookEncryptionKeys) throws -> Void
|
||||
public var exportAddressBookEncryptionKeys: () throws -> AddressBookEncryptionKeys
|
||||
|
||||
public var importUserMetadataEncryptionKeys: (UserMetadataEncryptionKeys) throws -> Void
|
||||
public var exportUserMetadataEncryptionKeys: () throws -> UserMetadataEncryptionKeys
|
||||
public var importUserMetadataEncryptionKeys: (UserMetadataEncryptionKeys, Account) throws -> Void
|
||||
public var exportUserMetadataEncryptionKeys: (Account) throws -> UserMetadataEncryptionKeys
|
||||
}
|
||||
|
|
|
@ -45,11 +45,11 @@ extension WalletStorageClient: DependencyKey {
|
|||
exportAddressBookEncryptionKeys: {
|
||||
try walletStorage.exportAddressBookEncryptionKeys()
|
||||
},
|
||||
importUserMetadataEncryptionKeys: { keys in
|
||||
try walletStorage.importUserMetadataEncryptionKeys(keys)
|
||||
importUserMetadataEncryptionKeys: { keys, account in
|
||||
try walletStorage.importUserMetadataEncryptionKeys(keys, account: account)
|
||||
},
|
||||
exportUserMetadataEncryptionKeys: {
|
||||
try walletStorage.exportUserMetadataEncryptionKeys()
|
||||
exportUserMetadataEncryptionKeys: { account in
|
||||
try walletStorage.exportUserMetadataEncryptionKeys(account: account)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ extension WalletStorageClient {
|
|||
resetZashi: { },
|
||||
importAddressBookEncryptionKeys: { _ in },
|
||||
exportAddressBookEncryptionKeys: { .empty },
|
||||
importUserMetadataEncryptionKeys: { _ in },
|
||||
exportUserMetadataEncryptionKeys: { .empty }
|
||||
importUserMetadataEncryptionKeys: { _, _ in },
|
||||
exportUserMetadataEncryptionKeys: { _ in .empty }
|
||||
)
|
||||
}
|
||||
|
|
|
@ -167,7 +167,7 @@ extension Root {
|
|||
|
||||
case .tabs, .initialization, .onboarding, .updateStateAfterConfigUpdate, .alert, .phraseDisplay, .synchronizerStateChanged,
|
||||
.welcome, .binding, .resetZashiSDKFailed, .resetZashiSDKSucceeded, .resetZashiKeychainFailed, .resetZashiKeychainRequest, .resetZashiFinishProcessing, .resetZashiKeychainFailedWithCorruptedData, .debug, .walletConfigLoaded, .exportLogs, .confirmationDialog,
|
||||
.notEnoughFreeSpace, .serverSetup, .serverSetupBindingUpdated, .batteryStateChanged, .cancelAllRunningEffects, .addressBookBinding, .addressBook, .addressBookContactBinding, .addressBookAccessGranted, .osStatusError, .observeTransactions, .foundTransactions, .minedTransaction, .fetchTransactionsForTheSelectedAccount, .fetchedTransactions, .noChangeInTransactions, .loadContacts, .contactsLoaded, .loadUserMetadata:
|
||||
.notEnoughFreeSpace, .serverSetup, .serverSetupBindingUpdated, .batteryStateChanged, .cancelAllRunningEffects, .addressBookBinding, .addressBook, .addressBookContactBinding, .addressBookAccessGranted, .osStatusError, .observeTransactions, .foundTransactions, .minedTransaction, .fetchTransactionsForTheSelectedAccount, .fetchedTransactions, .noChangeInTransactions, .loadContacts, .contactsLoaded, .loadUserMetadata, .resolveMetadataEncryptionKeys:
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
|
|
@ -319,51 +319,34 @@ extension Root {
|
|||
|
||||
let walletAccounts = try await sdkSynchronizer.walletAccounts()
|
||||
await send(.initialization(.loadedWalletAccounts(walletAccounts)))
|
||||
await send(.resolveMetadataEncryptionKeys)
|
||||
|
||||
try await sdkSynchronizer.start(false)
|
||||
|
||||
var selectedAccount: WalletAccount?
|
||||
|
||||
|
||||
for account in walletAccounts {
|
||||
if account.vendor == .zcash {
|
||||
selectedAccount = account
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if let account = selectedAccount {
|
||||
let addressBookEncryptionKeys = try? walletStorage.exportAddressBookEncryptionKeys()
|
||||
if addressBookEncryptionKeys == nil {
|
||||
var keys = AddressBookEncryptionKeys.empty
|
||||
try keys.cacheFor(
|
||||
seed: seedBytes,
|
||||
account: account.account,
|
||||
network: zcashSDKEnvironment.network.networkType
|
||||
)
|
||||
|
||||
do {
|
||||
var keys = AddressBookEncryptionKeys.empty
|
||||
try keys.cacheFor(
|
||||
seed: seedBytes,
|
||||
account: account.account,
|
||||
network: zcashSDKEnvironment.network.networkType
|
||||
)
|
||||
try walletStorage.importAddressBookEncryptionKeys(keys)
|
||||
} catch {
|
||||
// TODO: [#1408] error handling https://github.com/Electric-Coin-Company/zashi-ios/issues/1408
|
||||
}
|
||||
}
|
||||
|
||||
let userMetadataEncryptionKeys = try? walletStorage.exportUserMetadataEncryptionKeys()
|
||||
if userMetadataEncryptionKeys == nil {
|
||||
var keys = UserMetadataEncryptionKeys.empty
|
||||
try keys.cacheFor(
|
||||
seed: seedBytes,
|
||||
account: account.account,
|
||||
network: zcashSDKEnvironment.network.networkType
|
||||
)
|
||||
|
||||
do {
|
||||
try walletStorage.importUserMetadataEncryptionKeys(keys)
|
||||
} catch {
|
||||
// TODO: [#1408] error handling https://github.com/Electric-Coin-Company/zashi-ios/issues/1408
|
||||
}
|
||||
}
|
||||
|
||||
let uAddress = try? await sdkSynchronizer.getUnifiedAddress(account.id)
|
||||
await send(.initialization(.initializationSuccessfullyDone(uAddress)))
|
||||
} else {
|
||||
|
@ -410,7 +393,45 @@ extension Root {
|
|||
)
|
||||
|
||||
case .tabs(.addKeystoneHWWallet(.loadedWalletAccounts)), .tabs(.settings(.integrations(.addKeystoneHWWallet(.loadedWalletAccounts)))):
|
||||
return .send(.loadUserMetadata)
|
||||
return .merge(
|
||||
.send(.resolveMetadataEncryptionKeys),
|
||||
.send(.loadUserMetadata)
|
||||
)
|
||||
|
||||
case .resolveMetadataEncryptionKeys:
|
||||
do {
|
||||
let storedWallet: StoredWallet
|
||||
do {
|
||||
storedWallet = try walletStorage.exportWallet()
|
||||
} catch {
|
||||
return .send(.destination(.updateDestination(.osStatusError)))
|
||||
}
|
||||
try mnemonic.isValid(storedWallet.seedPhrase.value())
|
||||
let seedBytes = try mnemonic.toSeed(storedWallet.seedPhrase.value())
|
||||
|
||||
return .run { [walletAccounts = state.walletAccounts] send in
|
||||
do {
|
||||
|
||||
for account in walletAccounts {
|
||||
let userMetadataEncryptionKeys = try? walletStorage.exportUserMetadataEncryptionKeys(account.account)
|
||||
if userMetadataEncryptionKeys == nil {
|
||||
do {
|
||||
var keys = UserMetadataEncryptionKeys.empty
|
||||
try keys.cacheFor(
|
||||
seed: seedBytes,
|
||||
account: account.account,
|
||||
network: zcashSDKEnvironment.network.networkType
|
||||
)
|
||||
try walletStorage.importUserMetadataEncryptionKeys(keys, account.account)
|
||||
} catch {
|
||||
// TODO: [#1408] error handling https://github.com/Electric-Coin-Company/zashi-ios/issues/1408
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
return .none
|
||||
|
||||
case .initialization(.checkBackupPhraseValidation):
|
||||
let storedWallet: StoredWallet
|
||||
|
|
|
@ -185,6 +185,7 @@ public struct Root {
|
|||
|
||||
// UserMetadata
|
||||
case loadUserMetadata
|
||||
case resolveMetadataEncryptionKeys
|
||||
}
|
||||
|
||||
@Dependency(\.addressBook) var addressBook
|
||||
|
|
|
@ -211,7 +211,7 @@ extension TransactionDetailsView {
|
|||
.frame(width: 51, height: 51)
|
||||
|
||||
Asset.Assets.Icons.shieldTickFilled.image
|
||||
.zImage(size: 24, style: Design.Text.tertiary)
|
||||
.zImage(size: 24, style: Design.Text.primary)
|
||||
}
|
||||
}
|
||||
.offset(x: -4)
|
||||
|
@ -243,7 +243,7 @@ extension TransactionDetailsView {
|
|||
if store.isSensitiveContentHidden {
|
||||
Text(L10n.General.hideBalancesMost)
|
||||
} else {
|
||||
Text(store.transaction.zecAmount.decimalString())
|
||||
Text(store.transaction.netValue)
|
||||
+ Text(" \(tokenName)")
|
||||
.foregroundColor(Design.Text.quaternary.color(colorScheme))
|
||||
}
|
||||
|
|
|
@ -98,10 +98,11 @@ extension TransactionsManagerView {
|
|||
FilterView(title: L10n.Filter.notes, active: store.isNotesFilterActive) { store.send(.toggleFilter(.notes)) }
|
||||
FilterView(title: L10n.Filter.bookmarked, active: store.isBookmarkedFilterActive) { store.send(.toggleFilter(.bookmarked)) }
|
||||
}
|
||||
|
||||
HStack(spacing: 8) {
|
||||
FilterView(title: L10n.Filter.contact, active: store.isContactFilterActive) { store.send(.toggleFilter(.contact)) }
|
||||
}
|
||||
|
||||
// Hidden for now but possibly released in the near future
|
||||
// HStack(spacing: 8) {
|
||||
// FilterView(title: L10n.Filter.contact, active: store.isContactFilterActive) { store.send(.toggleFilter(.contact)) }
|
||||
// }
|
||||
}
|
||||
.padding(.bottom, 32)
|
||||
|
||||
|
|
|
@ -40,6 +40,8 @@ public struct TransactionState: Equatable, Identifiable {
|
|||
public var isMarkedAsRead = false
|
||||
public var isInAddressBook = false
|
||||
public var hasTransparentOutputs = false
|
||||
public var totalSpent: Zatoshi?
|
||||
public var totalReceived: Zatoshi?
|
||||
|
||||
public var rawID: Data? = nil
|
||||
|
||||
|
@ -198,6 +200,12 @@ public struct TransactionState: Equatable, Identifiable {
|
|||
public var totalAmount: Zatoshi {
|
||||
Zatoshi(zecAmount.amount + (fee?.amount ?? 0))
|
||||
}
|
||||
|
||||
public var netValue: String {
|
||||
isShieldingTransaction
|
||||
? Zatoshi(totalSpent?.amount ?? 0).decimalString()
|
||||
: zecAmount.decimalString()
|
||||
}
|
||||
|
||||
public init(
|
||||
errorMessage: String? = nil,
|
||||
|
@ -272,6 +280,8 @@ extension TransactionState {
|
|||
isTransparentRecipient = false
|
||||
self.hasTransparentOutputs = hasTransparentOutputs
|
||||
memoCount = transaction.memoCount
|
||||
totalSpent = transaction.totalSpent
|
||||
totalReceived = transaction.totalReceived
|
||||
|
||||
// TODO: [#1313] SDK improvements so a client doesn't need to determing if the transaction isPending
|
||||
// https://github.com/zcash/ZcashLightClientKit/issues/1313
|
||||
|
|
|
@ -18,17 +18,29 @@ public struct UserMetadataEncryptionKeys: Codable, Equatable {
|
|||
public static let version = 1
|
||||
}
|
||||
|
||||
var keys: [Int: UserMetadataKey]
|
||||
var keys: [Int: UserMetadataKeys]
|
||||
|
||||
public mutating func cacheFor(seed: [UInt8], account: Account, network: NetworkType) throws{
|
||||
public mutating func cacheFor(seed: [UInt8], account: Account, network: NetworkType) throws {
|
||||
guard let zip32AccountIndex = account.hdAccountIndex else {
|
||||
return
|
||||
}
|
||||
|
||||
keys[Int(zip32AccountIndex.index)] = try UserMetadataKey(seed: seed, account: account, network: network)
|
||||
guard let info = "metadata".data(using: .utf8) else {
|
||||
fatalError("Unable to prepare `info` info")
|
||||
}
|
||||
|
||||
let metadataKey = try AccountMetadataKey(
|
||||
from: seed,
|
||||
accountIndex: zip32AccountIndex,
|
||||
networkType: network
|
||||
)
|
||||
|
||||
let privateMetadataKeys = try metadataKey.derivePrivateUseMetadataKey(ufvk: account.ufvk, privateUseSubject: [UInt8](info))
|
||||
|
||||
keys[Int(zip32AccountIndex.index)] = UserMetadataKeys(privateKeys: privateMetadataKeys)
|
||||
}
|
||||
|
||||
public func getCached(account: Account) -> UserMetadataKey? {
|
||||
public func getCached(account: Account) -> UserMetadataKeys? {
|
||||
guard let zip32AccountIndex = account.hdAccountIndex else {
|
||||
return nil
|
||||
}
|
||||
|
@ -43,19 +55,25 @@ extension UserMetadataEncryptionKeys {
|
|||
)
|
||||
}
|
||||
|
||||
public struct UserMetadataKey: Codable, Equatable, Redactable {
|
||||
let key: SymmetricKey
|
||||
public struct UserMetadataKeys: Codable, Equatable, Redactable {
|
||||
let keys: [SymmetricKey]
|
||||
|
||||
public init(from decoder: any Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
key = SymmetricKey(data: try container.decode(Data.self))
|
||||
keys = try container.decode([Data].self).map { SymmetricKey(data: $0) }
|
||||
}
|
||||
|
||||
public func encode(to encoder: any Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
try key.withUnsafeBytes { key in
|
||||
let key = Data(key)
|
||||
try container.encode(key)
|
||||
do {
|
||||
let privateKeys = keys.map { symmetricKey in
|
||||
symmetricKey.withUnsafeBytes { key in
|
||||
return Data(key)
|
||||
}
|
||||
}
|
||||
try container.encode(privateKeys)
|
||||
} catch {
|
||||
fatalError("Unable to encode `UserMetadataKeys`")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -67,21 +85,8 @@ public struct UserMetadataKey: Codable, Equatable, Redactable {
|
|||
* control requirements for the seed phrase and the user metadata, this key
|
||||
* should be cached in the app's keystore.
|
||||
*/
|
||||
public init(seed: [UInt8], account: Account, network: NetworkType) throws {
|
||||
let zip32AccountIndex: Zip32AccountIndex
|
||||
|
||||
if let zip32AccountIndexUnwrapped = account.hdAccountIndex {
|
||||
zip32AccountIndex = zip32AccountIndexUnwrapped
|
||||
} else {
|
||||
zip32AccountIndex = Zip32AccountIndex(0)
|
||||
}
|
||||
|
||||
self.key = try SymmetricKey(data: DerivationToolClient.live().deriveArbitraryAccountKey(
|
||||
[UInt8]("ZashiAddressBookEncryptionV1".utf8),
|
||||
seed,
|
||||
zip32AccountIndex,
|
||||
network
|
||||
))
|
||||
public init(privateKeys: [Data]) {
|
||||
keys = privateKeys.map { SymmetricKey(data: $0) }
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -99,26 +104,60 @@ public struct UserMetadataKey: Codable, Equatable, Redactable {
|
|||
fatalError("Unable to prepare `encryption_key` info")
|
||||
}
|
||||
|
||||
return HKDF<SHA256>.deriveKey(inputKeyMaterial: key, info: salt + info, outputByteCount: 32)
|
||||
guard let firstKey = keys.first else {
|
||||
fatalError("Unable to process `firstKey`")
|
||||
}
|
||||
|
||||
return HKDF<SHA256>.deriveKey(inputKeyMaterial: firstKey, info: salt + info, outputByteCount: 32)
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives a one-time user metadata decryption keys.
|
||||
*
|
||||
* At decryption time, the one-time property MUST be ensured by generating a
|
||||
* random 32-byte salt.
|
||||
*/
|
||||
public func deriveDecryptionKeys(
|
||||
salt: Data
|
||||
) -> [SymmetricKey] {
|
||||
assert(salt.count == 32)
|
||||
|
||||
guard let info = "encryption_key".data(using: .utf8) else {
|
||||
fatalError("Unable to prepare `encryption_key` info")
|
||||
}
|
||||
|
||||
var decryptionKeys: [SymmetricKey] = []
|
||||
|
||||
keys.forEach {
|
||||
decryptionKeys.append(HKDF<SHA256>.deriveKey(inputKeyMaterial: $0, info: salt + info, outputByteCount: 32))
|
||||
}
|
||||
|
||||
return decryptionKeys
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives the filename that this key is able to decrypt.
|
||||
*/
|
||||
public func fileIdentifier() -> String? {
|
||||
public func fileIdentifier(account: Account) -> String? {
|
||||
guard let info = "file_identifier".data(using: .utf8) else {
|
||||
fatalError("Unable to prepare `file_identifier` info")
|
||||
}
|
||||
|
||||
guard let firstKey = keys.first else {
|
||||
fatalError("Unable to process `firstKey`")
|
||||
}
|
||||
|
||||
// Perform HKDF with SHA-256
|
||||
let hkdfKey = HKDF<SHA256>.deriveKey(inputKeyMaterial: key, info: info, outputByteCount: 32)
|
||||
let hkdfKey = HKDF<SHA256>.deriveKey(inputKeyMaterial: firstKey, info: info, outputByteCount: 32)
|
||||
|
||||
// Convert the HKDF output to a hex string
|
||||
let fileIdentifier = hkdfKey.withUnsafeBytes { rawBytes in
|
||||
rawBytes.map { String(format: "%02x", $0) }.joined()
|
||||
}
|
||||
|
||||
let prefix = "\(account.name?.lowercased() ?? "")"
|
||||
|
||||
// Prepend the prefix to the result
|
||||
return "zashi-user-metadata-\(fileIdentifier)"
|
||||
return "\(prefix)-metadata-\(fileIdentifier)"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -118,7 +118,7 @@ public struct TransactionRowView: View {
|
|||
Text(L10n.General.hideBalancesMost)
|
||||
} else {
|
||||
Text(transaction.isSpending ? "- " : "")
|
||||
+ Text(transaction.zecAmount.decimalString())
|
||||
+ Text(transaction.netValue)
|
||||
+ Text(" \(tokenName)")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2514,7 +2514,7 @@
|
|||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = "zashi-internal.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = RLPRR8CPQG;
|
||||
|
@ -2527,7 +2527,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.4.11;
|
||||
MARKETING_VERSION = 0.5.0;
|
||||
OTHER_SWIFT_FLAGS = "";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "co.electriccoin.secant-testnet";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
@ -2545,7 +2545,7 @@
|
|||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = "zashi-internal.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = RLPRR8CPQG;
|
||||
ENABLE_BITCODE = NO;
|
||||
|
@ -2557,7 +2557,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.4.11;
|
||||
MARKETING_VERSION = 0.5.0;
|
||||
OTHER_SWIFT_FLAGS = "";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "co.electriccoin.secant-testnet";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
@ -2575,7 +2575,7 @@
|
|||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = "zashi-internal.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = RLPRR8CPQG;
|
||||
ENABLE_BITCODE = NO;
|
||||
|
@ -2587,7 +2587,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.4.11;
|
||||
MARKETING_VERSION = 0.5.0;
|
||||
OTHER_SWIFT_FLAGS = "";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "co.electriccoin.secant-testnet";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
|
|
@ -554,7 +554,7 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Electric-Coin-Company/zcash-light-client-ffi",
|
||||
"state" : {
|
||||
"revision" : "668571f280e0a9c8a664ca2d948b23d71c8de896"
|
||||
"revision" : "5182c7a77759c07708d3e5d1120586bca3de832a"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -572,7 +572,7 @@
|
|||
"location" : "https://github.com/LukasKorba/ZcashLightClientKit",
|
||||
"state" : {
|
||||
"branch" : "preview-ffi-0.13.0",
|
||||
"revision" : "01f24d8e00260700c9a3841ac77dc867bd6f6bf0"
|
||||
"revision" : "630e24ec8714d499916989760f1c4c91db68d190"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
<key>NSUbiquitousContainerIsDocumentStorage</key>
|
||||
<true/>
|
||||
<key>NSUbiquitousContainerName</key>
|
||||
<string>Zashi Address Book</string>
|
||||
<string>Zashi Internal</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UIAppFonts</key>
|
||||
|
|
Loading…
Reference in New Issue