├── .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 |
--------------------------------------------------------------------------------