diff --git a/Example/FearlessUtils.xcodeproj/project.pbxproj b/Example/FearlessUtils.xcodeproj/project.pbxproj index 685ffb6..e8fcdaa 100644 --- a/Example/FearlessUtils.xcodeproj/project.pbxproj +++ b/Example/FearlessUtils.xcodeproj/project.pbxproj @@ -39,6 +39,7 @@ 84DA3B4524C8CE5A00B5E27F /* ScaleUInt8Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DA3B3B24C8CE5900B5E27F /* ScaleUInt8Tests.swift */; }; 84ED7256254A001A006102DF /* SubstrateQREncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84ED7255254A001A006102DF /* SubstrateQREncoderTests.swift */; }; 84ED725C254A0233006102DF /* SubstrateQRDecoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84ED725B254A0233006102DF /* SubstrateQRDecoderTests.swift */; }; + 84F4A847254CAD73000CF0A3 /* KeystoreBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F4A846254CAD73000CF0A3 /* KeystoreBuilderTests.swift */; }; A0FDCFB5B61F738FEE34A5E6 /* Pods_FearlessUtils_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 16D2E77D7010BEBF43DFE08E /* Pods_FearlessUtils_Tests.framework */; }; B2BF1BE136637C8404B79450 /* Pods_FearlessUtils_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9A63E44DBCDCE738F798358 /* Pods_FearlessUtils_Example.framework */; }; /* End PBXBuildFile section */ @@ -93,6 +94,7 @@ 84DA3B4824C8CEEF00B5E27F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = Info.plist; path = Tests/Info.plist; sourceTree = SOURCE_ROOT; }; 84ED7255254A001A006102DF /* SubstrateQREncoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubstrateQREncoderTests.swift; sourceTree = ""; }; 84ED725B254A0233006102DF /* SubstrateQRDecoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubstrateQRDecoderTests.swift; sourceTree = ""; }; + 84F4A846254CAD73000CF0A3 /* KeystoreBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeystoreBuilderTests.swift; sourceTree = ""; }; 93F24F5EEC204E4B0F918C39 /* Pods-FearlessUtils_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FearlessUtils_Example.debug.xcconfig"; path = "Target Support Files/Pods-FearlessUtils_Example/Pods-FearlessUtils_Example.debug.xcconfig"; sourceTree = ""; }; B82F3EE26AAEE44C6E7F9962 /* Pods-FearlessUtils_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FearlessUtils_Tests.release.xcconfig"; path = "Target Support Files/Pods-FearlessUtils_Tests/Pods-FearlessUtils_Tests.release.xcconfig"; sourceTree = ""; }; B9A63E44DBCDCE738F798358 /* Pods_FearlessUtils_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FearlessUtils_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -233,6 +235,7 @@ isa = PBXGroup; children = ( 84CB472324DBE82C00837E11 /* KeystoreExtractorTests.swift */, + 84F4A846254CAD73000CF0A3 /* KeystoreBuilderTests.swift */, ); path = Keystore; sourceTree = ""; @@ -517,6 +520,7 @@ 84ED7256254A001A006102DF /* SubstrateQREncoderTests.swift in Sources */, 842D1E7324CFF4EC00C30A7A /* KeypairDeriviationTests.swift in Sources */, 84CB472424DBE82C00837E11 /* KeystoreExtractorTests.swift in Sources */, + 84F4A847254CAD73000CF0A3 /* KeystoreBuilderTests.swift in Sources */, 842D1E7124CFF31800C30A7A /* KeypairDeriviation.swift in Sources */, 842D1E2524C9AC5D00C30A7A /* ScaleBoolOptionTests.swift in Sources */, 845D80ED24C8D91A00EC2540 /* SeedFactoryTests.swift in Sources */, diff --git a/Example/Podfile.lock b/Example/Podfile.lock index 2e4ab2d..355c5b4 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -44,7 +44,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: BigInt: 74b4d88367b0e819d9f77393549226d36faeb0d8 - FearlessUtils: d4e6e36e45ce2fb48c6981dba2192f1f6c7183eb + FearlessUtils: 979e4d4f7fce0432217588b6d693cb86166545e5 IrohaCrypto: bddf15865c4df189b9c5273ba9c675afae8b7f4c scrypt.c: b42ae06183251329d2b2c620c226fb541a4a3592 TweetNacl: 3abf4d1d2082b0114e7a67410e300892448951e6 diff --git a/FearlessUtils.podspec b/FearlessUtils.podspec index 81afb07..68d135c 100644 --- a/FearlessUtils.podspec +++ b/FearlessUtils.podspec @@ -19,8 +19,7 @@ Pod::Spec.new do |s| s.ios.deployment_target = '11.0' - s.user_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64'} - s.pod_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64' } + s.pod_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64', 'VALID_ARCHS' => 'x86_64 armv7 arm64' } s.source_files = 'FearlessUtils/Classes/**/*' s.dependency 'IrohaCrypto/sr25519', '~> 0.7.0' diff --git a/FearlessUtils/Classes/Common/Data+Random.swift b/FearlessUtils/Classes/Common/Data+Random.swift new file mode 100644 index 0000000..16ea544 --- /dev/null +++ b/FearlessUtils/Classes/Common/Data+Random.swift @@ -0,0 +1,20 @@ +import Foundation + +enum RandomDataError: Error { + case generatorFailed +} + +extension Data { + static func gerateRandomBytes(of length: Int) throws -> Data { + var data = Data(count: length) + let result = data.withUnsafeMutableBytes { + SecRandomCopyBytes(kSecRandomDefault, length, $0.baseAddress!) + } + + guard result == errSecSuccess else { + throw RandomDataError.generatorFailed + } + + return data + } +} diff --git a/FearlessUtils/Classes/Common/StorageKeyFactory.swift b/FearlessUtils/Classes/Common/StorageKeyFactory.swift index 147ccea..10c9321 100644 --- a/FearlessUtils/Classes/Common/StorageKeyFactory.swift +++ b/FearlessUtils/Classes/Common/StorageKeyFactory.swift @@ -1,15 +1,38 @@ import Foundation -public protocol StorageKeyFactoryProtocol { +public protocol StorageKeyFactoryProtocol: class { func createStorageKey(moduleName: String, serviceName: String) throws -> Data - func createStorageKey(moduleName: String, serviceName: String, identifier: Data) throws -> Data + func createStorageKey(moduleName: String, + serviceName: String, + identifier: Data, + hasher: StorageKeyHasher) throws -> Data } public enum StorageKeyFactoryError: Error { case badSerialization } -public struct StorageKeyFactory: StorageKeyFactoryProtocol { +public protocol StorageKeyHasher: class { + func hash(data: Data) throws -> Data +} + +public final class Blake128Concat: StorageKeyHasher { + public init() {} + + public func hash(data: Data) throws -> Data { + try data.blake128Concat() + } +} + +public final class Twox64Concat: StorageKeyHasher { + public init() {} + + public func hash(data: Data) throws -> Data { + data.twox64Concat() + } +} + +public final class StorageKeyFactory: StorageKeyFactoryProtocol { public init() {} public func createStorageKey(moduleName: String, serviceName: String) throws -> Data { @@ -24,10 +47,13 @@ public struct StorageKeyFactory: StorageKeyFactoryProtocol { return moduleKey.xxh128() + serviceKey.xxh128() } - public func createStorageKey(moduleName: String, serviceName: String, identifier: Data) throws -> Data { + public func createStorageKey(moduleName: String, + serviceName: String, + identifier: Data, + hasher: StorageKeyHasher) throws -> Data { let subkey = try createStorageKey(moduleName: moduleName, serviceName: serviceName) - let identifierHash = try identifier.blake128Concat() + let identifierHash: Data = try hasher.hash(data: identifier) return subkey + identifierHash } diff --git a/FearlessUtils/Classes/Keystore/KeystoreBuilder.swift b/FearlessUtils/Classes/Keystore/KeystoreBuilder.swift new file mode 100644 index 0000000..abc7c94 --- /dev/null +++ b/FearlessUtils/Classes/Keystore/KeystoreBuilder.swift @@ -0,0 +1,84 @@ +import Foundation +import IrohaCrypto +import TweetNacl + +public class KeystoreBuilder { + private var name: String? + private var creationDate = Date() + private var genesisHash: String? + + public init() {} +} + +extension KeystoreBuilder: KeystoreBuilding { + public func with(name: String) -> Self { + self.name = name + return self + } + + public func with(creationDate: Date) -> Self { + self.creationDate = creationDate + return self + } + + public func with(genesisHash: String) -> Self { + self.genesisHash = genesisHash + return self + } + + public func build(from data: KeystoreData, password: String?) throws -> KeystoreDefinition { + let scryptParameters = try ScryptParameters() + + let scryptData: Data + + if let password = password { + guard let passwordData = password.data(using: .utf8) else { + throw KeystoreExtractorError.invalidPasswordFormat + } + + scryptData = passwordData + } else { + scryptData = Data() + } + + let encryptionKey = try IRScryptKeyDeriviation() + .deriveKey(from: scryptData, + salt: scryptParameters.salt, + scryptN: UInt(scryptParameters.scryptN), + scryptP: UInt(scryptParameters.scryptP), + scryptR: UInt(scryptParameters.scryptR), + length: UInt(KeystoreConstants.encryptionKeyLength)) + + let nonce = try Data.gerateRandomBytes(of: KeystoreConstants.nonceLength) + + let secretKeyData: Data + switch data.cryptoType { + case .sr25519: + secretKeyData = try SNPrivateKey(rawData: data.secretKeyData).toEd25519Data() + case .ed25519: + secretKeyData = data.secretKeyData + case .ecdsa: + secretKeyData = data.secretKeyData + } + + let pcksData = KeystoreConstants.pkcs8Header + secretKeyData + + KeystoreConstants.pkcs8Divider + data.publicKeyData + let encrypted = try NaclSecretBox.secretBox(message: pcksData, nonce: nonce, key: encryptionKey) + let encoded = scryptParameters.encode() + nonce + encrypted + + let encodingType = [KeystoreEncodingType.scrypt.rawValue, KeystoreEncodingType.xsalsa.rawValue] + let encodingContent = [KeystoreEncodingContent.pkcs8.rawValue, data.cryptoType.rawValue] + let keystoreEncoding = KeystoreEncoding(content: encodingContent, + type: encodingType, + version: String(KeystoreConstants.version)) + + let meta = KeystoreMeta(name: name, + createdAt: Int64(creationDate.timeIntervalSince1970), + genesisHash: genesisHash) + + return KeystoreDefinition(address: data.address, + encoded: encoded.base64EncodedString(), + encoding: keystoreEncoding, + meta: meta) + } +} diff --git a/FearlessUtils/Classes/Keystore/KeystoreCommon.swift b/FearlessUtils/Classes/Keystore/KeystoreCommon.swift new file mode 100644 index 0000000..2864afb --- /dev/null +++ b/FearlessUtils/Classes/Keystore/KeystoreCommon.swift @@ -0,0 +1,49 @@ +import Foundation + +public protocol KeystoreExtracting { + func extractFromDefinition(_ info: KeystoreDefinition, + password: String?) throws -> KeystoreData +} + +public protocol KeystoreBuilding { + func with(name: String) -> Self + func with(creationDate: Date) -> Self + func with(genesisHash: String) -> Self + + func build(from data: KeystoreData, password: String?) throws -> KeystoreDefinition +} + +public enum KeystoreExtractorError: Error { + case invalidBase64 + case missingScryptSalt + case missingScryptN + case missingScryptP + case missingScryptR + case unsupportedEncoding + case unsupportedContent + case unsupportedCryptoType + case invalidPasswordFormat + case missingPkcs8Header + case missingPkcs8Divider +} + +public enum KeystoreBuilderError: Error { + case invalidPasswordFormat +} + +enum KeystoreEncodingType: String { + case scrypt = "scrypt" + case xsalsa = "xsalsa20-poly1305" +} + +enum KeystoreEncodingContent: String { + case pkcs8 = "pkcs8" +} + +public struct KeystoreConstants { + public static let nonceLength = 24 + public static let encryptionKeyLength = 32 + public static let pkcs8Header = Data(bytes: [48, 83, 2, 1, 1, 48, 5, 6, 3, 43, 101, 112, 4, 34, 4, 32]) + public static let pkcs8Divider = Data(bytes: [161, 35, 3, 33, 0]) + public static let version = 3 +} diff --git a/FearlessUtils/Classes/Keystore/KeystoreData.swift b/FearlessUtils/Classes/Keystore/KeystoreData.swift index 458357c..40dc931 100644 --- a/FearlessUtils/Classes/Keystore/KeystoreData.swift +++ b/FearlessUtils/Classes/Keystore/KeystoreData.swift @@ -1,12 +1,12 @@ import Foundation -public struct KeystoreData { - public let address: String +public struct KeystoreData: Equatable { + public let address: String? public let secretKeyData: Data public let publicKeyData: Data public let cryptoType: CryptoType - public init(address: String, secretKeyData: Data, publicKeyData: Data, cryptoType: CryptoType) { + public init(address: String?, secretKeyData: Data, publicKeyData: Data, cryptoType: CryptoType) { self.address = address self.secretKeyData = secretKeyData self.publicKeyData = publicKeyData diff --git a/FearlessUtils/Classes/Keystore/KeystoreDefinition.swift b/FearlessUtils/Classes/Keystore/KeystoreDefinition.swift index bb20c16..d2b3928 100644 --- a/FearlessUtils/Classes/Keystore/KeystoreDefinition.swift +++ b/FearlessUtils/Classes/Keystore/KeystoreDefinition.swift @@ -1,15 +1,15 @@ import Foundation public struct KeystoreDefinition: Codable { - public let address: String + public let address: String? public let encoded: String public let encoding: KeystoreEncoding - public let meta: KeystoreMeta + public let meta: KeystoreMeta? - public init(address: String, + public init(address: String?, encoded: String, encoding: KeystoreEncoding, - meta: KeystoreMeta) { + meta: KeystoreMeta?) { self.address = address self.encoded = encoded self.encoding = encoding @@ -32,9 +32,11 @@ public struct KeystoreEncoding: Codable { public struct KeystoreMeta: Codable { enum CodingKeys: String, CodingKey { case name - case created = "whenCreated" + case createdAt = "whenCreated" + case genesisHash } - public let name: String - public let created: Int64 + public let name: String? + public let createdAt: Int64? + public let genesisHash: String? } diff --git a/FearlessUtils/Classes/Keystore/KeystoreExtractor.swift b/FearlessUtils/Classes/Keystore/KeystoreExtractor.swift index 34f3177..fb6a8eb 100644 --- a/FearlessUtils/Classes/Keystore/KeystoreExtractor.swift +++ b/FearlessUtils/Classes/Keystore/KeystoreExtractor.swift @@ -2,39 +2,7 @@ import Foundation import IrohaCrypto import TweetNacl -public protocol KeystoreExtracting { - func extractFromDefinition(_ info: KeystoreDefinition, - password: String?) throws -> KeystoreData -} - -public enum KeystoreExtractorError: Error { - case invalidBase64 - case missingScryptSalt - case missingScryptN - case missingScryptP - case missingScryptR - case unsupportedEncoding - case unsupportedContent - case unsupportedCryptoType - case missingPkcs8Header - case missingPkcs8Divider -} - -private enum KeystoreEncodingType: String { - case scrypt = "scrypt" - case xsalsa = "xsalsa20-poly1305" -} - -private enum KeystoreEncodingContent: String { - case pkcs8 = "pkcs8" -} - -public struct KeystoreExtractor: KeystoreExtracting { - static let nonceLength = 24 - static let encryptionKeyLength = 32 - static let pkcs8Header = Data(bytes: [48, 83, 2, 1, 1, 48, 5, 6, 3, 43, 101, 112, 4, 34, 4, 32]) - static let pkcs8Divider = Data(bytes: [161, 35, 3, 33, 0]) - +public class KeystoreExtractor: KeystoreExtracting { public init() {} public func extractFromDefinition(_ info: KeystoreDefinition, @@ -57,21 +25,26 @@ public struct KeystoreExtractor: KeystoreExtracting { let scryptData: Data - if let passwordData = password?.data(using: .utf8) { + if let password = password { + guard let passwordData = password.data(using: .utf8) else { + throw KeystoreExtractorError.invalidPasswordFormat + } + scryptData = passwordData } else { scryptData = Data() } - let encryptionKey = try IRScryptKeyDeriviation().deriveKey(from: scryptData, - salt: scryptParameters.salt, - scryptN: UInt(scryptParameters.scryptN), - scryptP: UInt(scryptParameters.scryptP), - scryptR: UInt(scryptParameters.scryptR), - length: UInt(Self.encryptionKeyLength)) + let encryptionKey = try IRScryptKeyDeriviation() + .deriveKey(from: scryptData, + salt: scryptParameters.salt, + scryptN: UInt(scryptParameters.scryptN), + scryptP: UInt(scryptParameters.scryptP), + scryptR: UInt(scryptParameters.scryptR), + length: UInt(KeystoreConstants.encryptionKeyLength)) let nonceStart = ScryptParameters.encodedLength - let nonceEnd = ScryptParameters.encodedLength + Self.nonceLength + let nonceEnd = ScryptParameters.encodedLength + KeystoreConstants.nonceLength let nonce = Data(data[nonceStart.. KeystoreData { + let info = try KeystoreInfoFactory().createInfo(from: definition) + let contentType = definition.encoding.content.count > 0 ? definition.encoding.content[0] : nil - let cryptoTypeValue = definition.encoding.content.count > 1 ? definition.encoding.content[1] : nil guard contentType == KeystoreEncodingContent.pkcs8.rawValue else { throw KeystoreExtractorError.unsupportedContent } - guard let value = cryptoTypeValue, let cryptoType = CryptoType(rawValue: value) else { - throw KeystoreExtractorError.unsupportedCryptoType - } - - guard data.starts(with: Self.pkcs8Header) else { + guard data.starts(with: KeystoreConstants.pkcs8Header) else { throw KeystoreExtractorError.missingPkcs8Header } - guard let dividerRange = data.firstRange(of: Self.pkcs8Divider) else { + guard let dividerRange = data.firstRange(of: KeystoreConstants.pkcs8Divider) else { throw KeystoreExtractorError.missingPkcs8Divider } - let secretStart = Self.pkcs8Header.count + let secretStart = KeystoreConstants.pkcs8Header.count let secretEnd = dividerRange.startIndex let importedSecretData = Data(data[secretStart.. KeystoreInfo +} + +public enum KeystoreInfoFactoryError: Error { + case unsupportedCryptoType + case unsupportedAddressType +} + +public final class KeystoreInfoFactory: KeystoreInfoFactoryProtocol { + public init() {} + + public func createInfo(from definition: KeystoreDefinition) throws -> KeystoreInfo { + let cryptoTypeValue = definition.encoding.content.count > 1 ? definition.encoding.content[1] : nil + + guard let value = cryptoTypeValue, let cryptoType = CryptoType(rawValue: value) else { + throw KeystoreInfoFactoryError.unsupportedCryptoType + } + + let addressType: SNAddressType? + + if let address = definition.address, + let addressTypeValue = try? SS58AddressFactory().type(fromAddress: address) { + addressType = SNAddressType(rawValue: addressTypeValue.uint8Value) + } else { + addressType = nil + } + + return KeystoreInfo(address: definition.address, + addressType: addressType, + cryptoType: cryptoType, + meta: definition.meta) + } +} diff --git a/FearlessUtils/Classes/Keystore/ScryptParameters.swift b/FearlessUtils/Classes/Keystore/ScryptParameters.swift index 70d47de..f6d3c90 100644 --- a/FearlessUtils/Classes/Keystore/ScryptParameters.swift +++ b/FearlessUtils/Classes/Keystore/ScryptParameters.swift @@ -2,30 +2,68 @@ import Foundation enum ScryptParametersError: Error { case invalidDataLength + case invalidSalt } struct ScryptParameters { + static let saltLength = 32 static let encodedLength = 44 + static let saltRange = 0..= Self.encodedLength else { throw ScryptParametersError.invalidDataLength } - self.salt = Data(data[0..<32]) + self.salt = Data(data[Self.saltRange]) - let valueN: UInt32 = data[32..<36].withUnsafeBytes { $0.pointee } + let valueN: UInt32 = data[Self.scryptNRange].withUnsafeBytes { $0.pointee } self.scryptN = valueN.littleEndian - let valueP: UInt32 = data[36..<40].withUnsafeBytes { $0.pointee } + let valueP: UInt32 = data[Self.scryptPRange].withUnsafeBytes { $0.pointee } self.scryptP = valueP.littleEndian - let valueR: UInt32 = data[40..<44].withUnsafeBytes { $0.pointee } + let valueR: UInt32 = data[Self.scryptRRange].withUnsafeBytes { $0.pointee } self.scryptR = valueR.littleEndian } + + func encode() -> Data { + var data = Data(repeating: 0, count: Self.encodedLength) + data.replaceSubrange(Self.saltRange, with: salt) + + var scryptN = self.scryptN + data.replaceSubrange(Self.scryptNRange, with: Data(bytes: &scryptN, count: MemoryLayout.size)) + + var scryptP = self.scryptP + data.replaceSubrange(Self.scryptPRange, with: Data(bytes: &scryptP, count: MemoryLayout.size)) + + var scryptR = self.scryptR + data.replaceSubrange(Self.scryptRRange, with: Data(bytes: &scryptR, count: MemoryLayout.size)) + + return data + } } diff --git a/Tests/Common/StorageKeyFactoryTests.swift b/Tests/Common/StorageKeyFactoryTests.swift index ffd09dd..e7d99d6 100644 --- a/Tests/Common/StorageKeyFactoryTests.swift +++ b/Tests/Common/StorageKeyFactoryTests.swift @@ -3,7 +3,7 @@ import FearlessUtils class StorageKeyFactoryTests: XCTestCase { - func testKeyCreation() throws { + func testBlake128ConcatKeyCreation() throws { let factory = StorageKeyFactory() let identifier = try Data(hexString: "8ad2a3fba73321961cd5d1b8272aa95a21e75dd5b098fb36ed996961ac7b2931") @@ -12,9 +12,24 @@ class StorageKeyFactoryTests: XCTestCase { let key = try factory.createStorageKey(moduleName: "System", serviceName: "Account", - identifier: identifier) + identifier: identifier, + hasher: Blake128Concat()) XCTAssertEqual(key, expectedKey) } + func testTwox64ConcatKeyCreation() throws { + let factory = StorageKeyFactory() + + let identifier = try Data(hexString: "8ad2a3fba73321961cd5d1b8272aa95a21e75dd5b098fb36ed996961ac7b2931") + + let expectedKey = try Data(hexString: "0x5f3e4907f716ac89b6347d15ececedca3ed14b45ed20d054f05e37e2542cfe70c18acca30c9341bf8ad2a3fba73321961cd5d1b8272aa95a21e75dd5b098fb36ed996961ac7b2931") + + let key = try factory.createStorageKey(moduleName: "Staking", + serviceName: "Bonded", + identifier: identifier, + hasher: Twox64Concat()) + + XCTAssertEqual(key, expectedKey) + } } diff --git a/Tests/Keystore/KeystoreBuilderTests.swift b/Tests/Keystore/KeystoreBuilderTests.swift new file mode 100644 index 0000000..ad7d253 --- /dev/null +++ b/Tests/Keystore/KeystoreBuilderTests.swift @@ -0,0 +1,81 @@ +import XCTest +import FearlessUtils +import IrohaCrypto + +class KeystoreBuilderTests: XCTestCase { + func testOnSr25519Json() { + guard let url = Bundle(for: KeystoreExtractorTests.self) + .url(forResource: "keystore-sr25519", withExtension: "json") else { + XCTFail("Can't find resource") + return + } + + do { + let testData = try Data(contentsOf: url) + performTestForData(testData, password: "test5") + } catch { + XCTFail("Unexpected error \(error)") + } + } + + func testOnEd25519Json() { + guard let url = Bundle(for: KeystoreExtractorTests.self) + .url(forResource: "keystore-ed25519", withExtension: "json") else { + XCTFail("Can't find resource") + return + } + + do { + let testData = try Data(contentsOf: url) + performTestForData(testData, password: "test2") + } catch { + XCTFail("Unexpected error \(error)") + } + } + + func testOnEcdsaJson() { + guard let url = Bundle(for: KeystoreExtractorTests.self) + .url(forResource: "keystore-ecdsa", withExtension: "json") else { + XCTFail("Can't find resource") + return + } + + do { + let testData = try Data(contentsOf: url) + performTestForData(testData, password: "test3") + } catch { + XCTFail("Unexpected error \(error)") + } + } + + // MARK: Private + + private func performTestForData(_ testData: Data, password: String) { + do { + let definition = try JSONDecoder().decode(KeystoreDefinition.self, from: testData) + + let extractor = KeystoreExtractor() + + let expectedKeystoreData = try extractor.extractFromDefinition(definition, + password: password) + + var builder = KeystoreBuilder() + + if let name = definition.meta?.name { + builder = builder.with(name: name) + } + + if let creationTimestamp = definition.meta?.createdAt { + builder = builder.with(creationDate: Date(timeIntervalSince1970: TimeInterval(creationTimestamp))) + } + + let resultDefinition = try builder.build(from: expectedKeystoreData, password: password) + + let resultKeystoreData = try extractor.extractFromDefinition(resultDefinition, password: password) + + XCTAssertEqual(expectedKeystoreData, resultKeystoreData) + } catch { + XCTFail("Unexpected error \(error)") + } + } +}