├── .github └── workflows │ └── swift.yml ├── .gitignore ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ └── xcschemes │ └── DeSoIdentity.xcscheme ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── DeSoIdentity │ ├── DeSoIdentity.swift │ ├── Extensions │ ├── Array+Ext.swift │ ├── String+Ext.swift │ └── UInt+Ext.swift │ ├── Models │ ├── DerivedKeyInfo.swift │ ├── EncryptedMessagesThread.swift │ ├── Errors.swift │ ├── SharedSecret.swift │ └── UnsignedTransaction.swift │ ├── Views │ └── PresentationContextProvider.swift │ └── Workers │ ├── AuthWorker.swift │ ├── Crypto │ └── ECIES.swift │ ├── DeSo │ └── DeSoHelpers.swift │ ├── JWTCreator.swift │ ├── Keychain │ └── KeyInfoStorageWorker.swift │ ├── MessageDecryptionWorker.swift │ └── SignTransactionWorker.swift └── Tests └── DeSoIdentityTests ├── DeSoIdentityTests.swift ├── ECIESTests.swift ├── KeystoreTests.swift └── Mocks ├── MockAuthWorker.swift ├── MockJWTCreator.swift ├── MockKeyStore.swift ├── MockMessageDecrypter.swift ├── MockPresentationContextProvider.swift └── MockTransactionSigner.swift /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - name: Setup Xcode version 16 | uses: maxim-lobanov/setup-xcode@v1.3.0 17 | with: 18 | xcode-version: latest-stable 19 | - uses: actions/checkout@v2 20 | - name: Build 21 | run: swift build -v 22 | - name: Run tests 23 | run: swift test -v 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | 9 | .idea 10 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/DeSoIdentity.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 48 | 54 | 55 | 56 | 57 | 58 | 68 | 69 | 75 | 76 | 82 | 83 | 84 | 85 | 87 | 88 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "ASN1", 6 | "repositoryURL": "https://github.com/leif-ibsen/ASN1", 7 | "state": { 8 | "branch": null, 9 | "revision": "53e0b34a34f882386e09d10d6d00bb7e969e307f", 10 | "version": "1.2.1" 11 | } 12 | }, 13 | { 14 | "package": "BigInt", 15 | "repositoryURL": "https://github.com/leif-ibsen/BigInt", 16 | "state": { 17 | "branch": null, 18 | "revision": "56a12cb278293d8e6bec91df6a1e5265e294d6ce", 19 | "version": "1.2.5" 20 | } 21 | }, 22 | { 23 | "package": "CryptoSwift", 24 | "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "4e31051c63cc0ddf10a25cf5318856c510cf77f4", 28 | "version": "1.4.0" 29 | } 30 | }, 31 | { 32 | "package": "KeychainAccess", 33 | "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "84e546727d66f1adc5439debad16270d0fdd04e7", 37 | "version": "4.2.2" 38 | } 39 | }, 40 | { 41 | "package": "Base16", 42 | "repositoryURL": "https://github.com/alja7dali/swift-base16.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "4e452d40d08457bce120a621231e0b9e6815a9f8", 46 | "version": "1.0.0" 47 | } 48 | }, 49 | { 50 | "package": "Base58", 51 | "repositoryURL": "https://github.com/Alja7dali/swift-base58.git", 52 | "state": { 53 | "branch": null, 54 | "revision": "1eecf184e48d2702036663c7d238aab201bb9924", 55 | "version": "1.0.0" 56 | } 57 | }, 58 | { 59 | "package": "Bits", 60 | "repositoryURL": "https://github.com/alja7dali/swift-bits.git", 61 | "state": { 62 | "branch": null, 63 | "revision": "83668ec40d877d9e0012ba93359191cfc4bf04af", 64 | "version": "1.0.0" 65 | } 66 | }, 67 | { 68 | "package": "SHA256", 69 | "repositoryURL": "https://github.com/alja7dali/swift-sha256.git", 70 | "state": { 71 | "branch": null, 72 | "revision": "758852c8a92cacb8d125e152a2134b0cf9535e8e", 73 | "version": "1.0.0" 74 | } 75 | }, 76 | { 77 | "package": "SwiftECC", 78 | "repositoryURL": "https://github.com/leif-ibsen/SwiftECC", 79 | "state": { 80 | "branch": null, 81 | "revision": "621c70126966e5c289c4dc0ff801c6282b6baa05", 82 | "version": "1.0.2" 83 | } 84 | } 85 | ] 86 | }, 87 | "version": 1 88 | } 89 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "DeSoIdentity", 8 | platforms: [ 9 | .iOS(.v13), 10 | .macOS(.v10_15) 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, and make them visible to other packages. 14 | .library( 15 | name: "DeSoIdentity", 16 | targets: ["DeSoIdentity"]), 17 | ], 18 | dependencies: [ 19 | // Dependencies declare other packages that this package depends on. 20 | // .package(url: /* package url */, from: "1.0.0"), 21 | .package(name: "KeychainAccess", url: "https://github.com/kishikawakatsumi/KeychainAccess.git", from: "4.2.2"), 22 | .package(name: "CryptoSwift", url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMinor(from: "1.4.0")), 23 | .package(name: "SwiftECC", url: "https://github.com/leif-ibsen/SwiftECC", from: "1.0.2"), 24 | .package(name: "Base58", url: "https://github.com/Alja7dali/swift-base58.git", from: "1.0.0") 25 | ], 26 | targets: [ 27 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 28 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 29 | .target( 30 | name: "DeSoIdentity", 31 | dependencies: ["KeychainAccess", "CryptoSwift", "SwiftECC", "Base58"] 32 | ), 33 | .testTarget( 34 | name: "DeSoIdentityTests", 35 | dependencies: ["DeSoIdentity"] 36 | ), 37 | ] 38 | ) 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DeSo Identity 2 | 3 | This is the native iOS library for DeSo Identity. It's not ready yet, but will be soon. Watch this space! 4 | -------------------------------------------------------------------------------- /Sources/DeSoIdentity/DeSoIdentity.swift: -------------------------------------------------------------------------------- 1 | import AuthenticationServices 2 | 3 | /** 4 | The main entry point for the library 5 | */ 6 | public class Identity { 7 | 8 | /** 9 | The possible responses when login is called 10 | */ 11 | public enum LoginResponse { 12 | case success(selectedPublicKey: String, allLoadedPublicKeys: [String]) 13 | case failed(error: Error) 14 | } 15 | 16 | /** 17 | Completion handler called upon successful login. 18 | - Parameter response: the response of the login request 19 | */ 20 | public typealias LoginCompletion = ((_ response: LoginResponse) -> Void) 21 | 22 | private let authWorker: Authable 23 | private var keyStore: KeyInfoStorable 24 | private let transactionSigner: TransactionSignable 25 | private let messageDecrypter: MessageDecryptable 26 | private let jwtWorker: JWTFetchable 27 | private let context: PresentationContextProvidable 28 | private let network: Network 29 | private let overrideIdentityURL: String? 30 | 31 | public convenience init(network: Network = .mainnet, overrideIdentityURL: String? = nil) throws { 32 | #if os(iOS) 33 | guard let window = UIApplication.shared.windows.first else { 34 | throw IdentityError.missingPresentationAnchor 35 | } 36 | let context = PresentationContextProvider(anchor: window) 37 | #elseif os(macOS) 38 | guard let window = NSApplication.shared.windows.first else { 39 | throw IdentityError.missingPresentationAnchor 40 | } 41 | let context = PresentationContextProvider(anchor: window) 42 | #endif 43 | 44 | #if targetEnvironment(simulator) 45 | let keyStore = EphemeralKeyStore() 46 | #else 47 | let keyStore = KeyInfoStorageWorker() 48 | #endif 49 | 50 | self.init( 51 | authWorker: AuthWorker(), 52 | keyStore: keyStore, 53 | transactionSigner: SignTransactionWorker(), 54 | messageDecrypter: MessageDecryptionWorker(), 55 | jwtWorker: JWTWorker(), 56 | context: context, 57 | network: network, 58 | overrideIdentityURL: overrideIdentityURL 59 | ) 60 | } 61 | 62 | internal init( 63 | authWorker: Authable, 64 | keyStore: KeyInfoStorable, 65 | transactionSigner: TransactionSignable, 66 | messageDecrypter: MessageDecryptable, 67 | jwtWorker: JWTFetchable, 68 | context: PresentationContextProvidable, 69 | network: Network, 70 | overrideIdentityURL: String? 71 | ) { 72 | self.authWorker = authWorker 73 | self.keyStore = keyStore 74 | self.transactionSigner = transactionSigner 75 | self.messageDecrypter = messageDecrypter 76 | self.jwtWorker = jwtWorker 77 | self.context = context 78 | self.network = network 79 | self.overrideIdentityURL = overrideIdentityURL 80 | } 81 | 82 | // TODO: When Swift 5.5. is widely available, update this to use async/await 83 | /** 84 | Call this to log in to one or more accounts. 85 | - Parameter completion: Will be called on completion of the login flow 86 | */ 87 | public func login(_ completion: @escaping LoginCompletion ) { 88 | authWorker.presentAuthSession(context: context, on: self.network, overrideUrl: overrideIdentityURL, with: completion) 89 | } 90 | 91 | /** 92 | Call this to log an account out 93 | - Parameter publicKey: The true public key of the account to be logged out 94 | - Returns: An array of the remaining logged in true public keys 95 | */ 96 | public func logout(_ publicKey: String) throws -> [String] { 97 | try keyStore.clearDerivedKeyInfo(for: publicKey) 98 | 99 | // Question: when an account is logged out, presumably we also need to delete any shared secrets relating to its private key? 100 | 101 | return try keyStore.getAllStoredKeys() 102 | } 103 | 104 | /** 105 | Get a list of the true publis keys currently logged in 106 | - Returns: An array of all the currently logged in true public keys 107 | */ 108 | public func getLoggedInKeys() throws -> [String] { 109 | return try keyStore.getAllStoredKeys() 110 | } 111 | 112 | /** 113 | Remove all the info currently stored 114 | */ 115 | public func removeAllKeys() throws { 116 | try keyStore.clearAllStoredInfo() 117 | } 118 | 119 | /** 120 | Sign a transaction to be committed to the blockchain. Note, this does not write the transaction, it just signs it. 121 | - Parameter transaction: and `UnsignedTransaction` object to be signed 122 | - Returns: A signed hash of the transaction 123 | */ 124 | public func sign(_ transaction: UnsignedTransaction) throws -> String { 125 | return try transactionSigner.signTransaction(transaction) 126 | } 127 | 128 | /** 129 | Decrypt private messages from a collection of threads 130 | - Parameters: 131 | - threads: An array of `EncryptedMessagesThread` objects to be decrypted 132 | - myPublicKey: The public key of the calling user's account 133 | - errorOnFailure: true if failure to decrypt messages should return an error, false if messages which cannot be decrypted should just be ommitted from the results 134 | - Returns: A dictionary with keys of the publicKeys of the threads and values of the messages contained in the thread, in the order they were sent 135 | */ 136 | public func decrypt(_ threads: [EncryptedMessagesThread], for myPublicKey: String, errorOnFailure: Bool = false) throws -> [String: [String]] { 137 | return try messageDecrypter.decryptThreads(threads, for: myPublicKey, errorOnFailure: errorOnFailure) 138 | } 139 | 140 | /** 141 | Decrypt private messages from a single thread 142 | - Parameters: 143 | - thread: An `EncryptedMessagesThread` object to be decrypted 144 | - myPublicKey: The public key of the calling user's account 145 | - errorOnFailure: true if failure to decrypt messages should return an error, false if messages which cannot be decrypted should just be ommitted from the results 146 | - Returns: An array of decrypted message strings in the order they were sent 147 | */ 148 | public func decrypt(_ thread: EncryptedMessagesThread, for myPublicKey: String, errorOnFailure: Bool = false) throws -> [String] { 149 | return try messageDecrypter.decryptThread(thread, for: myPublicKey, errorOnFailure: errorOnFailure) 150 | } 151 | 152 | /** 153 | Retrieve a JWT that verifies ownership of the publicKey 154 | - Parameter publicKey: The public key for which ownership is to be verified 155 | - Returns: A base64 JWT string 156 | - Throws: Error if the publicKey is not logged in 157 | */ 158 | public func jwt(for publicKey: String) throws -> String { 159 | return try jwtWorker.getJWT(for: publicKey) 160 | } 161 | } 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /Sources/DeSoIdentity/Extensions/Array+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Andy Boyd on 08/07/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Array { 11 | func slice(from start: Int, to end: Int? = nil) -> Array { 12 | if let end = end { 13 | return Array(self[start..(endian: T, count: Int) -> [UInt8] { 16 | var _endian = endian 17 | let bytePtr = withUnsafePointer(to: &_endian) { 18 | $0.withMemoryRebound(to: UInt8.self, capacity: count) { 19 | UnsafeBufferPointer(start: $0, count: count) 20 | } 21 | } 22 | return [UInt8](bytePtr) 23 | } 24 | } 25 | 26 | extension UInt16: UIntToBytesConvertable { 27 | var toBytes: [UInt8] { 28 | if CFByteOrderGetCurrent() == Int(CFByteOrderLittleEndian.rawValue) { 29 | return toByteArr(endian: self.littleEndian, 30 | count: MemoryLayout.size) 31 | } else { 32 | return toByteArr(endian: self.bigEndian, 33 | count: MemoryLayout.size) 34 | } 35 | } 36 | } 37 | 38 | extension UInt32: UIntToBytesConvertable { 39 | var toBytes: [UInt8] { 40 | if CFByteOrderGetCurrent() == Int(CFByteOrderLittleEndian.rawValue) { 41 | return toByteArr(endian: self.littleEndian, 42 | count: MemoryLayout.size) 43 | } else { 44 | return toByteArr(endian: self.bigEndian, 45 | count: MemoryLayout.size) 46 | } 47 | } 48 | } 49 | 50 | extension UInt64: UIntToBytesConvertable { 51 | var toBytes: [UInt8] { 52 | if CFByteOrderGetCurrent() == Int(CFByteOrderLittleEndian.rawValue) { 53 | return toByteArr(endian: self.littleEndian, 54 | count: MemoryLayout.size) 55 | } else { 56 | return toByteArr(endian: self.bigEndian, 57 | count: MemoryLayout.size) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/DeSoIdentity/Models/DerivedKeyInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DerivedKeyInfo.swift 3 | // 4 | // 5 | // Created by Andy Boyd on 30/06/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum Network: String, Codable { 11 | case mainnet 12 | case testnet 13 | } 14 | 15 | public struct DerivedKeyInfo: Codable, Equatable { 16 | public let publicKey: String 17 | public let derivedPublicKey: String 18 | public let derivedSeedHex: String 19 | public let btcDepositAddress: String 20 | public let expirationBlock: Int 21 | public let accessSignature: String 22 | public let network: Network 23 | public let jwt: String 24 | public let derivedJwt: String 25 | } 26 | 27 | extension DerivedKeyInfo { 28 | init?(_ query: [URLQueryItem]) { 29 | // TODO: Confirm with Identity web app if this matches how the data will be returned from the auth session 30 | guard let truePubKey = query.first(where: { $0.name == "publicKey" })?.value, 31 | let newPubKey = query.first(where: { $0.name == "derivedPublicKey" })?.value, 32 | let newPrivateKey = query.first(where: { $0.name == "derivedSeedHex" })?.value, 33 | let signedHash = query.first(where: { $0.name == "accessSignature" })?.value, 34 | let btcDepositAddress = query.first(where: { $0.name == "btcDepositAddress" })?.value, 35 | let network = Network(rawValue: query.first(where: { $0.name == "network" })?.value ?? ""), 36 | let expirationBlockValue = query.first(where: { $0.name == "expirationBlock" })?.value, 37 | let expirationBlock = Int(expirationBlockValue), 38 | let jwt = query.first(where: { $0.name == "jwt" })?.value, 39 | let derivedJwt = query.first(where: { $0.name == "derivedJwt" })?.value else { 40 | return nil 41 | } 42 | 43 | self.publicKey = truePubKey 44 | self.derivedPublicKey = newPubKey 45 | self.derivedSeedHex = newPrivateKey 46 | self.accessSignature = signedHash 47 | self.btcDepositAddress = btcDepositAddress 48 | self.network = network 49 | self.expirationBlock = expirationBlock 50 | self.jwt = jwt 51 | self.derivedJwt = derivedJwt 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/DeSoIdentity/Models/EncryptedMessagesThread.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EncryptedMessages.swift 3 | // 4 | // 5 | // Created by Andy Boyd on 29/06/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct EncryptedMessagesThread: Equatable { 11 | let publicKey: String 12 | let encryptedMessages: [String] 13 | } 14 | -------------------------------------------------------------------------------- /Sources/DeSoIdentity/Models/Errors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Errors.swift 3 | // 4 | // 5 | // Created by Andy Boyd on 29/06/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum IdentityError: Swift.Error { 11 | case missingPresentationAnchor 12 | case notLoggedIn 13 | case missingInfoForPublicKey 14 | case keyInfoExpired 15 | case missingSharedSecret 16 | case signatureNotAString 17 | } 18 | 19 | enum CryptoError: Swift.Error { 20 | case badPrivateKey 21 | case badPublicKey 22 | case badSignature 23 | case couldNotGetPublicKey 24 | case emptyMessage 25 | case messageTooLong 26 | case couldNotGenerateRandomBytes(status: OSStatus) 27 | case invalidCipherText 28 | case incorrectMAC 29 | } 30 | -------------------------------------------------------------------------------- /Sources/DeSoIdentity/Models/SharedSecret.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Andy Boyd on 05/07/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | struct SharedSecret: Codable { 11 | let secret: String 12 | let privateKey: String 13 | let publicKey: String 14 | let myTruePublicKey: String 15 | } 16 | -------------------------------------------------------------------------------- /Sources/DeSoIdentity/Models/UnsignedTransaction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnsignedTransaction.swift 3 | // 4 | // 5 | // Created by Andy Boyd on 29/06/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct UnsignedTransaction: Equatable { 11 | let publicKey: String 12 | let transactionHex: String 13 | } 14 | -------------------------------------------------------------------------------- /Sources/DeSoIdentity/Views/PresentationContextProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PresentationContextProvider.swift 3 | // 4 | // 5 | // Created by Andy Boyd on 29/06/2021. 6 | // 7 | 8 | import Foundation 9 | import AuthenticationServices 10 | 11 | protocol PresentationContextProvidable: ASWebAuthenticationPresentationContextProviding {} 12 | 13 | class PresentationContextProvider: NSObject, PresentationContextProvidable { 14 | private let anchor: ASPresentationAnchor 15 | 16 | init(anchor: ASPresentationAnchor) { 17 | self.anchor = anchor 18 | } 19 | 20 | func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { 21 | return anchor 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/DeSoIdentity/Workers/AuthWorker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthWorker.swift 3 | // 4 | // 5 | // Created by Andy Boyd on 29/06/2021. 6 | // 7 | 8 | import Foundation 9 | import AuthenticationServices 10 | 11 | protocol Authable { 12 | func presentAuthSession(context: PresentationContextProvidable, 13 | on network: Network, overrideUrl: String?, 14 | with completion: @escaping Identity.LoginCompletion) 15 | } 16 | 17 | class AuthWorker: Authable { 18 | private var keyStore: KeyInfoStorable = KeyInfoStorageWorker() 19 | 20 | func presentAuthSession(context: PresentationContextProvidable, 21 | on network: Network, overrideUrl: String?, 22 | with completion: @escaping Identity.LoginCompletion) { 23 | var url: String 24 | if let overrideUrl = overrideUrl { 25 | url = overrideUrl 26 | } else { 27 | let baseUrl = "https://identity.bitclout.com" 28 | url = baseUrl + "/derive" 29 | } 30 | let callbackScheme = (Bundle.main.bundleIdentifier ?? UUID().uuidString) + ".identity" 31 | url = url 32 | + "?callback=" 33 | + callbackScheme.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)! 34 | + "://".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! 35 | + "&webview=true" 36 | if network == .testnet { 37 | url = url + "&testnet=true" 38 | } 39 | 40 | let session = ASWebAuthenticationSession(url: URL(string: url)!, 41 | callbackURLScheme: callbackScheme) { url, error in 42 | guard let url = url else { 43 | print(error?.localizedDescription ?? "No URL Returned") 44 | return 45 | } 46 | 47 | let components = URLComponents(url: url, resolvingAgainstBaseURL: false) 48 | guard let query = components?.queryItems, 49 | let keyData = DerivedKeyInfo(query) else { 50 | print("Unexpected data returned") 51 | return 52 | } 53 | do { 54 | try self.keyStore.store(keyData) 55 | let allKeys = try self.keyStore.getAllStoredKeys() 56 | completion(.success(selectedPublicKey: keyData.publicKey, allLoadedPublicKeys: allKeys)) 57 | } catch { 58 | print(error.localizedDescription) 59 | completion(.failed(error: error)) 60 | } 61 | } 62 | session.presentationContextProvider = context 63 | session.prefersEphemeralWebBrowserSession = false 64 | session.start() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/DeSoIdentity/Workers/Crypto/ECIES.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ECIES.swift 3 | // 4 | // 5 | // Created by Andy Boyd on 06/07/2021. 6 | // 7 | 8 | import Foundation 9 | import Security 10 | 11 | import CryptoSwift 12 | import SwiftECC 13 | import BigInt 14 | import ASN1 15 | 16 | private let ec = Domain.instance(curve: .EC256k1) 17 | 18 | func randomBytes(count: Int? = nil) throws -> [UInt8] { 19 | var bytes = [UInt8](repeating: 0, count: count ?? Int.random(in: 1..<2048)) 20 | let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) 21 | 22 | guard status == errSecSuccess else { 23 | throw CryptoError.couldNotGenerateRandomBytes(status: status) 24 | } 25 | return bytes 26 | } 27 | 28 | func kdf(secret: [UInt8], outputLength: Int) -> [UInt8] { 29 | var ctr: UInt8 = 1 30 | var written = 0 31 | var result: [UInt8] = [] 32 | while (written < outputLength) { 33 | var ctrs: [UInt8] = [ctr >> 24, ctr >> 16, ctr >> 8, ctr] 34 | ctrs.append(contentsOf: secret) 35 | let hashResult = Hash.sha256(ctrs) 36 | result.append(contentsOf: hashResult) 37 | written += 32 38 | ctr += 1 39 | } 40 | return result 41 | } 42 | 43 | func aesCtrEncrypt(iv: [UInt8], key: [UInt8], data: [UInt8]) throws -> [UInt8] { 44 | var cipher = try AES(key: key, blockMode: CTR(iv: iv), padding: .noPadding).makeEncryptor() 45 | let firstChunk = try cipher.update(withBytes: data) 46 | let secondChunk = try cipher.finish() 47 | return firstChunk + secondChunk 48 | } 49 | 50 | func aesCtrDecrypt(iv: [UInt8], key: [UInt8], data: [UInt8]) throws -> [UInt8] { 51 | var cipher = try AES(key: key, blockMode: CTR(iv: iv), padding: .noPadding).makeDecryptor() 52 | let firstChunk = try cipher.update(withBytes: data) 53 | let secondChunk = try cipher.finish() 54 | return firstChunk + secondChunk 55 | } 56 | 57 | 58 | func aesCtrEncryptLegacy(iv: [UInt8], key: [UInt8], data: [UInt8]) throws -> [UInt8] { 59 | var cipher = try AES(key: key, blockMode: CTR(iv: iv), padding: .pkcs7).makeEncryptor() 60 | return try cipher.update(withBytes: data) 61 | } 62 | 63 | func aesCtrDecryptLegacy(iv: [UInt8], key: [UInt8], data: [UInt8]) throws -> [UInt8] { 64 | var cipher = try AES(key: key, blockMode: CTR(iv: iv), padding: .pkcs7).makeDecryptor() 65 | return try cipher.update(withBytes: data) 66 | } 67 | 68 | func hmacSha256Sign(key: [UInt8], msg: [UInt8]) throws -> [UInt8] { 69 | return try HMAC(key: key, variant: .sha256).authenticate(msg) 70 | } 71 | 72 | /** 73 | Obtain the public elliptic curve key from a private 74 | */ 75 | func getPublicKey(from privateKey: [UInt8]) throws -> [UInt8] { 76 | guard privateKey.count == 32 else { throw CryptoError.badPrivateKey } 77 | let privKey = try ECPrivateKey(domain: ec, s: BInt(magnitude: privateKey)) 78 | let pubKey = ec.multiply(ec.g, privKey.s) 79 | return try ec.encodePoint(pubKey) 80 | } 81 | 82 | /** 83 | ECDSA 84 | */ 85 | func sign(privateKey: [UInt8], msg: [UInt8]) throws -> [UInt8] { 86 | guard privateKey.count == 32 else { throw CryptoError.badPrivateKey } 87 | guard msg.count > 0 else { throw CryptoError.emptyMessage } 88 | guard msg.count <= 32 else { throw CryptoError.messageTooLong } 89 | 90 | let privKey = try ECPrivateKey(domain: ec, s: BInt(magnitude: privateKey)) 91 | return privKey.sign(msg: msg).asn1.encode() 92 | } 93 | 94 | /** 95 | Verify ECDSA signatures 96 | */ 97 | func verify(publicKey: [UInt8], msg: [UInt8], sig: [UInt8]) throws -> Bool { 98 | guard publicKey.count == 65, publicKey[0] == 4 else { throw CryptoError.badPublicKey } 99 | guard msg.count > 0 else { throw CryptoError.emptyMessage } 100 | guard msg.count <= 32 else { throw CryptoError.messageTooLong } 101 | let pubKey = try ECPublicKey(domain: ec, w: ec.decodePoint(publicKey)) 102 | let signature = try ECSignature(asn1: ASN1.build(sig), domain: ec) 103 | return pubKey.verify(signature: signature, msg: msg) 104 | } 105 | 106 | /** 107 | ECDH 108 | This function derives a point from a private key and a non matching public key, and returns the X coordinate of the derived point 109 | */ 110 | func deriveX(privateKeyA: [UInt8], publicKeyB: [UInt8]) throws -> [UInt8] { 111 | let keyA = BInt(magnitude: privateKeyA) 112 | let keyB = try ec.decodePoint(publicKeyB) 113 | 114 | let derived = ec.multiply(keyB, keyA).x 115 | return derived.asMagnitudeBytes() 116 | } 117 | 118 | /** 119 | Encrypt AES-128-CTR and serialise as in Parity 120 | Serialization: 121 | */ 122 | func encrypt(publicKeyTo: [UInt8], msg: [UInt8], ephemPrivateKey: [UInt8]? = nil, iv: [UInt8]? = nil, legacy: Bool = false) throws -> [UInt8] { 123 | let ephemPrivateKey = try ephemPrivateKey ?? randomBytes(count: 32) 124 | let ephemPublicKey = try getPublicKey(from: ephemPrivateKey) 125 | 126 | let sharedPx = try deriveX(privateKeyA: ephemPrivateKey, publicKeyB: publicKeyTo) 127 | let hash = kdf(secret: sharedPx, outputLength: 32) 128 | let iv = try iv ?? randomBytes(count: 16) 129 | let encryptionKey = hash.slice(from: 0, to: 16) 130 | 131 | let macKey = Hash.sha256(hash.slice(from: 16)) 132 | 133 | let cipherText = try legacy ? 134 | aesCtrEncryptLegacy(iv: iv, key: encryptionKey, data: msg) : 135 | aesCtrEncrypt(iv: iv, key: encryptionKey, data: msg) 136 | 137 | let dataToMac = iv + cipherText 138 | let hmac = try hmacSha256Sign(key: macKey, msg: dataToMac) 139 | 140 | return ephemPublicKey + iv + cipherText + hmac 141 | } 142 | 143 | /** 144 | Decrypt serialised AES-128-CTR 145 | */ 146 | func decrypt(privateKey: [UInt8], encrypted: [UInt8], legacy: Bool = false) throws -> [UInt8] { 147 | let metaLength = 1 + 64 + 16 + 32 148 | guard encrypted.count > metaLength, encrypted[0] >= 2, encrypted[0] <= 4 else { throw CryptoError.invalidCipherText } 149 | 150 | // deserialize 151 | let ephemPublicKey = encrypted.slice(from: 0, to: 65) 152 | let cipherTextLength = encrypted.count - metaLength 153 | let iv = encrypted.slice(from: 65, to: 65 + 16) 154 | let cipherAndIv = encrypted.slice(from: 65, to: 65 + 16 + cipherTextLength) 155 | let cipherText = cipherAndIv.slice(from: 16) 156 | let msgMac = encrypted.slice(from: 65 + 16 + cipherTextLength) 157 | 158 | // check HMAC 159 | let px = try deriveX(privateKeyA: privateKey, publicKeyB: ephemPublicKey) 160 | let hash = kdf(secret: px, outputLength: 32) 161 | let encryptionKey = hash.slice(from: 0, to: 16) 162 | let macKey = Hash.sha256(hash.slice(from: 16)) 163 | guard try hmacSha256Sign(key: macKey, msg: cipherAndIv) == msgMac else { throw CryptoError.incorrectMAC } 164 | 165 | return try legacy ? 166 | aesCtrDecryptLegacy(iv: iv, key: encryptionKey, data: cipherText) : 167 | aesCtrDecrypt(iv: iv, key: encryptionKey, data: cipherText) 168 | } 169 | 170 | /** 171 | Encrypt AES-128-CTR and serialise as in Parity 172 | Using ECDH shared secret KDF 173 | Serialization: 174 | */ 175 | func encryptShared(privateKeySender: [UInt8], 176 | publicKeyRecipient: [UInt8], 177 | msg: [UInt8], 178 | ephemPrivateKey: [UInt8]? = nil, 179 | iv: [UInt8]? = nil) throws -> [UInt8] { 180 | let sharedPx = try deriveX(privateKeyA: privateKeySender, publicKeyB: publicKeyRecipient) 181 | return try encryptShared(sharedPx: sharedPx, msg: msg, ephemPrivateKey: ephemPrivateKey, iv: iv) 182 | } 183 | 184 | func encryptShared(sharedPx: [UInt8], 185 | msg: [UInt8], 186 | ephemPrivateKey: [UInt8]? = nil, 187 | iv: [UInt8]? = nil) throws -> [UInt8] { 188 | let sharedPrivateKey = kdf(secret: sharedPx, outputLength: 32) 189 | let sharedPublicKey = try getPublicKey(from: sharedPrivateKey) 190 | 191 | return try encrypt(publicKeyTo: sharedPublicKey, 192 | msg: msg, 193 | ephemPrivateKey: ephemPrivateKey, 194 | iv: iv) 195 | } 196 | 197 | /** 198 | Decrypt serialised AES-128-CTR 199 | Using ECDH shared secret KDF 200 | */ 201 | func decryptShared(privateKeyRecipient: [UInt8], publicKeySender: [UInt8], encrypted: [UInt8]) throws -> [UInt8] { 202 | let sharedPx = try deriveX(privateKeyA: privateKeyRecipient, publicKeyB: publicKeySender) 203 | let sharedPxString = sharedPx.reduce(into: "") { res, cur in 204 | res.append("\(cur),") 205 | } 206 | print(sharedPxString) 207 | return try decryptShared(sharedPx: sharedPx, encrypted: encrypted) 208 | } 209 | 210 | func decryptShared(sharedPx: [UInt8], encrypted: [UInt8]) throws -> [UInt8] { 211 | let sharedPrivateKey = kdf(secret: sharedPx, outputLength: 32) 212 | return try decrypt(privateKey: sharedPrivateKey, encrypted: encrypted) 213 | } 214 | 215 | /** 216 | Sign a Transaction Hex for submission 217 | */ 218 | func signTransaction(seedHex: String, transactionHex: String) throws -> String { 219 | guard let s = BInt(seedHex, radix: 16) else { throw CryptoError.badPrivateKey } 220 | let privateKey = try ECPrivateKey(domain: Domain.instance(curve: .EC256k1), s: s) 221 | 222 | let transactionBytes = [UInt8](hex: transactionHex) 223 | let transactionHash = Hash.sha256(transactionBytes) 224 | let signature = privateKey.sign(msg: transactionHash, deterministic: true) 225 | let signatureBytes = signature.asn1.encode() 226 | 227 | let signatureLength = UInt64(signatureBytes.count).toBytes.filter { $0 != 0 } 228 | 229 | let slicedTransactionBytes = transactionBytes.dropLast() 230 | let signedTransactionBytes: [UInt8] = slicedTransactionBytes + signatureLength + signatureBytes 231 | return signedTransactionBytes.toHexString() 232 | } 233 | -------------------------------------------------------------------------------- /Sources/DeSoIdentity/Workers/DeSo/DeSoHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Andy Boyd on 20/08/2021. 6 | // 7 | 8 | import Foundation 9 | import Base58 10 | import CryptoSwift 11 | 12 | enum Base58Error: Error { 13 | case invalidFormat 14 | case checksumError 15 | } 16 | 17 | func Base58CheckDecodePrefix(input: String, prefixLen: Int) throws -> (result: [UInt8], prefix: [UInt8]) { 18 | guard !input.isEmpty else { throw Base58Error.invalidFormat } 19 | let decoded = try Base58.decode(input.makeBytes()) 20 | if decoded.count < 5 { 21 | throw Base58Error.invalidFormat 22 | } 23 | 24 | let toSum = decoded.slice(from: 0, to: decoded.count - 4) 25 | guard checksum(toSum) == decoded.suffix(4) else { 26 | throw Base58Error.checksumError 27 | } 28 | 29 | let prefix = decoded.prefix(prefixLen) 30 | let payload = decoded.slice(from: prefixLen, to: decoded.count - 4) 31 | return (payload, Array(prefix)) 32 | } 33 | 34 | private func checksum(_ input: [UInt8]) -> [UInt8] { 35 | let h = Hash.sha256(input) 36 | let h2 = Hash.sha256(h) 37 | return h2.slice(from: 0, to: 4) 38 | } 39 | -------------------------------------------------------------------------------- /Sources/DeSoIdentity/Workers/JWTCreator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JWTCreator.swift 3 | // 4 | // 5 | // Created by Andy Boyd on 29/06/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol JWTFetchable { 11 | // TODO: Confirm interface. Might need something more to specify the public key to use 12 | func getJWT(for publicKey: String) throws -> String 13 | } 14 | 15 | class JWTWorker: JWTFetchable { 16 | 17 | private let keyStore: KeyInfoStorable 18 | 19 | init(keyStore: KeyInfoStorable = KeyInfoStorageWorker()) { 20 | self.keyStore = keyStore 21 | } 22 | 23 | func getJWT(for publicKey: String) throws -> String { 24 | guard let keyInfo = try keyStore.getDerivedKeyInfo(for: publicKey) else { 25 | throw IdentityError.missingInfoForPublicKey 26 | } 27 | 28 | // TODO: Check if derived key is expired and get a new one if necessary? 29 | 30 | return keyInfo.jwt 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/DeSoIdentity/Workers/Keychain/KeyInfoStorageWorker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyInfoStorageWorker.swift 3 | // 4 | // 5 | // Created by Andy Boyd on 30/06/2021. 6 | // 7 | 8 | import Foundation 9 | import KeychainAccess 10 | 11 | enum StorableKeys: String { 12 | case derivedKeyInfo 13 | case sharedSecrets 14 | } 15 | 16 | protocol KeyInfoStorable { 17 | var keychain: DataStorable { get set } 18 | mutating func store(_ info: DerivedKeyInfo) throws 19 | mutating func store(sharedSecret: SharedSecret) throws 20 | mutating func clearAllStoredInfo() throws 21 | mutating func clearDerivedKeyInfo(for publicKey: String) throws 22 | mutating func clearAllSharedSecrets(for privateKey: String) throws 23 | mutating func clearSharedSecret(for privateKey: String, and publicKey: String) throws 24 | func getDerivedKeyInfo(for publicKey: String) throws -> DerivedKeyInfo? 25 | func getAllStoredKeys() throws -> [String] 26 | func getSharedSecret(for myPublicKey: String, and otherPublicKey: String) throws -> SharedSecret? 27 | func getAllSharedSecrets() throws -> [SharedSecret] 28 | } 29 | 30 | protocol DataStorable { 31 | func getData(_ key: String) throws -> Data? 32 | mutating func set(_ value: Data, key: String) throws 33 | mutating func remove(_ key: String) throws 34 | } 35 | 36 | /** 37 | Ephemeral Key store for use when storage to the keychain is not desired or supported, e.g. when testing on simulator 38 | */ 39 | class EphemeralKeyStore: KeyInfoStorable { 40 | var keychain: DataStorable = [String: String]() 41 | } 42 | 43 | /** 44 | KeyInfoStorageWorker will store and retrieve key information from the secure device keychain 45 | */ 46 | class KeyInfoStorageWorker: KeyInfoStorable { 47 | var keychain: DataStorable 48 | 49 | init() { 50 | guard let bundleId = Bundle.main.bundleIdentifier else { 51 | fatalError() 52 | } 53 | self.keychain = Keychain(service: bundleId) 54 | } 55 | } 56 | 57 | extension Keychain: DataStorable { 58 | func getData(_ key: String) throws -> Data? { 59 | return try getData(key, ignoringAttributeSynchronizable: true) 60 | } 61 | 62 | func remove(_ key: String) throws { 63 | try remove(key, ignoringAttributeSynchronizable: true) 64 | } 65 | 66 | func set(_ value: Data, key: String) throws { 67 | try set(value, key: key, ignoringAttributeSynchronizable: true) 68 | } 69 | } 70 | 71 | extension Dictionary: DataStorable where Key == String, Value == String { 72 | func getData(_ key: String) throws -> Data? { 73 | guard let string = self[key] else { return nil } 74 | return Data(base64Encoded: string) 75 | } 76 | 77 | mutating func set(_ value: Data, key: String) throws { 78 | self[key] = value.base64EncodedString() 79 | } 80 | 81 | mutating func remove(_ key: String) throws { 82 | self[key] = nil 83 | } 84 | } 85 | 86 | extension KeyInfoStorable { 87 | mutating func store(_ info: DerivedKeyInfo) throws { 88 | var storedInfo = try getExistingDerivedKeyInfo() ?? [:] 89 | let data = try JSONEncoder().encode(info) 90 | storedInfo[info.publicKey] = data 91 | try setKeyInfoOnKeychain(storedInfo) 92 | } 93 | 94 | mutating func store(sharedSecret: SharedSecret) throws { 95 | var storedSecrets = try getExistingSharedSecrets() ?? [] 96 | if let existingIndex = storedSecrets 97 | .firstIndex( 98 | where: { 99 | $0.privateKey == sharedSecret.privateKey && 100 | $0.publicKey == sharedSecret.publicKey 101 | } 102 | ) { 103 | storedSecrets[existingIndex] = sharedSecret 104 | } else { 105 | storedSecrets.append(sharedSecret) 106 | } 107 | try setSharedSecretsOnKeychain(storedSecrets) 108 | } 109 | 110 | mutating func clearAllStoredInfo() throws { 111 | try keychain.remove(StorableKeys.derivedKeyInfo.rawValue) 112 | try keychain.remove(StorableKeys.sharedSecrets.rawValue) 113 | } 114 | 115 | mutating func clearDerivedKeyInfo(for publicKey: String) throws { 116 | guard var storedInfo = try getExistingDerivedKeyInfo() else { return } 117 | storedInfo[publicKey] = nil 118 | try setKeyInfoOnKeychain(storedInfo) 119 | } 120 | 121 | mutating func clearAllSharedSecrets(for privateKey: String) throws { 122 | guard var storedSecrets = try getExistingSharedSecrets() else { return } 123 | storedSecrets.removeAll(where: { 124 | $0.privateKey == privateKey 125 | }) 126 | try setSharedSecretsOnKeychain(storedSecrets) 127 | } 128 | 129 | mutating func clearSharedSecret(for privateKey: String, and publicKey: String) throws { 130 | guard var storedSecrets = try getExistingSharedSecrets() else { return } 131 | storedSecrets.removeAll(where: { 132 | $0.privateKey == privateKey && 133 | $0.publicKey == publicKey 134 | }) 135 | try setSharedSecretsOnKeychain(storedSecrets) 136 | } 137 | 138 | func getDerivedKeyInfo(for publicKey: String) throws -> DerivedKeyInfo? { 139 | guard let thisPublicKeyData = try getExistingDerivedKeyInfo()?[publicKey] else { return nil } 140 | return try JSONDecoder().decode(DerivedKeyInfo.self, from: thisPublicKeyData) 141 | } 142 | 143 | func getAllStoredKeys() throws -> [String] { 144 | guard let data = try keychain.getData(StorableKeys.derivedKeyInfo.rawValue) else { return [] } 145 | let storedData = try JSONDecoder().decode([String: Data].self, from: data) 146 | return storedData.keys.map { $0 } 147 | } 148 | 149 | func getSharedSecret(for myPublicKey: String, and otherPublicKey: String) throws -> SharedSecret? { 150 | let sharedSecrets = try getAllSharedSecrets() 151 | return sharedSecrets 152 | .first { 153 | $0.myTruePublicKey == myPublicKey && $0.publicKey == otherPublicKey 154 | } 155 | } 156 | 157 | func getAllSharedSecrets() throws -> [SharedSecret] { 158 | guard let data = try keychain.getData(StorableKeys.sharedSecrets.rawValue) else { return [] } 159 | let storedData = try JSONDecoder().decode([SharedSecret].self, from: data) 160 | return storedData 161 | } 162 | 163 | private func getExistingDerivedKeyInfo() throws -> [String: Data]? { 164 | guard let data = try keychain.getData(StorableKeys.derivedKeyInfo.rawValue) else { return nil } 165 | return try JSONDecoder().decode([String: Data].self, from: data) 166 | } 167 | 168 | private mutating func setKeyInfoOnKeychain(_ info: [String: Data]) throws { 169 | let keychainData = try JSONEncoder().encode(info) 170 | try keychain.set(keychainData, key: StorableKeys.derivedKeyInfo.rawValue) 171 | } 172 | 173 | private func getExistingSharedSecrets() throws -> [SharedSecret]? { 174 | guard let data = try keychain.getData(StorableKeys.sharedSecrets.rawValue) else { return nil } 175 | return try JSONDecoder().decode([SharedSecret].self, from: data) 176 | } 177 | 178 | private mutating func setSharedSecretsOnKeychain(_ secrets: [SharedSecret]) throws { 179 | let keychainData = try JSONEncoder().encode(secrets) 180 | try keychain.set(keychainData, key: StorableKeys.sharedSecrets.rawValue) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /Sources/DeSoIdentity/Workers/MessageDecryptionWorker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageDecryptionWorker.swift 3 | // 4 | // 5 | // Created by Andy Boyd on 29/06/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol MessageDecryptable { 11 | func decryptThreads(_ encryptedMessageThreads: [EncryptedMessagesThread], for publicKey: String, errorOnFailure: Bool) throws -> [String: [String]] 12 | func decryptThread(_ thread: EncryptedMessagesThread, for publicKey: String, errorOnFailure: Bool) throws -> [String] 13 | } 14 | 15 | class MessageDecryptionWorker: MessageDecryptable { 16 | 17 | private let keyStore: KeyInfoStorable 18 | 19 | init(keyStore: KeyInfoStorable = KeyInfoStorageWorker()) { 20 | self.keyStore = keyStore 21 | } 22 | 23 | func decryptThreads(_ encryptedMessageThreads: [EncryptedMessagesThread], for publicKey: String, errorOnFailure: Bool) throws -> [String: [String]] { 24 | return try encryptedMessageThreads.reduce(into: [:], { res, this in 25 | do { 26 | res[this.publicKey] = try decryptThread(this, for: publicKey, errorOnFailure: errorOnFailure) 27 | } catch { 28 | if errorOnFailure { 29 | throw error 30 | } 31 | } 32 | }) 33 | } 34 | 35 | func decryptThread(_ thread: EncryptedMessagesThread, for publicKey: String, errorOnFailure: Bool) throws -> [String] { 36 | guard let sharedSecret = try? keyStore.getSharedSecret(for: publicKey, and: thread.publicKey) else { 37 | throw IdentityError.missingSharedSecret 38 | } 39 | var decrypted: [String] = [] 40 | do { 41 | decrypted = try decrypt(messages: thread.encryptedMessages, with: sharedSecret) 42 | } catch { 43 | if errorOnFailure { 44 | throw error 45 | } 46 | } 47 | return decrypted 48 | } 49 | 50 | private func decrypt(messages: [String], with secret: SharedSecret) throws -> [String] { 51 | return try messages.compactMap { 52 | return try decryptShared(sharedPx: secret.secret.uInt8Array, 53 | encrypted: $0.uInt8Array).stringValue 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/DeSoIdentity/Workers/SignTransactionWorker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignTransactionWorker.swift 3 | // 4 | // 5 | // Created by Andy Boyd on 29/06/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol TransactionSignable { 11 | func signTransaction(_ transaction: UnsignedTransaction) throws -> String 12 | } 13 | 14 | class SignTransactionWorker: TransactionSignable { 15 | 16 | private let keyStore: KeyInfoStorable 17 | 18 | init(keyStore: KeyInfoStorable = KeyInfoStorageWorker()) { 19 | self.keyStore = keyStore 20 | } 21 | 22 | func signTransaction(_ transaction: UnsignedTransaction) throws -> String { 23 | guard let key = try keyStore.getDerivedKeyInfo(for: transaction.publicKey) else { 24 | throw IdentityError.missingInfoForPublicKey 25 | } 26 | 27 | // TODO: Can we check here if the derived key is still valid, or should we sign it, try to commit, and see if it fails? 28 | return try DeSoIdentity.signTransaction(seedHex: key.derivedSeedHex, transactionHex: transaction.transactionHex) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Tests/DeSoIdentityTests/DeSoIdentityTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DeSoIdentity 3 | 4 | final class DeSoIdentityTests: XCTestCase { 5 | var sut: Identity! 6 | var authWorker: MockAuthWorker! 7 | var keyStore: MockKeyStore! 8 | var transactionSigner: MockTransactionSigner! 9 | var messageDecrypter: MockMessageDecrypter! 10 | var jwtCreator: MockJWTCreator! 11 | var context: MockPresentationContextProvider! 12 | 13 | override func setUp() { 14 | authWorker = MockAuthWorker() 15 | keyStore = MockKeyStore() 16 | transactionSigner = MockTransactionSigner() 17 | messageDecrypter = MockMessageDecrypter() 18 | jwtCreator = MockJWTCreator() 19 | context = MockPresentationContextProvider() 20 | 21 | sut = Identity( 22 | authWorker: authWorker, 23 | keyStore: keyStore, 24 | transactionSigner: transactionSigner, 25 | messageDecrypter: messageDecrypter, 26 | jwtWorker: jwtCreator, 27 | context: context, 28 | network: .testnet, 29 | overrideIdentityURL: nil 30 | ) 31 | } 32 | 33 | func testLoginCallsAuthWorker() { 34 | let expectation = self.expectation(description: "completion") 35 | sut.login() { _ in 36 | expectation.fulfill() 37 | } 38 | waitForExpectations(timeout: 5, handler: nil) 39 | XCTAssertTrue(authWorker.calledPresentAuthSession) 40 | } 41 | 42 | func testLogoutCallsKeyStore() { 43 | let _ = try! sut.logout("foobar") 44 | XCTAssertTrue(keyStore.calledClearDerivedKeyInfo) 45 | XCTAssertEqual(keyStore.publicKeyRequestedForClearInfo, "foobar") 46 | } 47 | 48 | func testGetLoggedInKeysCallsKeyStore() { 49 | let _ = try! sut.getLoggedInKeys() 50 | XCTAssertTrue(keyStore.calledGetAllStoredKeys) 51 | } 52 | 53 | func testRemoveAllKeysCallsKeyStore() { 54 | try! sut.removeAllKeys() 55 | XCTAssertTrue(keyStore.calledClearAllStoredInfo) 56 | } 57 | 58 | func testSignCallsTransactionSigner() { 59 | let transaction = UnsignedTransaction(publicKey: "qwerty", 60 | transactionHex: "123456") 61 | let _ = try! sut.sign(transaction) 62 | XCTAssertTrue(transactionSigner.calledSignTransaction) 63 | XCTAssertEqual(transactionSigner.transactionRequestedForSign, transaction) 64 | } 65 | 66 | func testDecryptThreadsCallsMessageDecrypter() { 67 | let encrypted = [EncryptedMessagesThread(publicKey: "alalalala", 68 | encryptedMessages: ["foo", "foobar", "batbla"])] 69 | let myPublicKey = "ghghghgh" 70 | let _ = try! sut.decrypt(encrypted, for: myPublicKey, errorOnFailure: true) 71 | XCTAssertTrue(messageDecrypter.calledDecryptThreads) 72 | XCTAssertEqual(messageDecrypter.messagesToDecrypt, encrypted) 73 | XCTAssertEqual(messageDecrypter.publicKeyToDecryptFor, myPublicKey) 74 | XCTAssertEqual(messageDecrypter.errorOnFailure, true) 75 | } 76 | 77 | func testDecryptSingleThreadCallsMessageDecrypter() { 78 | let encrypted = EncryptedMessagesThread(publicKey: "alalalala", 79 | encryptedMessages: ["foo", "foobar", "batbla"]) 80 | let myPublicKey = "ghghghgh" 81 | let _ = try! sut.decrypt(encrypted, for: myPublicKey, errorOnFailure: true) 82 | XCTAssertTrue(messageDecrypter.calledDecryptThread) 83 | XCTAssertEqual(messageDecrypter.threadToDecrypt, encrypted) 84 | XCTAssertEqual(messageDecrypter.publicKeyToDecryptFor, myPublicKey) 85 | XCTAssertEqual(messageDecrypter.errorOnFailure, true) 86 | } 87 | 88 | func testJWTCallsJWTWorker() { 89 | let pubKey = "asdfasdf" 90 | let _ = try! sut.jwt(for: pubKey) 91 | XCTAssertTrue(jwtCreator.calledGetJWT) 92 | XCTAssertEqual(jwtCreator.publicKeyToGetJWTFor, pubKey) 93 | 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Tests/DeSoIdentityTests/ECIESTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Test.m 3 | // 4 | // 5 | // Created by Andy Boyd on 08/07/2021. 6 | // 7 | 8 | import XCTest 9 | import SwiftECC 10 | import BigInt 11 | import CryptoSwift 12 | import ASN1 13 | import Base58 14 | 15 | @testable import DeSoIdentity 16 | 17 | final class ECIESTests: XCTestCase { 18 | 19 | private func getRandomKeypair() -> (private: [UInt8], public: [UInt8])? { 20 | let privateK = BInt(bitWidth: 256) 21 | return (private: privateK.asMagnitudeBytes(), public: getPublicKeyBuffer(from: privateK)) 22 | } 23 | 24 | private func getPublicKeyBuffer(from privateKey: BInt) -> [UInt8] { 25 | let domain = Domain.instance(curve: .EC256k1) 26 | let publicK = domain.multiply(domain.g, privateKey) 27 | return try! domain.encodePoint(publicK) 28 | } 29 | 30 | func testDerive() { 31 | let keysA = getRandomKeypair() 32 | let keysB = getRandomKeypair() 33 | 34 | let derived1 = try! deriveX(privateKeyA: keysA!.private, publicKeyB: keysB!.public) 35 | let derived2 = try! deriveX(privateKeyA: keysB!.private, publicKeyB: keysA!.public) 36 | XCTAssertEqual(derived1, derived2) 37 | } 38 | 39 | func testDeriveNegativeCase() { 40 | let keysA = getRandomKeypair() 41 | let keysB = getRandomKeypair() 42 | let keysC = getRandomKeypair() 43 | 44 | let derived1 = try! deriveX(privateKeyA: keysA!.private, publicKeyB: keysB!.public) 45 | let derived2 = try! deriveX(privateKeyA: keysB!.private, publicKeyB: keysC!.public) 46 | XCTAssertNotEqual(derived1, derived2) 47 | } 48 | 49 | func testVerify() { 50 | let keys = getRandomKeypair() 51 | let msg: [UInt8] = try! randomBytes(count: 32) 52 | 53 | let sig = try! sign(privateKey: keys!.private, msg: msg) 54 | let verified = try! verify(publicKey: keys!.public, msg: msg, sig: sig) 55 | XCTAssertTrue(verified) 56 | } 57 | 58 | func testVerifyNegativeCase() { 59 | let keys1 = getRandomKeypair() 60 | let keys2 = getRandomKeypair() 61 | let msg: [UInt8] = try! randomBytes(count: 32) 62 | 63 | let sig = try! sign(privateKey: keys1!.private, msg: msg) 64 | let verified = try! verify(publicKey: keys2!.public, msg: msg, sig: sig) 65 | XCTAssertFalse(verified) 66 | } 67 | 68 | func testEncryptDecryptLegacy() { 69 | let keys = getRandomKeypair() 70 | let msgText = "Hello, World!" 71 | let msg = msgText.uInt8Array 72 | XCTAssertNotEqual(msg.count, 0) 73 | 74 | let encrypted = try! encrypt(publicKeyTo: keys!.public, msg: msg, legacy: true) 75 | XCTAssertNotEqual(msg, encrypted) 76 | 77 | let decrypted = try! decrypt(privateKey: keys!.private, encrypted: encrypted, legacy: true) 78 | let decryptedText = decrypted.stringValue 79 | XCTAssertEqual(msgText, decryptedText) 80 | } 81 | 82 | func testEncryptDecryptShared() { 83 | let keysA = getRandomKeypair()! 84 | let keysB = getRandomKeypair()! 85 | 86 | let msgText = "Hello, World!" 87 | let msg = msgText.uInt8Array 88 | XCTAssertNotEqual(msg.count, 0) 89 | 90 | let encrypted = try! encryptShared(privateKeySender: keysA.private, publicKeyRecipient: keysB.public, msg: msg) 91 | XCTAssertNotEqual(msg, encrypted) 92 | 93 | let decrypted = try! decryptShared(privateKeyRecipient: keysB.private, publicKeySender: keysA.public, encrypted: encrypted) 94 | let decryptedText = decrypted.stringValue 95 | XCTAssertEqual(msgText, decryptedText) 96 | } 97 | 98 | func testEncryptDecryptWithSharedSecret() { 99 | let keysA = getRandomKeypair()! 100 | let keysB = getRandomKeypair()! 101 | let msgText = "Hello, World!" 102 | let msg = msgText.uInt8Array 103 | XCTAssertNotEqual(msg.count, 0) 104 | 105 | let sharedPx = try! deriveX(privateKeyA: keysA.private, publicKeyB: keysB.public) 106 | XCTAssertNotEqual(sharedPx.count, 0) 107 | 108 | let encrypted = try! encryptShared(sharedPx: sharedPx, msg: msg) 109 | XCTAssertNotEqual(msg, encrypted) 110 | 111 | let decrypted = try! decryptShared(sharedPx: sharedPx, encrypted: encrypted) 112 | let decryptedText = decrypted.stringValue 113 | XCTAssertEqual(msgText, decryptedText) 114 | } 115 | 116 | func testSignTransaction() { 117 | let keypair = getRandomKeypair()! 118 | let input = try! randomBytes(count: .random(in: 200..<1500)) 119 | 120 | let seedHex = keypair.private.toHexString() 121 | let inputHex = input.toHexString() 122 | 123 | let signed = try? signTransaction(seedHex: seedHex, transactionHex: inputHex) 124 | XCTAssertNotNil(signed) 125 | } 126 | 127 | /// The following tests require a private key and other inputs to run successfully. For obvious reasons, those input strings are not committed. 128 | /// Unfortunately, Github CI does not support Xcode 12.5 at present, so XCTExpectFailure will not compile. So, the following tests have been left commented out to turn the CI job green 129 | /// These will run successfully on Xcode 12.5+, but remember to find the appropriate input strings before trying (and please don't commit you're private seedhex!) 130 | 131 | 132 | // func testSignKnown() { 133 | // // seed hex of the signing user 134 | // let seedHex = "" 135 | // // unsigned transaction hex, output of any transaction constructing API call 136 | // let inputHex = "" 137 | // // signed transaction hex, input to the corresponding sumbit-transaction call 138 | // let expected = "" 139 | // XCTExpectFailure("This will fail until values are supplied above. Comment out this line to properly run the test") 140 | // do { 141 | // let signedHash = try signTransaction(seedHex: seedHex, transactionHex: inputHex) 142 | // XCTAssertEqual(signedHash, expected) 143 | // } catch { 144 | // print(error.localizedDescription) 145 | // XCTFail() 146 | // } 147 | // } 148 | 149 | // func testDecryptLegacyKnown() { 150 | // // seed hex for the receiving user 151 | // let seedHex = "" 152 | // // encrypted V1 hex string from get-messages-stateless 153 | // let encrypted = "" 154 | // // the actual message text 155 | // let expected = "" 156 | // XCTExpectFailure("This will fail until values are supplied above. Comment out this line to properly run the test") 157 | // 158 | // do { 159 | // let decrypted = try decrypt(privateKey: [UInt8](hex: seedHex), encrypted: [UInt8](hex: encrypted), legacy: true) 160 | // XCTAssertNotEqual(decrypted.stringValue, expected) 161 | // } catch { 162 | // XCTFail(error.localizedDescription) 163 | // } 164 | // } 165 | 166 | 167 | // func testDecryptSharedKnown() { 168 | // // encrypted V2 hex string from get-messages-stateless 169 | // let encrypted = "" 170 | // // seed hex for receiving user 171 | // let seedHex = "" 172 | // // public key (in Base58 prefixed format) for sending user 173 | // let pKeyOther = "" 174 | // // the actual message text 175 | // let expected = "" 176 | // 177 | // XCTExpectFailure("This will fail until values are supplied above. Comment out this line to properly run the test") 178 | // do { 179 | // let decodedOtherPK = try Base58CheckDecodePrefix(input: pKeyOther, prefixLen: 3).result 180 | // let decrypted = try decryptShared(privateKeyRecipient: [UInt8](hex: seedHex), publicKeySender: [UInt8](decodedOtherPK), encrypted: [UInt8](hex: encrypted)) 181 | // XCTAssertEqual(decrypted.stringValue, expected) 182 | // } catch { 183 | // XCTFail(error.localizedDescription) 184 | // } 185 | // } 186 | 187 | // func testBase58Decode() { 188 | // // public key (in Base58 prefixed format) to check 189 | // let base58PK = "" 190 | // let decodedPK = try? Base58CheckDecodePrefix(input: base58PK, prefixLen: 3) 191 | // XCTAssertNotNil(decodedPK) 192 | // } 193 | } 194 | -------------------------------------------------------------------------------- /Tests/DeSoIdentityTests/KeystoreTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Andy Boyd on 28/09/2021. 6 | // 7 | 8 | import XCTest 9 | 10 | @testable import DeSoIdentity 11 | 12 | final class KeystoreTests: XCTestCase { 13 | 14 | var sut: EphemeralKeyStore! 15 | var readableKeychain: [String: String] { 16 | return sut.keychain as! [String: String] 17 | } 18 | 19 | var testInfo = DerivedKeyInfo(publicKey: "abcdef12345", 20 | derivedPublicKey: "oewbvowiuvweoi", 21 | derivedSeedHex: "eo838hfoiwe9g", 22 | btcDepositAddress: "38r87c43gbsiu", 23 | expirationBlock: 123, 24 | accessSignature: "f498hf2398hfvu47", 25 | network: .testnet, 26 | jwt: "fiweesboigf848", 27 | derivedJwt: "@e983yf0842h98h") 28 | 29 | override func setUp() { 30 | super.setUp() 31 | sut = EphemeralKeyStore() 32 | } 33 | 34 | func testStore() { 35 | try! sut.store(testInfo) 36 | let allStoredInfo = readableKeychain[StorableKeys.derivedKeyInfo.rawValue]! 37 | let allStoredData = Data(base64Encoded: allStoredInfo)! 38 | let storedKeys = try! JSONDecoder().decode([String: Data].self, from: allStoredData) 39 | let storedData = storedKeys[testInfo.publicKey]! 40 | let storedInfo = try! JSONDecoder().decode(DerivedKeyInfo.self, from: storedData) 41 | XCTAssertEqual(storedInfo, testInfo) 42 | } 43 | 44 | func testGet() { 45 | try! sut.store(testInfo) 46 | let storedInfo = try! sut.getDerivedKeyInfo(for: testInfo.publicKey) 47 | XCTAssertEqual(storedInfo, testInfo) 48 | } 49 | 50 | func testRemove() { 51 | try! sut.store(testInfo) 52 | XCTAssertNotNil(readableKeychain[StorableKeys.derivedKeyInfo.rawValue]) 53 | 54 | try! sut.keychain.remove(StorableKeys.derivedKeyInfo.rawValue) 55 | XCTAssertNil(readableKeychain[StorableKeys.derivedKeyInfo.rawValue]) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Tests/DeSoIdentityTests/Mocks/MockAuthWorker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Andy Boyd on 02/07/2021. 6 | // 7 | 8 | import Foundation 9 | @testable import DeSoIdentity 10 | 11 | class MockAuthWorker: Authable { 12 | var calledPresentAuthSession: Bool = false 13 | var contextProvided: PresentationContextProvidable? 14 | var networkRequested: Network? 15 | var overrideUrlSet: String? 16 | func presentAuthSession(context: PresentationContextProvidable, 17 | on network: Network, 18 | overrideUrl: String?, 19 | with completion: @escaping Identity.LoginCompletion) { 20 | calledPresentAuthSession = true 21 | contextProvided = context 22 | networkRequested = network 23 | overrideUrlSet = overrideUrl 24 | completion(.success(selectedPublicKey: "foo", allLoadedPublicKeys: ["bar", "bat"])) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/DeSoIdentityTests/Mocks/MockJWTCreator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Andy Boyd on 02/07/2021. 6 | // 7 | 8 | import Foundation 9 | @testable import DeSoIdentity 10 | 11 | class MockJWTCreator: JWTFetchable { 12 | var mockJWT = "foobarJWT" 13 | var calledGetJWT: Bool = false 14 | var publicKeyToGetJWTFor: String? 15 | func getJWT(for publicKey: String) throws -> String { 16 | calledGetJWT = true 17 | publicKeyToGetJWTFor = publicKey 18 | return mockJWT 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/DeSoIdentityTests/Mocks/MockKeyStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Andy Boyd on 02/07/2021. 6 | // 7 | 8 | import Foundation 9 | @testable import DeSoIdentity 10 | 11 | class MockKeyStore: KeyInfoStorable { 12 | var keychain: DataStorable = [String: String]() 13 | var mockInfo: DerivedKeyInfo? = DerivedKeyInfo(publicKey: "foo", 14 | derivedPublicKey: "bar", 15 | derivedSeedHex: "bat", 16 | btcDepositAddress: "12345", 17 | expirationBlock: 23, 18 | accessSignature: "djadlvjbdsljvb", 19 | network: .testnet, 20 | jwt: "jooooot", 21 | derivedJwt: "tooooooj") 22 | 23 | var mockSharedSecret: SharedSecret? = SharedSecret(secret: "foo", 24 | privateKey: "bar", 25 | publicKey: "bat", 26 | myTruePublicKey: "foobarbat") 27 | 28 | var calledStoreInfo: Bool = false 29 | var infoRequestedToStore: DerivedKeyInfo? 30 | func store(_ info: DerivedKeyInfo) throws { 31 | calledStoreInfo = true 32 | infoRequestedToStore = info 33 | } 34 | 35 | var calledClearDerivedKeyInfo: Bool = false 36 | var publicKeyRequestedForClearInfo: String? 37 | func clearDerivedKeyInfo(for publicKey: String) throws { 38 | calledClearDerivedKeyInfo = true 39 | publicKeyRequestedForClearInfo = publicKey 40 | } 41 | 42 | var calledGetDerivedKeyInfo: Bool = false 43 | var publicKeyRequestedForGetInfo: String? 44 | func getDerivedKeyInfo(for publicKey: String) throws -> DerivedKeyInfo? { 45 | calledGetDerivedKeyInfo = true 46 | publicKeyRequestedForGetInfo = publicKey 47 | return mockInfo 48 | } 49 | 50 | var calledGetAllStoredKeys: Bool = false 51 | func getAllStoredKeys() throws -> [String] { 52 | calledGetAllStoredKeys = true 53 | return ["foo"] 54 | } 55 | 56 | var calledGetSharedSecret: Bool = false 57 | var myPublicKeyToGetSharedSecret: String? 58 | var otherPublicKeyToGetSharedSecret: String? 59 | func getSharedSecret(for myPublicKey: String, and otherPublicKey: String) throws -> SharedSecret? { 60 | calledGetSharedSecret = true 61 | myPublicKeyToGetSharedSecret = myPublicKey 62 | otherPublicKeyToGetSharedSecret = otherPublicKey 63 | return mockSharedSecret 64 | } 65 | 66 | var calledGetAllSharedSecrets: Bool = false 67 | func getAllSharedSecrets() throws -> [SharedSecret] { 68 | calledGetAllSharedSecrets = true 69 | return [mockSharedSecret!] 70 | } 71 | 72 | var calledStoreSharedSecret: Bool = false 73 | var sharedSecretToStore: SharedSecret? 74 | func store(sharedSecret: SharedSecret) throws { 75 | calledStoreInfo = true 76 | sharedSecretToStore = sharedSecret 77 | } 78 | 79 | var calledClearAllStoredInfo: Bool = false 80 | func clearAllStoredInfo() throws { 81 | calledClearAllStoredInfo = true 82 | } 83 | 84 | var calledClearAllSharedSecretsForKey: Bool = false 85 | var privateKeyToDeleteSharedSecretsFor: String? 86 | func clearAllSharedSecrets(for privateKey: String) throws { 87 | calledClearAllSharedSecretsForKey = true 88 | privateKeyToDeleteSharedSecretsFor = privateKey 89 | } 90 | 91 | var calledClearSharedSecret: Bool = false 92 | var privateKeyToClearSingleSharedSecret: String? 93 | var publicKeyToClearSingleSharedSecret: String? 94 | func clearSharedSecret(for privateKey: String, and publicKey: String) throws { 95 | calledClearSharedSecret = true 96 | privateKeyToDeleteSharedSecretsFor = privateKey 97 | publicKeyToClearSingleSharedSecret = publicKey 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Tests/DeSoIdentityTests/Mocks/MockMessageDecrypter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Andy Boyd on 02/07/2021. 6 | // 7 | 8 | import Foundation 9 | @testable import DeSoIdentity 10 | 11 | class MockMessageDecrypter: MessageDecryptable { 12 | var mockDecryptedMessages = ["foo": ["bar"], "bar": ["bat"], "bat": ["foo"]] 13 | var calledDecryptThreads: Bool = false 14 | var messagesToDecrypt: [EncryptedMessagesThread]? 15 | var publicKeyToDecryptFor: String? 16 | var errorOnFailure: Bool? 17 | func decryptThreads(_ encryptedMessageThreads: [EncryptedMessagesThread], for myPublicKey: String, errorOnFailure: Bool) throws -> [String: [String]] { 18 | calledDecryptThreads = true 19 | messagesToDecrypt = encryptedMessageThreads 20 | publicKeyToDecryptFor = myPublicKey 21 | self.errorOnFailure = errorOnFailure 22 | return mockDecryptedMessages 23 | } 24 | 25 | var calledDecryptThread: Bool = false 26 | var threadToDecrypt: EncryptedMessagesThread? 27 | func decryptThread(_ thread: EncryptedMessagesThread, for publicKey: String, errorOnFailure: Bool) throws -> [String] { 28 | calledDecryptThread = true 29 | threadToDecrypt = thread 30 | publicKeyToDecryptFor = publicKey 31 | self.errorOnFailure = errorOnFailure 32 | return mockDecryptedMessages[thread.publicKey] ?? [] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/DeSoIdentityTests/Mocks/MockPresentationContextProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Andy Boyd on 02/07/2021. 6 | // 7 | 8 | import Foundation 9 | import AuthenticationServices 10 | @testable import DeSoIdentity 11 | 12 | class MockPresentationContextProvider: NSObject, PresentationContextProvidable { 13 | #if os(iOS) 14 | var mockPresentationAnchor = UIWindow() 15 | #elseif os(macOS) 16 | var mockPresentationAnchor = NSWindow() 17 | #endif 18 | var calledPresentationAnchor: Bool = false 19 | var sessionForAnchor: ASWebAuthenticationSession? 20 | func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { 21 | calledPresentationAnchor = true 22 | sessionForAnchor = session 23 | return mockPresentationAnchor 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Tests/DeSoIdentityTests/Mocks/MockTransactionSigner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Andy Boyd on 02/07/2021. 6 | // 7 | 8 | import Foundation 9 | @testable import DeSoIdentity 10 | 11 | class MockTransactionSigner: TransactionSignable { 12 | var mockSignedHex: String = "foobarbat" 13 | 14 | var calledSignTransaction: Bool = false 15 | var transactionRequestedForSign: UnsignedTransaction? 16 | func signTransaction(_ transaction: UnsignedTransaction) throws -> String { 17 | calledSignTransaction = true 18 | transactionRequestedForSign = transaction 19 | return mockSignedHex 20 | } 21 | 22 | 23 | } 24 | --------------------------------------------------------------------------------