Skip to content

Commit

Permalink
gh-716 update RegisterNotificationTokenRequest
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrey Scherbovich committed Nov 25, 2020
1 parent 72e2baf commit 8c9350d
Show file tree
Hide file tree
Showing 10 changed files with 187 additions and 37 deletions.
20 changes: 20 additions & 0 deletions Multisig.xcodeproj/project.pbxproj
Expand Up @@ -362,6 +362,9 @@
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 */; };
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 */; };
Expand Down Expand Up @@ -867,6 +870,9 @@
5588366E2565385F0014E8C7 /* RemoveSafeCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveSafeCell.swift; sourceTree = "<group>"; };
5588366F2565385F0014E8C7 /* RemoveSafeCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RemoveSafeCell.xib; sourceTree = "<group>"; };
558F3CB124AB2C9500BBCDF1 /* RefreshableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshableScrollView.swift; sourceTree = "<group>"; };
559670FE256E88FC0097D3A2 /* Signer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Signer.swift; sourceTree = "<group>"; };
55967104256E92470097D3A2 /* SignerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignerTests.swift; sourceTree = "<group>"; };
5596710C256E935F0097D3A2 /* MockSecureStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSecureStore.swift; sourceTree = "<group>"; };
5596C03725507EAE00EF23A5 /* CollectibleTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CollectibleTableViewCell.xib; sourceTree = "<group>"; };
5596C03D25507F3C00EF23A5 /* CollectibleTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectibleTableViewCell.swift; sourceTree = "<group>"; };
5596C0472550813B00EF23A5 /* CollectiblesHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectiblesHeaderView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1928,6 +1934,7 @@
55A585352501417E005E778B /* PrivateKey */,
0A0983A724BC5CC8009EE296 /* TokenRegistry */,
0A53071F254311C400E8A270 /* SafeTransactionSigner.swift */,
559670FE256E88FC0097D3A2 /* Signer.swift */,
);
path = Models;
sourceTree = "<group>";
Expand Down Expand Up @@ -1973,6 +1980,7 @@
551DDEEA244F192300C719D3 /* Models */ = {
isa = PBXGroup;
children = (
5596710B256E933D0097D3A2 /* Mocks */,
555312F0250673840008206B /* Private Key */,
047300FD24781081004F1200 /* AddressTests.swift */,
551DDEEF244F1A0A00C719D3 /* SafeTests.swift */,
Expand All @@ -1986,6 +1994,7 @@
0A19D43C249A5BF100A316B6 /* Transaction */,
0A0983A824BC6222009EE296 /* InMemoryTokenStoreTests.swift */,
0A0983AA24BC638C009EE296 /* HardcodedTokenStoreTests.swift */,
55967104256E92470097D3A2 /* SignerTests.swift */,
);
path = Models;
sourceTree = "<group>";
Expand Down Expand Up @@ -2094,6 +2103,14 @@
path = ReusableViews;
sourceTree = "<group>";
};
5596710B256E933D0097D3A2 /* Mocks */ = {
isa = PBXGroup;
children = (
5596710C256E935F0097D3A2 /* MockSecureStore.swift */,
);
path = Mocks;
sourceTree = "<group>";
};
55A585352501417E005E778B /* PrivateKey */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -2842,6 +2859,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 */,
Expand Down Expand Up @@ -2970,6 +2988,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 */,
Expand All @@ -2983,6 +3002,7 @@
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 */,
551DDEF0244F1A0A00C719D3 /* SafeTests.swift in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion Multisig/Cross-layer/App.swift
Expand Up @@ -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(
Expand Down
Expand Up @@ -7,27 +7,50 @@
//

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 = String(Int(Date().timeIntervalSince1970 * 1_000))
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: UUID, safes: [Address], token: String, bundle: String, version: String, buildNumber: String) throws {
self.uuid = deviceID.uuidString.lowercased()
self.safes = safes.map { $0.checksummed }
self.cloudMessagingToken = token
self.bundle = bundle
self.version = version
self.buildNumber = buildNumber

let string = [
"gnosis-safe",
self.uuid,
self.safes.reduce("") { $0 + $1 },
self.cloudMessagingToken,
self.bundle,
self.version,
self.deviceType,
self.buildNumber,
self.timestamp
]
.reduce("") { $0 + $1 }

if let signature = try? Signer.sign(string).value {
self.signatures = [signature]
} else {
self.signatures = nil
}
}

struct Response: Decodable {
Expand All @@ -43,7 +66,21 @@ 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: UUID,
safes: [Address],
token: String,
bundle: String,
version: String,
buildNumber: String) throws -> RegisterNotificationTokenRequest.Response {

return try execute(
request: try RegisterNotificationTokenRequest(deviceID: deviceID,
safes: safes,
token: token,
bundle: bundle,
version: version,
buildNumber: buildNumber)
)
}
}
Expand Up @@ -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
}

Expand Down
18 changes: 3 additions & 15 deletions Multisig/Logic/Models/SafeTransactionSigner.swift
Expand Up @@ -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 {
Expand Down
46 changes: 46 additions & 0 deletions 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 {
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: .ascii)!)
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)
}
}
25 changes: 10 additions & 15 deletions Multisig/UI/App/RemoteNotificationHandler.swift
Expand Up @@ -13,8 +13,8 @@ import Firebase

class RemoteNotificationHandler {

@UserDefault(key: "io.gnosis.multisig.deviceID")
private var storedDeviceID: String?
@UserDefaultWithDefault(key: "io.gnosis.multisig.deviceID", defaultValue: UUID().uuidString)
private var storedDeviceID: String

@EnumDefault(key: "io.gnosis.multisig.authorizationStatus")
private var authorizationStatus: UNAuthorizationStatus?
Expand All @@ -24,13 +24,12 @@ 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? {
private var deviceID: UUID {
get {
storedDeviceID.flatMap { UUID(uuidString: $0) }
UUID(uuidString: storedDeviceID)!
}
set {
storedDeviceID = newValue?.uuidString
storedDeviceID = newValue.uuidString
}
}

Expand Down Expand Up @@ -67,9 +66,7 @@ class RemoteNotificationHandler {

func safeAdded(address: Address) {
logDebug("Safe added: \(address)")
if authorizationStatus == nil {
requestUserPermissionAndRegister()
} else {
if authorizationStatus != nil {
register(addresses: [address])
}
}
Expand Down Expand Up @@ -166,28 +163,26 @@ 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
do {
let response = try App.shared.safeTransactionService
try App.shared.safeTransactionService
.register(deviceID: self.deviceID,
safes: addresses,
token: token,
bundle: appConfig.bundleIdentifier,
version: appConfig.marketingVersion,
buildNumber: appConfig.buildVersion)
self.deviceID = response.uuid
} catch {
logError("Failed to register device", error)
}
}
}

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.deviceID, address: address)
} catch {
logError("Failed to unregister device", error)
}
Expand Down
10 changes: 10 additions & 0 deletions Multisig/UI/App/UserDefaultPropertyWrapper.swift
Expand Up @@ -17,6 +17,16 @@ struct UserDefault<T> {
}
}

@propertyWrapper
struct UserDefaultWithDefault<T> {
var key: String
var defaultValue: T
var wrappedValue: T {
set { UserDefaults.standard.set(newValue, forKey: key) }
get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue }
}
}

@propertyWrapper
struct EnumDefault<E: RawRepresentable> {
var key: String
Expand Down
30 changes: 30 additions & 0 deletions 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]()
}
}

0 comments on commit 8c9350d

Please sign in to comment.