diff --git a/Multisig.xcodeproj/project.pbxproj b/Multisig.xcodeproj/project.pbxproj index 837234b95..7a2bcd8bd 100644 --- a/Multisig.xcodeproj/project.pbxproj +++ b/Multisig.xcodeproj/project.pbxproj @@ -362,6 +362,11 @@ 558836702565385F0014E8C7 /* RemoveSafeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5588366E2565385F0014E8C7 /* RemoveSafeCell.swift */; }; 558836712565385F0014E8C7 /* RemoveSafeCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5588366F2565385F0014E8C7 /* RemoveSafeCell.xib */; }; 558F3CB224AB2C9500BBCDF1 /* RefreshableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 558F3CB124AB2C9500BBCDF1 /* RefreshableScrollView.swift */; }; + 559670FF256E88FC0097D3A2 /* Signer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559670FE256E88FC0097D3A2 /* Signer.swift */; }; + 55967105256E92470097D3A2 /* SignerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55967104256E92470097D3A2 /* SignerTests.swift */; }; + 5596710D256E935F0097D3A2 /* MockSecureStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5596710C256E935F0097D3A2 /* MockSecureStore.swift */; }; + 55967119256EB2120097D3A2 /* RegisterNotificationTokenRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55967118256EB2120097D3A2 /* RegisterNotificationTokenRequestTests.swift */; }; + 55967123256EB7690097D3A2 /* ConfirmationRequestNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55967122256EB7690097D3A2 /* ConfirmationRequestNotification.swift */; }; 5596C03825507EAE00EF23A5 /* CollectibleTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5596C03725507EAE00EF23A5 /* CollectibleTableViewCell.xib */; }; 5596C03E25507F3C00EF23A5 /* CollectibleTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5596C03D25507F3C00EF23A5 /* CollectibleTableViewCell.swift */; }; 5596C0482550813B00EF23A5 /* CollectiblesHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5596C0472550813B00EF23A5 /* CollectiblesHeaderView.swift */; }; @@ -867,6 +872,11 @@ 5588366E2565385F0014E8C7 /* RemoveSafeCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveSafeCell.swift; sourceTree = ""; }; 5588366F2565385F0014E8C7 /* RemoveSafeCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RemoveSafeCell.xib; sourceTree = ""; }; 558F3CB124AB2C9500BBCDF1 /* RefreshableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshableScrollView.swift; sourceTree = ""; }; + 559670FE256E88FC0097D3A2 /* Signer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Signer.swift; sourceTree = ""; }; + 55967104256E92470097D3A2 /* SignerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignerTests.swift; sourceTree = ""; }; + 5596710C256E935F0097D3A2 /* MockSecureStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSecureStore.swift; sourceTree = ""; }; + 55967118256EB2120097D3A2 /* RegisterNotificationTokenRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterNotificationTokenRequestTests.swift; sourceTree = ""; }; + 55967122256EB7690097D3A2 /* ConfirmationRequestNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationRequestNotification.swift; sourceTree = ""; }; 5596C03725507EAE00EF23A5 /* CollectibleTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CollectibleTableViewCell.xib; sourceTree = ""; }; 5596C03D25507F3C00EF23A5 /* CollectibleTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectibleTableViewCell.swift; sourceTree = ""; }; 5596C0472550813B00EF23A5 /* CollectiblesHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectiblesHeaderView.swift; sourceTree = ""; }; @@ -1208,6 +1218,7 @@ 0A0EFCA1246EADD100D3D8BF /* Data */ = { isa = PBXGroup; children = ( + 55967116256EB1E50097D3A2 /* Services */, 5556A03924489F5D003EC861 /* Persistence */, ); path = Data; @@ -1808,6 +1819,7 @@ 0AF2737024DC57FA007E4012 /* IncomingEtherNotification.swift */, 0AF2737224DC580D007E4012 /* IncomingTokenNotification.swift */, 0AF2736E24DC57E8007E4012 /* MultisigNotification.swift */, + 55967122256EB7690097D3A2 /* ConfirmationRequestNotification.swift */, ); path = Notifications; sourceTree = ""; @@ -1928,6 +1940,7 @@ 55A585352501417E005E778B /* PrivateKey */, 0A0983A724BC5CC8009EE296 /* TokenRegistry */, 0A53071F254311C400E8A270 /* SafeTransactionSigner.swift */, + 559670FE256E88FC0097D3A2 /* Signer.swift */, ); path = Models; sourceTree = ""; @@ -1973,6 +1986,7 @@ 551DDEEA244F192300C719D3 /* Models */ = { isa = PBXGroup; children = ( + 5596710B256E933D0097D3A2 /* Mocks */, 555312F0250673840008206B /* Private Key */, 047300FD24781081004F1200 /* AddressTests.swift */, 551DDEEF244F1A0A00C719D3 /* SafeTests.swift */, @@ -1986,6 +2000,7 @@ 0A19D43C249A5BF100A316B6 /* Transaction */, 0A0983A824BC6222009EE296 /* InMemoryTokenStoreTests.swift */, 0A0983AA24BC638C009EE296 /* HardcodedTokenStoreTests.swift */, + 55967104256E92470097D3A2 /* SignerTests.swift */, ); path = Models; sourceTree = ""; @@ -2094,6 +2109,30 @@ path = ReusableViews; sourceTree = ""; }; + 5596710B256E933D0097D3A2 /* Mocks */ = { + isa = PBXGroup; + children = ( + 5596710C256E935F0097D3A2 /* MockSecureStore.swift */, + ); + path = Mocks; + sourceTree = ""; + }; + 55967116256EB1E50097D3A2 /* Services */ = { + isa = PBXGroup; + children = ( + 55967117256EB1F10097D3A2 /* Safe Transaction Service */, + ); + path = Services; + sourceTree = ""; + }; + 55967117256EB1F10097D3A2 /* Safe Transaction Service */ = { + isa = PBXGroup; + children = ( + 55967118256EB2120097D3A2 /* RegisterNotificationTokenRequestTests.swift */, + ); + path = "Safe Transaction Service"; + sourceTree = ""; + }; 55A585352501417E005E778B /* PrivateKey */ = { isa = PBXGroup; children = ( @@ -2842,6 +2881,7 @@ 0AC94458254068D800921CA5 /* NSNotification+Events.swift in Sources */, 0A0983A624BC5CA1009EE296 /* HardcodedTokenStore.swift in Sources */, 55CEFC2024939525003B2B19 /* TrackingEvent.swift in Sources */, + 559670FF256E88FC0097D3A2 /* Signer.swift in Sources */, 0ADA911225656FEC0004DEC0 /* TransactionListViewController.swift in Sources */, 0ADDA20B24533F1E0066457E /* EnterSafeNameView.swift in Sources */, 0471D13224728C2C00B22A70 /* EmailLink.swift in Sources */, @@ -2970,6 +3010,7 @@ files = ( 0A0983AB24BC638C009EE296 /* HardcodedTokenStoreTests.swift in Sources */, 0A9BC33D24603AE800EB9C5D /* ENSTests.swift in Sources */, + 5596710D256E935F0097D3A2 /* MockSecureStore.swift in Sources */, 0A9BC34C246053DB00EB9C5D /* DataInitWithHexTests.swift in Sources */, 5532D4CD2449A1980067505A /* LogServiceTests.swift in Sources */, 0A6430F5247ECB3E006FD30A /* ConfigurationKeyTests.swift in Sources */, @@ -2983,8 +3024,10 @@ 551DDEEE244F19AE00C719D3 /* CoreDataTestCase.swift in Sources */, 5532D4CB2449A1980067505A /* LoggableErrorTests.swift in Sources */, 55B66E8E24810BC800249E98 /* AppSettingsTests.swift in Sources */, + 55967105256E92470097D3A2 /* SignerTests.swift in Sources */, 5532D4DE2449A34A0067505A /* TestUtils.swift in Sources */, 5556A03C24489F5D003EC861 /* CoreDataStackTests.swift in Sources */, + 55967119256EB2120097D3A2 /* RegisterNotificationTokenRequestTests.swift in Sources */, 551DDEF0244F1A0A00C719D3 /* SafeTests.swift in Sources */, 5532D4CC2449A1980067505A /* LogFormatterTests.swift in Sources */, 0ACC4C4E247534EC00ADF201 /* GnosisSafeTests.swift in Sources */, @@ -3019,6 +3062,7 @@ 0AF2736B24DC57C6007E4012 /* NotificationPayload.swift in Sources */, 0AF2737524DC581A007E4012 /* ExecutedMultisigTransactionNotification.swift in Sources */, 0AF2736F24DC57E8007E4012 /* MultisigNotification.swift in Sources */, + 55967123256EB7690097D3A2 /* ConfirmationRequestNotification.swift in Sources */, 0AF2736D24DC57D5007E4012 /* PlainAddress.swift in Sources */, 0AF2737124DC57FA007E4012 /* IncomingEtherNotification.swift in Sources */, ); diff --git a/Multisig/Cross-layer/App.swift b/Multisig/Cross-layer/App.swift index fb3b6f599..7cd17956a 100644 --- a/Multisig/Cross-layer/App.swift +++ b/Multisig/Cross-layer/App.swift @@ -24,7 +24,7 @@ class App { // Data Layer var coreDataStack: CoreDataProtocol = CoreDataStack() - let keychainService = KeychainService(identifier: App.configuration.app.bundleIdentifier) + var keychainService: SecureStore = KeychainService(identifier: App.configuration.app.bundleIdentifier) // Services let safeTransactionService = SafeTransactionService( diff --git a/Multisig/Data/Services/Notification Service/RegisterNotificationTokenRequest.swift b/Multisig/Data/Services/Notification Service/RegisterNotificationTokenRequest.swift index 99ea6e425..4bb0fd826 100644 --- a/Multisig/Data/Services/Notification Service/RegisterNotificationTokenRequest.swift +++ b/Multisig/Data/Services/Notification Service/RegisterNotificationTokenRequest.swift @@ -7,27 +7,64 @@ // import Foundation +import Web3 struct RegisterNotificationTokenRequest: JSONRequest { - let uuid: String? + let uuid: String let safes: [String] let cloudMessagingToken: String let bundle: String let version: String let deviceType: String = "IOS" let buildNumber: String + let timestamp: String? + let signatures: [String]? + var httpMethod: String { return "POST" } var urlPath: String { return "/api/v1/notifications/devices/" } typealias ResponseType = Response - init(deviceID: UUID? = nil, safes: [Address], token: String, bundle: String, version: String, buildNumber: String) { - self.uuid = deviceID?.uuidString.lowercased() + init(deviceID: String, + safes: [Address], + token: String, + bundle: String, + version: String, + buildNumber: String, + timestamp: String?) throws { + + guard UUID(uuidString: deviceID) != nil else { + preconditionFailure("'deviceID' should be UUID string") + } + self.uuid = deviceID.lowercased() self.safes = safes.map { $0.checksummed } self.cloudMessagingToken = token self.bundle = bundle self.version = version self.buildNumber = buildNumber + self.timestamp = timestamp + + let string = [ + "gnosis-safe", + self.uuid, + self.safes.joined(), + self.cloudMessagingToken, + self.bundle, + self.version, + self.deviceType, + self.buildNumber, + self.timestamp ?? "" + ] + .joined() + + if let signature = try? Signer.sign(string).value { + guard timestamp != nil else { + preconditionFailure("'timestamp' parameter is required if signing key exists") + } + self.signatures = [signature] + } else { + self.signatures = nil + } } struct Response: Decodable { @@ -43,7 +80,22 @@ struct RegisterNotificationTokenRequest: JSONRequest { extension SafeTransactionService { - func register(deviceID: UUID? = nil, safes: [Address], token: String, bundle: String, version: String, buildNumber: String) throws -> RegisterNotificationTokenRequest.Response { - return try execute(request: RegisterNotificationTokenRequest(deviceID: deviceID, safes: safes, token: token, bundle: bundle, version: version, buildNumber: buildNumber)) + @discardableResult + func register(deviceID: String, + safes: [Address], + token: String, + bundle: String, + version: String, + buildNumber: String, + timestamp: String?) throws -> RegisterNotificationTokenRequest.Response { + return try execute( + request: try RegisterNotificationTokenRequest(deviceID: deviceID, + safes: safes, + token: token, + bundle: bundle, + version: version, + buildNumber: buildNumber, + timestamp: timestamp) + ) } } diff --git a/Multisig/Data/Services/Notification Service/UnregisterNotificationTokenRequest.swift b/Multisig/Data/Services/Notification Service/UnregisterNotificationTokenRequest.swift index e4101a966..e2accc3b8 100644 --- a/Multisig/Data/Services/Notification Service/UnregisterNotificationTokenRequest.swift +++ b/Multisig/Data/Services/Notification Service/UnregisterNotificationTokenRequest.swift @@ -17,9 +17,9 @@ struct UnregisterNotificationTokenRequest: JSONRequest { typealias ResponseType = EmptyResponse - init(deviceID: UUID, address: Address) { + init(deviceID: String, address: Address) { self.address = address.checksummed - self.deviceID = deviceID.uuidString.lowercased() + self.deviceID = deviceID.lowercased() } struct EmptyResponse: Decodable { @@ -28,7 +28,7 @@ struct UnregisterNotificationTokenRequest: JSONRequest { } extension SafeTransactionService { - func unregister(deviceID: UUID, address: Address) throws { + func unregister(deviceID: String, address: Address) throws { try execute(request: UnregisterNotificationTokenRequest(deviceID: deviceID, address: address)) } } diff --git a/Multisig/Data/Services/Safe Transaction Service/SignTransactionRequest.swift b/Multisig/Data/Services/Safe Transaction Service/SignTransactionRequest.swift index ab8261982..86666e1a1 100644 --- a/Multisig/Data/Services/Safe Transaction Service/SignTransactionRequest.swift +++ b/Multisig/Data/Services/Safe Transaction Service/SignTransactionRequest.swift @@ -44,7 +44,7 @@ struct SignTransactionRequest: JSONRequest { nonce = transaction.nonce.description contractTransactionHash = transaction.safeTxHash!.description let signature = try SafeTransactionSigner().sign(transaction, by: safeAddress) - self.sender = signature.sender + self.sender = signature.signer self.signature = signature.value } diff --git a/Multisig/Logic/Models/SafeTransactionSigner.swift b/Multisig/Logic/Models/SafeTransactionSigner.swift index a13b7021c..8e6fa5614 100644 --- a/Multisig/Logic/Models/SafeTransactionSigner.swift +++ b/Multisig/Logic/Models/SafeTransactionSigner.swift @@ -11,26 +11,14 @@ import Web3 class SafeTransactionSigner { - struct Signature { - var value: String - var sender: String - } - - func sign(_ transaction: Transaction, by safeAddress: Address) throws -> Signature { + func sign(_ transaction: Transaction, by safeAddress: Address) throws -> Signer.Signature { let hashToSign = Data(ethHex: transaction.safeTxHash!.description) let data = transaction.encodeTransactionData(for: AddressString(safeAddress)) guard EthHasher.hash(data) == hashToSign else { throw "Invalid safeTxHash, please check the transaction data" } - guard let pkData = try App.shared.keychainService.data(forKey: KeychainKey.ownerPrivateKey.rawValue) else { - throw "Private key not found" - } - let privateKey = try EthereumPrivateKey(pkData.bytes) - let eoaSignature = try privateKey.sign(hash: hashToSign.bytes) - let sender = privateKey.address.hex(eip55: true) - let v = String(eoaSignature.v + 27, radix: 16) - let safeSignature = "\(eoaSignature.r.toHexString())\(eoaSignature.s.toHexString())\(v)" - return Signature(value: safeSignature, sender: sender) + + return try Signer.sign(hash: hashToSign) } class func numberOfKeysImported() -> Int { diff --git a/Multisig/Logic/Models/Signer.swift b/Multisig/Logic/Models/Signer.swift new file mode 100644 index 000000000..4bcb4a153 --- /dev/null +++ b/Multisig/Logic/Models/Signer.swift @@ -0,0 +1,46 @@ +// +// Signer.swift +// Multisig +// +// Created by Andrey Scherbovich on 25.11.20. +// Copyright © 2020 Gnosis Ltd. All rights reserved. +// + +import Foundation +import Web3 + +class Signer { + struct Signature: Equatable { + var value: String + var signer: String + } + + /// Signs the hash of the provided string with a stored private key. + /// Currently the app can store only one private key. + /// - Parameters: + /// - string: string to hash and sign + /// - Throws: errors during sisgning process + /// - Returns: Signature object containing hex(r) hex(s) hex(v + 27) as one strig of secp256k1 signature + static func sign(_ string: String) throws -> Signature { + let hash = EthHasher.hash(string.data(using: .utf8)!) + return try sign(hash: hash) + } + + /// Signs the hash with a stored private key by provided address. + /// Currently the app can store only one private key. + /// - Parameters: + /// - hash: hash to sign + /// - Throws: errors during sisgning process + /// - Returns: Signature object containing hex(r) hex(s) hex(v + 27) as one strig of secp256k1 signature + static func sign(hash: Data) throws -> Signature { + guard let pkData = try App.shared.keychainService.data(forKey: KeychainKey.ownerPrivateKey.rawValue) else { + throw "Private key not found" + } + let privateKey = try EthereumPrivateKey(pkData.bytes) + let signer = privateKey.address.hex(eip55: true) + let eoaSignature = try privateKey.sign(hash: hash.bytes) + let v = String(eoaSignature.v + 27, radix: 16) + let signature = "\(eoaSignature.r.toHexString())\(eoaSignature.s.toHexString())\(v)" + return Signature(value: signature, signer: signer) + } +} diff --git a/Multisig/UI/App/RemoteNotificationHandler.swift b/Multisig/UI/App/RemoteNotificationHandler.swift index da9015b7c..c1bb3f167 100644 --- a/Multisig/UI/App/RemoteNotificationHandler.swift +++ b/Multisig/UI/App/RemoteNotificationHandler.swift @@ -12,7 +12,6 @@ import UserNotifications import Firebase class RemoteNotificationHandler { - @UserDefault(key: "io.gnosis.multisig.deviceID") private var storedDeviceID: String? @@ -24,16 +23,6 @@ class RemoteNotificationHandler { private var queue = DispatchQueue(label: "RemoteNotificationHandlerQueue") - // This is temporary, will be removed when we store device id in database - private var deviceID: UUID? { - get { - storedDeviceID.flatMap { UUID(uuidString: $0) } - } - set { - storedDeviceID = newValue?.uuidString - } - } - func setUpMessaging(delegate: MessagingDelegate & UNUserNotificationCenterDelegate) { logDebug("Setting up notification handling") Messaging.messaging().delegate = delegate @@ -49,6 +38,9 @@ class RemoteNotificationHandler { func appStarted() { logDebug("App started") + if storedDeviceID == nil { + storedDeviceID = UUID().uuidString + } monitorAuthorizationStatus() } @@ -79,6 +71,12 @@ class RemoteNotificationHandler { unregister(address: address) } + /// For add / remove signing key + func signingKeyUpdated() { + logDebug("Signing key updated") + registerAll() + } + func received(notification userInfo: [AnyHashable: Any]) { assert(Thread.isMainThread) logDebug("Received notification: \(userInfo)") @@ -166,17 +164,22 @@ class RemoteNotificationHandler { private func register(addresses: [Address]) { guard let token = self.token else { return } - queue.async { + queue.async { [unowned self] in let appConfig = App.configuration.app + var timestamp: String? + if let _ = try? App.shared.keychainService.data(forKey: KeychainKey.ownerPrivateKey.rawValue) { + // add timestamp if there is a signing key + timestamp = String(Int(Date().timeIntervalSince1970 * 1_000)) + } do { - let response = try App.shared.safeTransactionService - .register(deviceID: self.deviceID, + try App.shared.safeTransactionService + .register(deviceID: self.storedDeviceID!, safes: addresses, token: token, bundle: appConfig.bundleIdentifier, version: appConfig.marketingVersion, - buildNumber: appConfig.buildVersion) - self.deviceID = response.uuid + buildNumber: appConfig.buildVersion, + timestamp: timestamp) } catch { logError("Failed to register device", error) } @@ -184,10 +187,9 @@ class RemoteNotificationHandler { } private func unregister(address: Address) { - guard let deviceID = deviceID else { return } - queue.async { + queue.async { [unowned self] in do { - try App.shared.safeTransactionService.unregister(deviceID: deviceID, address: address) + try App.shared.safeTransactionService.unregister(deviceID: self.storedDeviceID!, address: address) } catch { logError("Failed to unregister device", error) } diff --git a/Multisig/UI/App/UIKit/AppSettingsViewController/AppSettingsViewController.swift b/Multisig/UI/App/UIKit/AppSettingsViewController/AppSettingsViewController.swift index c2bcb1388..6a8c72127 100644 --- a/Multisig/UI/App/UIKit/AppSettingsViewController/AppSettingsViewController.swift +++ b/Multisig/UI/App/UIKit/AppSettingsViewController/AppSettingsViewController.swift @@ -165,6 +165,7 @@ class AppSettingsViewController: UITableViewController { try App.shared.keychainService.removeData( forKey: KeychainKey.ownerPrivateKey.rawValue) AppSettings.setSigningKeyAddress(nil) + App.shared.notificationHandler.signingKeyUpdated() App.shared.snackbar.show(message: "Owner key removed from this app") Tracker.shared.setUserProperty("0", for: TrackingUserProperty.numKeysImported) self.reload() diff --git a/Multisig/UI/Settings/App Settings/Owner Wallet Management/SelectOwnerAddressViewModel.swift b/Multisig/UI/Settings/App Settings/Owner Wallet Management/SelectOwnerAddressViewModel.swift index 3952a1d82..b6ccf8dc0 100644 --- a/Multisig/UI/Settings/App Settings/Owner Wallet Management/SelectOwnerAddressViewModel.swift +++ b/Multisig/UI/Settings/App Settings/Owner Wallet Management/SelectOwnerAddressViewModel.swift @@ -57,6 +57,7 @@ class SelectOwnerAddressViewModel: ObservableObject { try App.shared.keychainService.removeData(forKey: KeychainKey.ownerPrivateKey.rawValue) try App.shared.keychainService.save(data: pkData, forKey: KeychainKey.ownerPrivateKey.rawValue) AppSettings.setSigningKeyAddress(addresses[selectedIndex].checksummed) + App.shared.notificationHandler.signingKeyUpdated() App.shared.snackbar.show(message: "Owner key successfully imported") Tracker.shared.setUserProperty("1", for: TrackingUserProperty.numKeysImported) return true diff --git a/MultisigTests/Data/Services/Safe Transaction Service/RegisterNotificationTokenRequestTests.swift b/MultisigTests/Data/Services/Safe Transaction Service/RegisterNotificationTokenRequestTests.swift new file mode 100644 index 000000000..e49cf4344 --- /dev/null +++ b/MultisigTests/Data/Services/Safe Transaction Service/RegisterNotificationTokenRequestTests.swift @@ -0,0 +1,54 @@ +// +// RegisterNotificationTokenRequestTests.swift +// MultisigTests +// +// Created by Andrey Scherbovich on 25.11.20. +// Copyright © 2020 Gnosis Ltd. All rights reserved. +// + +import XCTest +@testable import Multisig + +class RegisterNotificationTokenRequestTests: XCTestCase { + let mockStore = MockSecureStore() + + override func setUpWithError() throws { + App.shared.keychainService = mockStore + try! mockStore.save(data: Data(hex: "0xe7979e5f2ceb1d4ef76019d1fdba88b50ceefe0575bbfdf94969837c50a5d895"), + forKey: KeychainKey.ownerPrivateKey.rawValue) + } + + + func testRequestInitCalculatesProperSignature() throws { + let request = try RegisterNotificationTokenRequest( + deviceID: "bb30cd3e-e0ad-4e9a-b726-44db67a0820b", + safes: [Address("0xEefFcdEAB4AC6005E90566B08EAda3994A573C1E")], + token: "erXBYb-CxU1jtSvwfZrxqW:APA91bH0IWkMWOGizlbNAwxV6OVjEmNR1feRs2WBT7BE6aVMm2C-x1COKqNYq19t5YNjIzVBKDyVVEqFojlkvEtiSaJA0lCZL0LfuEwfc8p9jfBuM6HG82pczVbnMev1J0gXlB3bIlAP", + bundle: "io.gnosis.multisig.dev.rinkeby", + version: "2.6.0", + buildNumber: "1", + timestamp: "1606319110027") + XCTAssertEqual(request.signatures, ["460ab62322407376576be061a6bfaaaa78cd1be4e0421d88cd635d0568ff2d473280d0edfd898bf1fd73f3fea8206ef91d6fbf6d9dc63b5d7d1378b8e4059f691c"]) + + // test vector from requirements doc + try! mockStore.save(data: privateKey(for: "display bless asset brother fish sauce lyrics grit friend online tumble useless"), + forKey: KeychainKey.ownerPrivateKey.rawValue) + let request1 = try RegisterNotificationTokenRequest( + deviceID: "33971c4e-fb98-4e18-a08d-13c881ae292a", + safes: [Address("0x4dEBDD6CEe25b2F931D2FE265D70e1a533B02453"), Address("0x72ac1760daF52986421b1552BdCa04707E78950e")], + token: "dSh5Se1XgEiTiY-4cv1ixY:APA91bG3vYjy9VgB3X3u5EsBphJABchb8Xgg2cOSSekPsxDsfE5xyBeu6gKY0wNhbJHgQUQQGocrHx0Shbx6JMFx2VOyhJx079AduN01NWD1-WjQerY5s3l-cLnHoNNn8fJfARqSUb3G", + bundle: "io.gnosis.multisig.prod.mainnet", + version: "2.7.0", + buildNumber: "199", + timestamp: "1605186645155") + XCTAssertEqual(request1.signatures, ["671edd513d60363612071af9fb08f2414ab6984c3a669b0f29a6f9c885620b626814d1383731dc0fa985e86bd52e1cb6c3adcd75ff806856ece24f65d56d628d1c"]) + } + + private func privateKey(for mnemonic: String) -> Data { + let mnemonic = "display bless asset brother fish sauce lyrics grit friend online tumble useless" + let seedData = BIP39.seedFromMmemonics(mnemonic)! + let rootNode = HDNode(seed: seedData)!.derive(path: HDNode.defaultPathMetamaskPrefix, + derivePrivateKey: true)! + return rootNode.derive(index: 0, derivePrivateKey: true)!.privateKey! + } +} diff --git a/MultisigTests/Logic/Models/Mocks/MockSecureStore.swift b/MultisigTests/Logic/Models/Mocks/MockSecureStore.swift new file mode 100644 index 000000000..ba67a68e7 --- /dev/null +++ b/MultisigTests/Logic/Models/Mocks/MockSecureStore.swift @@ -0,0 +1,30 @@ +// +// MockSecureStore.swift +// MultisigTests +// +// Created by Andrey Scherbovich on 25.11.20. +// Copyright © 2020 Gnosis Ltd. All rights reserved. +// + +import Foundation +@testable import Multisig + +class MockSecureStore: SecureStore { + private var _store = [String: Data]() + + func save(data: Data, forKey: String) throws { + _store[forKey] = data + } + + func data(forKey: String) throws -> Data? { + return _store[forKey] + } + + func removeData(forKey: String) throws { + _store.removeValue(forKey: forKey) + } + + func destroy() throws { + _store = [String: Data]() + } +} diff --git a/MultisigTests/Logic/Models/SignerTests.swift b/MultisigTests/Logic/Models/SignerTests.swift new file mode 100644 index 000000000..38d1d7ecf --- /dev/null +++ b/MultisigTests/Logic/Models/SignerTests.swift @@ -0,0 +1,28 @@ +// +// SignerTests.swift +// MultisigTests +// +// Created by Andrey Scherbovich on 25.11.20. +// Copyright © 2020 Gnosis Ltd. All rights reserved. +// + +import XCTest +@testable import Multisig + +class SignerTests: XCTestCase { + let mockStore = MockSecureStore() + + override func setUpWithError() throws { + super.setUp() + App.shared.keychainService = mockStore + try! mockStore.save(data: Data(hex: "0xe7979e5f2ceb1d4ef76019d1fdba88b50ceefe0575bbfdf94969837c50a5d895"), + forKey: KeychainKey.ownerPrivateKey.rawValue) + } + + func testSigner() throws { + let string = "gnosis-safe" + let expected = Signer.Signature(value: "99a7a03e9597e85a0cc4188d270b72b1df2de943de804f144976f4c1e23116ff274d2dec4ee7201b88bdadf08259a5dc8e7e2bbf372347de3470beeab904e5d01b", + signer: "0x728cafe9fB8CC2218Fb12a9A2D9335193caa07e0") + XCTAssertEqual(try Signer.sign(string), expected) + } +} diff --git a/NotificationServiceExtension/NotificationService.swift b/NotificationServiceExtension/NotificationService.swift index 3e14f5399..aa9e09b13 100644 --- a/NotificationServiceExtension/NotificationService.swift +++ b/NotificationServiceExtension/NotificationService.swift @@ -24,7 +24,8 @@ class NotificationService: UNNotificationServiceExtension { NewConfirmationNotification.self, ExecutedMultisigTransactionNotification.self, IncomingTokenNotification.self, - IncomingEtherNotification.self + IncomingEtherNotification.self, + ConfirmationRequestNotification.self ] as [MultisigNotification.Type]) .compactMap({ $0.init(payload: payload) }) .first diff --git a/NotificationServiceExtension/Notifications/ConfirmationRequestNotification.swift b/NotificationServiceExtension/Notifications/ConfirmationRequestNotification.swift new file mode 100644 index 000000000..880e1e6fd --- /dev/null +++ b/NotificationServiceExtension/Notifications/ConfirmationRequestNotification.swift @@ -0,0 +1,33 @@ +// +// ConfirmationRequestNotification.swift +// NotificationServiceExtension +// +// Created by Andrey Scherbovich on 25.11.20. +// Copyright © 2020 Gnosis Ltd. All rights reserved. +// + +import Foundation + +struct ConfirmationRequestNotification: MultisigNotification { + let address: PlainAddress + + init?(payload: NotificationPayload) { + guard + let rawType = payload.type, + let type = NotificationType(rawValue: rawType), + type == .confirmationRequest, + let address = PlainAddress(payload.address) + else { + return nil + } + self.address = address + } + + var localizedTitle: String { + "Confirmation required" + } + + var localizedBody: String { + "\(address.truncatedInMiddle): A transaction requires your confirmation!" + } +} diff --git a/NotificationServiceExtension/Notifications/MultisigNotification.swift b/NotificationServiceExtension/Notifications/MultisigNotification.swift index 4e8d41e87..df906f8a4 100644 --- a/NotificationServiceExtension/Notifications/MultisigNotification.swift +++ b/NotificationServiceExtension/Notifications/MultisigNotification.swift @@ -13,6 +13,7 @@ enum NotificationType: String { case incomingToken = "INCOMING_TOKEN" case executedMultisigTx = "EXECUTED_MULTISIG_TRANSACTION" case newConfirmation = "NEW_CONFIRMATION" + case confirmationRequest = "CONFIRMATION_REQUEST" } protocol MultisigNotification {