Skip to content

Commit

Permalink
WIP: Implement AddressBook encryption
Browse files Browse the repository at this point in the history
  • Loading branch information
str4d committed Nov 2, 2024
1 parent 7f4cbf6 commit 8db66c4
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import RemoteStorage
import Combine

import WalletStorage
import CryptoKit

extension AddressBookClient: DependencyKey {
private enum Constants {
Expand Down Expand Up @@ -157,14 +158,13 @@ extension AddressBookClient: DependencyKey {
}
)
}

private static func encryptContacts(_ abContacts: AddressBookContacts) throws -> Data {
@Dependency(\.walletStorage) var walletStorage

// TODO: str4d
// guard let encryptionKeys = try? walletStorage.exportAddressBookEncryptionKeys() else {
// throw AddressBookClient.AddressBookClientError.missingEncryptionKey
// }
guard let encryptionKeys = try? walletStorage.exportAddressBookEncryptionKeys(), let addressBookKey = encryptionKeys.getCached(account: 0) else {
throw AddressBookClient.AddressBookClientError.missingEncryptionKey
}

// here you have an array of all contacts
// you also have a key from the keychain
Expand All @@ -185,20 +185,36 @@ extension AddressBookClient: DependencyKey {
data.append(serializedContact)
}

return data
// Generate a fresh one-time sub-key for encrypting the address book.
let salt = SymmetricKey(size: SymmetricKeySize.bits256)
return try salt.withUnsafeBytes { salt in
let salt = Data(salt)
let subKey = addressBookKey.deriveEncryptionKey(salt: salt)

// Encrypt the serialized address book.
// CryptoKit encodes the SealedBox as `nonce || ciphertext || tag`.
let sealed = try ChaChaPoly.seal(data, using: subKey)

// Prepand the salt to the SealedBox so we can re-derive the sub-key.
return salt + sealed.combined
}
}

private static func decryptData(_ data: Data) throws -> AddressBookContacts {
private static func decryptData(_ encrypted: Data) throws -> AddressBookContacts {
@Dependency(\.walletStorage) var walletStorage

// TODO: str4d
// guard let encryptionKeys = try? walletStorage.exportAddressBookEncryptionKeys() else {
// throw AddressBookClient.AddressBookClientError.missingEncryptionKey
// }
guard let encryptionKeys = try? walletStorage.exportAddressBookEncryptionKeys(), let addressBookKey = encryptionKeys.getCached(account: 0) else {
throw AddressBookClient.AddressBookClientError.missingEncryptionKey
}

// Derive the sub-key for decrypting the address book.
let salt = encrypted.prefix(upTo: 32)
let subKey = addressBookKey.deriveEncryptionKey(salt: salt)

// Unseal the encrypted address book.
let sealed = try ChaChaPoly.SealedBox.init(combined: encrypted.suffix(from: 32))
let data = try ChaChaPoly.open(sealed, using: subKey)

// here you have the encrypted data from the cloud, the blob
// you also have a key from the keychain

var offset = 0

// Deserialize `version`
Expand Down
14 changes: 6 additions & 8 deletions modules/Sources/Features/Root/RootInitialization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -296,14 +296,12 @@ extension Root {

let addressBookEncryptionKeys = try? walletStorage.exportAddressBookEncryptionKeys()
if addressBookEncryptionKeys == nil {
// TODO: str4d
// here you know the encryption key for the address book is missing, we need to generate one

// here you have `storedWallet.seedPhrase.seedPhrase`, a seed as String

// once the key is prepared, store it
// let keys == AddressBookEncryptionKeys(key: "")
// try walletStorage.importAddressBookEncryptionKeys(keys)
var keys = AddressBookEncryptionKeys.empty
keys.cacheFor(seedPhrase: storedWallet.seedPhrase, account: 0)
// TODO: Why are the address book keys stored in wallet storage?
// Doesn't that mean they require the same authentication as the
// seed phrase, instead of being accessible without biometric auth?
try walletStorage.importAddressBookEncryptionKeys(keys)
}

return .run { send in
Expand Down
29 changes: 29 additions & 0 deletions modules/Sources/Generated/XCAssets+Generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ public enum Asset {
public static let shieldTick = ImageAsset(name: "shieldTick")
public static let shieldedFunds = ImageAsset(name: "shieldedFunds")
public static let surroundedShield = ImageAsset(name: "surroundedShield")
public static let test = DataAsset(name: "test")
public static let tooltip = ImageAsset(name: "tooltip")
public static let zashiTitle = ImageAsset(name: "zashiTitle")
}
Expand Down Expand Up @@ -305,6 +306,34 @@ public extension ColorAsset.SystemColor {
}
}

public struct DataAsset {
public fileprivate(set) var name: String

#if os(iOS) || os(tvOS) || os(macOS)
@available(iOS 9.0, macOS 10.11, *)
public var data: NSDataAsset {
guard let data = NSDataAsset(asset: self) else {
fatalError("Unable to load data asset named \(name).")
}
return data
}
#endif
}

#if os(iOS) || os(tvOS) || os(macOS)
@available(iOS 9.0, macOS 10.11, *)
public extension NSDataAsset {
convenience init?(asset: DataAsset) {
let bundle = BundleToken.bundle
#if os(iOS) || os(tvOS)
self.init(name: asset.name, bundle: bundle)
#elseif os(macOS)
self.init(name: NSDataAsset.Name(asset.name), bundle: bundle)
#endif
}
}
#endif

public struct ImageAsset {
public fileprivate(set) var name: String

Expand Down
57 changes: 53 additions & 4 deletions modules/Sources/Models/AddressBookEncryptionKeys.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,67 @@
//

import Foundation
import CryptoKit
import Utils

/// Representation of the address book encryption keys
public struct AddressBookEncryptionKeys: Codable, Equatable {
public let key: String
var keys: [Int: AddressBookKey]

public init(key: String) {
self.key = key
public mutating func cacheFor(seedPhrase: SeedPhrase, account: Int) {
keys[account] = AddressBookKey(seedPhrase: seedPhrase, account: account)
}

public func getCached(account: Int) -> AddressBookKey? {
keys[account]
}
}

extension AddressBookEncryptionKeys {
public static let empty = Self(
key: ""
keys: [:]
)
}

public struct AddressBookKey: Codable, Equatable, Redactable {
let key: SymmetricKey

public init(from decoder: any Decoder) throws {
let container = try decoder.singleValueContainer()
key = SymmetricKey(data: try container.decode(Data.self))
}

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)
}
}

/**
* Derives the long-term key that can decrypt the given account's encrypted
* address book.
*
* This requires access to the seed phrase. If the app has separate access
* control requirements for the seed phrase and the address book, this key
* should be cached in the app's keystore.
*/
public init(seedPhrase: SeedPhrase, account: Int) {
// TODO: Use SDK to derive the key.
self.key = SymmetricKey(data: Data())
}

/**
* Derives a one-time address book encryption key.
*
* At encryption time, the one-time property MUST be ensured by generating a
* random 32-byte salt.
*/
public func deriveEncryptionKey(
salt: Data
) -> SymmetricKey {
assert(salt.count == 32)
return HKDF<SHA256>.deriveKey(inputKeyMaterial: key, salt: salt, outputByteCount: 32)
}
}

0 comments on commit 8db66c4

Please sign in to comment.