From 6fe6521d3ee34f40496bc2e8259cca749ecbe747 Mon Sep 17 00:00:00 2001 From: Alexander Osokin Date: Mon, 6 Mar 2023 18:50:18 +0500 Subject: [PATCH 01/24] IOS-3059 Generate mnemonic --- TangemSdk/TangemSdk.xcodeproj/project.pbxproj | 56 + .../TangemSdk/Common/Extensions/Byte+.swift | 17 + .../TangemSdk/Common/Extensions/Data+.swift | 4 + .../Common/HDWallet/BIP39/BIP39.swift | 60 + .../Common/HDWallet/BIP39/EntropyLength.swift | 27 + .../Common/HDWallet/BIP39/Mnemonic.swift | 16 + .../Common/HDWallet/BIP39/MnemonicError.swift | 16 + .../Common/HDWallet/BIP39/Wordlist.swift | 40 + .../HDWallet/BIP39/Wordlists/english.txt | 2048 +++++++++++++++++ TangemSdk/TangemSdk/Crypto/CryptoUtils.swift | 2 +- TangemSdk/TangemSdkTests/ByteUtilsTest.swift | 10 + .../TangemSdkTests/Seed/MnemonicTests.swift | 76 + .../Seed/seed_test_vectors.json | 148 ++ 13 files changed, 2519 insertions(+), 1 deletion(-) create mode 100644 TangemSdk/TangemSdk/Common/HDWallet/BIP39/BIP39.swift create mode 100644 TangemSdk/TangemSdk/Common/HDWallet/BIP39/EntropyLength.swift create mode 100644 TangemSdk/TangemSdk/Common/HDWallet/BIP39/Mnemonic.swift create mode 100644 TangemSdk/TangemSdk/Common/HDWallet/BIP39/MnemonicError.swift create mode 100644 TangemSdk/TangemSdk/Common/HDWallet/BIP39/Wordlist.swift create mode 100644 TangemSdk/TangemSdk/Common/HDWallet/BIP39/Wordlists/english.txt create mode 100644 TangemSdk/TangemSdkTests/Seed/MnemonicTests.swift create mode 100644 TangemSdk/TangemSdkTests/Seed/seed_test_vectors.json diff --git a/TangemSdk/TangemSdk.xcodeproj/project.pbxproj b/TangemSdk/TangemSdk.xcodeproj/project.pbxproj index 7fe74640d..0ac3f3c6d 100644 --- a/TangemSdk/TangemSdk.xcodeproj/project.pbxproj +++ b/TangemSdk/TangemSdk.xcodeproj/project.pbxproj @@ -293,6 +293,14 @@ B0EC650E260131220088F03D /* ReadWalletsListCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0EC650D260131220088F03D /* ReadWalletsListCommand.swift */; }; DA216CC0282E4D86003585B9 /* AccessCodeRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA216CBF282E4D86003585B9 /* AccessCodeRepository.swift */; }; DA6C752A292682650070EEFD /* LAContext+.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA6C7529292682650070EEFD /* LAContext+.swift */; }; + DC1244B329B60B6F0037BC05 /* BIP39.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244B229B60B6F0037BC05 /* BIP39.swift */; }; + DC1244B529B60E480037BC05 /* english.txt in Resources */ = {isa = PBXBuildFile; fileRef = DC1244B429B60E480037BC05 /* english.txt */; }; + DC1244B929B610550037BC05 /* MnemonicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244B829B610550037BC05 /* MnemonicTests.swift */; }; + DC1244BD29B61DCB0037BC05 /* seed_test_vectors.json in Resources */ = {isa = PBXBuildFile; fileRef = DC1244BC29B61DCB0037BC05 /* seed_test_vectors.json */; }; + DC59CB0429AF597900EC14E1 /* Wordlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC59CB0329AF597900EC14E1 /* Wordlist.swift */; }; + DC59CB0A29AF6F9C00EC14E1 /* EntropyLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC59CB0929AF6F9C00EC14E1 /* EntropyLength.swift */; }; + DC59CB0C29AF706100EC14E1 /* MnemonicError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC59CB0B29AF706100EC14E1 /* MnemonicError.swift */; }; + DC59CB0E29AF70C700EC14E1 /* Mnemonic.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC59CB0D29AF70C700EC14E1 /* Mnemonic.swift */; }; DC8B0E3F286F221D009D64F7 /* BiometricsUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC8B0E3E286F221D009D64F7 /* BiometricsUtil.swift */; }; DCA9706628E35EAD0046E62E /* GenerateOTPCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA9706528E35EAD0046E62E /* GenerateOTPCommand.swift */; }; DCEA3ABC2875AEBA00B0B0DA /* BiometricsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCEA3ABB2875AEBA00B0B0DA /* BiometricsStorage.swift */; }; @@ -605,6 +613,14 @@ DA216CBF282E4D86003585B9 /* AccessCodeRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessCodeRepository.swift; sourceTree = ""; }; DA6C7529292682650070EEFD /* LAContext+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LAContext+.swift"; sourceTree = ""; }; DADB544F298BAFBC00491102 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; + DC1244B229B60B6F0037BC05 /* BIP39.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BIP39.swift; sourceTree = ""; }; + DC1244B429B60E480037BC05 /* english.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = english.txt; sourceTree = ""; }; + DC1244B829B610550037BC05 /* MnemonicTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MnemonicTests.swift; sourceTree = ""; }; + DC1244BC29B61DCB0037BC05 /* seed_test_vectors.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = seed_test_vectors.json; sourceTree = ""; }; + DC59CB0329AF597900EC14E1 /* Wordlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wordlist.swift; sourceTree = ""; }; + DC59CB0929AF6F9C00EC14E1 /* EntropyLength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntropyLength.swift; sourceTree = ""; }; + DC59CB0B29AF706100EC14E1 /* MnemonicError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MnemonicError.swift; sourceTree = ""; }; + DC59CB0D29AF70C700EC14E1 /* Mnemonic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mnemonic.swift; sourceTree = ""; }; DC8B0E3E286F221D009D64F7 /* BiometricsUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiometricsUtil.swift; sourceTree = ""; }; DCA9706528E35EAD0046E62E /* GenerateOTPCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerateOTPCommand.swift; sourceTree = ""; }; DCEA3ABB2875AEBA00B0B0DA /* BiometricsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiometricsStorage.swift; sourceTree = ""; }; @@ -676,6 +692,7 @@ 5D170AEF26B42C88000D4F36 /* HDWallet */ = { isa = PBXGroup; children = ( + DC59CB0129AF582800EC14E1 /* BIP39 */, 5D2B798426BBED1D002A78D4 /* BIP32 */, 5D14091B26B9692500B811A1 /* BIP44.swift */, 5D14091D26B9886800B811A1 /* HDWalletError.swift */, @@ -1183,6 +1200,7 @@ 5DA80CA7231D247A00A50A10 /* TangemSdkTests */ = { isa = PBXGroup; children = ( + DC1244BB29B61DAC0037BC05 /* Seed */, 5D38D0672679040C0052F67C /* Jsons */, 5DA80CA8231D247A00A50A10 /* CryptoUtilsTests.swift */, 5DA80CAA231D247A00A50A10 /* Info.plist */, @@ -1379,6 +1397,36 @@ path = Deserialization; sourceTree = ""; }; + DC1244BB29B61DAC0037BC05 /* Seed */ = { + isa = PBXGroup; + children = ( + DC1244B829B610550037BC05 /* MnemonicTests.swift */, + DC1244BC29B61DCB0037BC05 /* seed_test_vectors.json */, + ); + path = Seed; + sourceTree = ""; + }; + DC59CB0129AF582800EC14E1 /* BIP39 */ = { + isa = PBXGroup; + children = ( + DC59CB0229AF590200EC14E1 /* Wordlists */, + DC59CB0929AF6F9C00EC14E1 /* EntropyLength.swift */, + DC59CB0329AF597900EC14E1 /* Wordlist.swift */, + DC59CB0B29AF706100EC14E1 /* MnemonicError.swift */, + DC59CB0D29AF70C700EC14E1 /* Mnemonic.swift */, + DC1244B229B60B6F0037BC05 /* BIP39.swift */, + ); + path = BIP39; + sourceTree = ""; + }; + DC59CB0229AF590200EC14E1 /* Wordlists */ = { + isa = PBXGroup; + children = ( + DC1244B429B60E480037BC05 /* english.txt */, + ); + path = Wordlists; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -1527,6 +1575,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + DC1244B529B60E480037BC05 /* english.txt in Resources */, 5DD5368C2476B3FB00F5DE88 /* Error.ahap in Resources */, 5D5369D224461F62002886E0 /* module.modulemap in Resources */, 5D6A92D82344E2D700158457 /* Localizable.strings in Resources */, @@ -1538,6 +1587,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + DC1244BD29B61DCB0037BC05 /* seed_test_vectors.json in Resources */, 5D54408A2682260000F7D05B /* SetAccessCode.json in Resources */, 5D46F537274D68010004681F /* DeriveWalletPublicKey.json in Resources */, 5D54408E268226B600F7D05B /* Depersonalize.json in Resources */, @@ -1604,6 +1654,7 @@ 5D0243BE26CACACD00B76F37 /* IndicatorView.swift in Sources */, 5D1D945C2722FD7400FD6DAB /* CreateWalletTask.swift in Sources */, 5DE59F2D23D96FE500312DA4 /* TerminalKeysService.swift in Sources */, + DC59CB0A29AF6F9C00EC14E1 /* EntropyLength.swift in Sources */, DCEA3ABE2875AF0F00B0B0DA /* SecureStorageKey.swift in Sources */, DC8B0E3F286F221D009D64F7 /* BiometricsUtil.swift in Sources */, 5D445B7026E2944300F6F0FE /* AuthorizeMode.swift in Sources */, @@ -1635,6 +1686,7 @@ 5D94475E2652F0D400EA9CD2 /* AnyJSONRPCRunnable.swift in Sources */, 5D38D075267A442B0052F67C /* TrustedCardsRepo.swift in Sources */, 5D6508262673936900A8D45B /* OptionSetCodable.swift in Sources */, + DC59CB0C29AF706100EC14E1 /* MnemonicError.swift in Sources */, 5DE43A6226D5157900ECA36A /* ReadBackupDataCommand.swift in Sources */, B006971825FFABA10040D203 /* InteractionMode.swift in Sources */, 5DDD6C5625D2D14000E48D7B /* TlvLogging.swift in Sources */, @@ -1654,6 +1706,7 @@ 5D73FC2926B8140200DF1BB4 /* DerivationPath.swift in Sources */, B06EBBC12534794100B0FEEA /* ChangeFileSettingsCommand.swift in Sources */, 5DDD6C6C25D30B0D00E48D7B /* SuccessResponse.swift in Sources */, + DC59CB0429AF597900EC14E1 /* Wordlist.swift in Sources */, 5D8666622731687A0095CC82 /* ResetCodesViewModel.swift in Sources */, 5D6831D423DB31AB0095BB1D /* PurgeWalletCommand.swift in Sources */, DA216CC0282E4D86003585B9 /* AccessCodeRepository.swift in Sources */, @@ -1671,6 +1724,7 @@ 5D0A6C5D2428CF3C0094FA83 /* Error+.swift in Sources */, 5D270F2726A020DA00D2EDC1 /* WalletDataDeserializer.swift in Sources */, 5DA5B613233E11A50058C720 /* ResponseApdu.swift in Sources */, + DC59CB0E29AF70C700EC14E1 /* Mnemonic.swift in Sources */, 5D503A40276A4411007B1C1A /* RoundedButtonStyle.swift in Sources */, B0EC650E260131220088F03D /* ReadWalletsListCommand.swift in Sources */, 5DE43A5E26D4F45800ECA36A /* LinkPrimaryCardCommand.swift in Sources */, @@ -1758,6 +1812,7 @@ 5DA3A2EF251CA507009A8E08 /* CheckUserCodesCommand.swift in Sources */, 5D705B5B23DAF2BB002CCD7A /* Config.swift in Sources */, 5D6A92EC2346069700158457 /* TangemSdk.swift in Sources */, + DC1244B329B60B6F0037BC05 /* BIP39.swift in Sources */, 5DFFC49F233B9D69004964E8 /* NFCReader.swift in Sources */, 5DE43A6626D515B100ECA36A /* FinalizePrimaryCardTask.swift in Sources */, 5D26CEE5243C902C00994CC0 /* Secp256k1Utils.swift in Sources */, @@ -1786,6 +1841,7 @@ 5DA7942A236C64D100B33DB5 /* IntUtilsTests.swift in Sources */, 5DA80CA9231D247A00A50A10 /* CryptoUtilsTests.swift in Sources */, 5D713B2D236C3F6400E4F6FC /* StringUtilsTest.swift in Sources */, + DC1244B929B610550037BC05 /* MnemonicTests.swift in Sources */, 5DD127A224F3D1A0009ACA29 /* JsonTests.swift in Sources */, 5DAD449E236B2435006C38F8 /* DataExtensionTests.swift in Sources */, 5D6795B2237AEFB60075A330 /* ApduTests.swift in Sources */, diff --git a/TangemSdk/TangemSdk/Common/Extensions/Byte+.swift b/TangemSdk/TangemSdk/Common/Extensions/Byte+.swift index 4367a5d78..09a8d4a45 100644 --- a/TangemSdk/TangemSdk/Common/Extensions/Byte+.swift +++ b/TangemSdk/TangemSdk/Common/Extensions/Byte+.swift @@ -18,6 +18,23 @@ extension UInt8 { public var hexString: String { return String(format: "%02X", self) } + + func toBits() -> [String] { + let totalBitsCount = 8 + + var bits = [String](repeating: "0", count: totalBitsCount) + + for index in 0.. [String] { + return flatMap { $0.toBits() } + } @available(iOS 13.0, *) func decodeTlv(tag: TlvTag) -> T? { diff --git a/TangemSdk/TangemSdk/Common/HDWallet/BIP39/BIP39.swift b/TangemSdk/TangemSdk/Common/HDWallet/BIP39/BIP39.swift new file mode 100644 index 000000000..f4b5751a5 --- /dev/null +++ b/TangemSdk/TangemSdk/Common/HDWallet/BIP39/BIP39.swift @@ -0,0 +1,60 @@ +// +// BIP39.swift +// TangemSdk +// +// Created by Alexander Osokin on 06.03.2023. +// Copyright © 2023 Tangem AG. All rights reserved. +// + +import Foundation + +@available(iOS 13.0, *) +public struct BIP39 { + /// Generate a mnemonic. + /// - Parameters: + /// - entropyLength: An entropy length to use. Default is 128 bit. + /// - wordlist: A wordlist to use. Default is english. + /// - Returns: Generated mnemonic + public func generateMnemonic(entropyLength: EntropyLength = .bits128, wordlist: Wordlist = .en) throws -> [String] { + guard entropyLength.rawValue % 32 == 0 else { + throw MnemonicError.mnenmonicCreationFailed + } + + let entropyBytesCount = entropyLength.rawValue / 8 + let entropyData = try CryptoUtils.generateRandomBytes(count: entropyBytesCount) + return try generateMnemonic(from: entropyData, wordlist: wordlist) + } + + /// Generate a mnemonic from data. Useful for testing purposes. + /// - Parameters: + /// - data: Entropy data in hex format + /// - wordlist: A wordlist to use. + /// - Returns: Generated mnemonic + func generateMnemonic(from entropyData: Data, wordlist: Wordlist) throws -> [String] { + guard let entropyLength = EntropyLength(rawValue: entropyData.count * 8) else { + throw MnemonicError.invalidEntropyLength + } + + let entropyHashBits = entropyData.getSha256().toBits() + let checksumBitLength = entropyLength.rawValue / 32 + let entropyChecksumBits = entropyHashBits.prefix(checksumBitLength) + + let entropyBits = entropyData.toBits() + let concatenatedBits = entropyBits + entropyChecksumBits + let bitIndexes = concatenatedBits.chunked(into: 11) + let indexes = bitIndexes.compactMap { Int($0.joined(), radix: 2) } + + guard indexes.count == entropyLength.wordsCount else { + throw MnemonicError.mnenmonicCreationFailed + } + + let allWords = wordlist.words + + guard indexes.allSatisfy({ $0 < allWords.count }) else { + throw MnemonicError.mnenmonicCreationFailed + } + + let words = indexes.map { allWords[$0] } + return words + } +} diff --git a/TangemSdk/TangemSdk/Common/HDWallet/BIP39/EntropyLength.swift b/TangemSdk/TangemSdk/Common/HDWallet/BIP39/EntropyLength.swift new file mode 100644 index 000000000..5aaddac97 --- /dev/null +++ b/TangemSdk/TangemSdk/Common/HDWallet/BIP39/EntropyLength.swift @@ -0,0 +1,27 @@ +// +// EntropyLength.swift +// TangemSdk +// +// Created by Alexander Osokin on 01.03.2023. +// Copyright © 2023 Tangem AG. All rights reserved. +// + +import Foundation + +public enum EntropyLength: Int { + case bits128 = 128 + case bits160 = 160 + case bits192 = 192 + case bits224 = 224 + case bits256 = 256 + + var wordsCount: Int { + switch self { + case .bits128: return 12 + case .bits160: return 15 + case .bits192: return 18 + case .bits224: return 21 + case .bits256: return 24 + } + } +} diff --git a/TangemSdk/TangemSdk/Common/HDWallet/BIP39/Mnemonic.swift b/TangemSdk/TangemSdk/Common/HDWallet/BIP39/Mnemonic.swift new file mode 100644 index 000000000..553e4155c --- /dev/null +++ b/TangemSdk/TangemSdk/Common/HDWallet/BIP39/Mnemonic.swift @@ -0,0 +1,16 @@ +// +// Mnemonic.swift +// TangemSdk +// +// Created by Alexander Osokin on 01.03.2023. +// Copyright © 2023 Tangem AG. All rights reserved. +// + +import Foundation + +@available(iOS 13.0, *) +public enum Mnemonic { + public static func generateMnemonic(entropyLength: EntropyLength, wordlist: Wordlist) throws { + + } +} diff --git a/TangemSdk/TangemSdk/Common/HDWallet/BIP39/MnemonicError.swift b/TangemSdk/TangemSdk/Common/HDWallet/BIP39/MnemonicError.swift new file mode 100644 index 000000000..970d2db13 --- /dev/null +++ b/TangemSdk/TangemSdk/Common/HDWallet/BIP39/MnemonicError.swift @@ -0,0 +1,16 @@ +// +// MnemonicError.swift +// TangemSdk +// +// Created by Alexander Osokin on 01.03.2023. +// Copyright © 2023 Tangem AG. All rights reserved. +// + +import Foundation + +public enum MnemonicError: Error { + case invalidEntropyLength + case invalidWordCount + case invalidWordsFile + case mnenmonicCreationFailed +} diff --git a/TangemSdk/TangemSdk/Common/HDWallet/BIP39/Wordlist.swift b/TangemSdk/TangemSdk/Common/HDWallet/BIP39/Wordlist.swift new file mode 100644 index 000000000..c2067a48f --- /dev/null +++ b/TangemSdk/TangemSdk/Common/HDWallet/BIP39/Wordlist.swift @@ -0,0 +1,40 @@ +// +// Wordlist.swift +// TangemSdk +// +// Created by Alexander Osokin on 01.03.2023. +// Copyright © 2023 Tangem AG. All rights reserved. +// + +import Foundation + +@available(iOS 13.0, *) +public enum Wordlist { + case en + + /// This var reads a big array from a file + public var words: [String] { + (try? readWords(from: fileName)) ?? [] + } + + private var fileName: String { + switch self { + case .en: + return "english" + } + } + + private func readWords(from fileName: String) throws -> [String] { + guard let path = Bundle.sdkBundle.path(forResource: fileName, ofType: "txt") else { + throw MnemonicError.invalidWordsFile + } + + let content = try String(contentsOfFile: path, encoding: .utf8) + let words = content.trim().components(separatedBy: "\n") + guard words.count == 2048 else { + throw MnemonicError.invalidWordCount + } + + return words + } +} diff --git a/TangemSdk/TangemSdk/Common/HDWallet/BIP39/Wordlists/english.txt b/TangemSdk/TangemSdk/Common/HDWallet/BIP39/Wordlists/english.txt new file mode 100644 index 000000000..942040ed5 --- /dev/null +++ b/TangemSdk/TangemSdk/Common/HDWallet/BIP39/Wordlists/english.txt @@ -0,0 +1,2048 @@ +abandon +ability +able +about +above +absent +absorb +abstract +absurd +abuse +access +accident +account +accuse +achieve +acid +acoustic +acquire +across +act +action +actor +actress +actual +adapt +add +addict +address +adjust +admit +adult +advance +advice +aerobic +affair +afford +afraid +again +age +agent +agree +ahead +aim +air +airport +aisle +alarm +album +alcohol +alert +alien +all +alley +allow +almost +alone +alpha +already +also +alter +always +amateur +amazing +among +amount +amused +analyst +anchor +ancient +anger +angle +angry +animal +ankle +announce +annual +another +answer +antenna +antique +anxiety +any +apart +apology +appear +apple +approve +april +arch +arctic +area +arena +argue +arm +armed +armor +army +around +arrange +arrest +arrive +arrow +art +artefact +artist +artwork +ask +aspect +assault +asset +assist +assume +asthma +athlete +atom +attack +attend +attitude +attract +auction +audit +august +aunt +author +auto +autumn +average +avocado +avoid +awake +aware +away +awesome +awful +awkward +axis +baby +bachelor +bacon +badge +bag +balance +balcony +ball +bamboo +banana +banner +bar +barely +bargain +barrel +base +basic +basket +battle +beach +bean +beauty +because +become +beef +before +begin +behave +behind +believe +below +belt +bench +benefit +best +betray +better +between +beyond +bicycle +bid +bike +bind +biology +bird +birth +bitter +black +blade +blame +blanket +blast +bleak +bless +blind +blood +blossom +blouse +blue +blur +blush +board +boat +body +boil +bomb +bone +bonus +book +boost +border +boring +borrow +boss +bottom +bounce +box +boy +bracket +brain +brand +brass +brave +bread +breeze +brick +bridge +brief +bright +bring +brisk +broccoli +broken +bronze +broom +brother +brown +brush +bubble +buddy +budget +buffalo +build +bulb +bulk +bullet +bundle +bunker +burden +burger +burst +bus +business +busy +butter +buyer +buzz +cabbage +cabin +cable +cactus +cage +cake +call +calm +camera +camp +can +canal +cancel +candy +cannon +canoe +canvas +canyon +capable +capital +captain +car +carbon +card +cargo +carpet +carry +cart +case +cash +casino +castle +casual +cat +catalog +catch +category +cattle +caught +cause +caution +cave +ceiling +celery +cement +census +century +cereal +certain +chair +chalk +champion +change +chaos +chapter +charge +chase +chat +cheap +check +cheese +chef +cherry +chest +chicken +chief +child +chimney +choice +choose +chronic +chuckle +chunk +churn +cigar +cinnamon +circle +citizen +city +civil +claim +clap +clarify +claw +clay +clean +clerk +clever +click +client +cliff +climb +clinic +clip +clock +clog +close +cloth +cloud +clown +club +clump +cluster +clutch +coach +coast +coconut +code +coffee +coil +coin +collect +color +column +combine +come +comfort +comic +common +company +concert +conduct +confirm +congress +connect +consider +control +convince +cook +cool +copper +copy +coral +core +corn +correct +cost +cotton +couch +country +couple +course +cousin +cover +coyote +crack +cradle +craft +cram +crane +crash +crater +crawl +crazy +cream +credit +creek +crew +cricket +crime +crisp +critic +crop +cross +crouch +crowd +crucial +cruel +cruise +crumble +crunch +crush +cry +crystal +cube +culture +cup +cupboard +curious +current +curtain +curve +cushion +custom +cute +cycle +dad +damage +damp +dance +danger +daring +dash +daughter +dawn +day +deal +debate +debris +decade +december +decide +decline +decorate +decrease +deer +defense +define +defy +degree +delay +deliver +demand +demise +denial +dentist +deny +depart +depend +deposit +depth +deputy +derive +describe +desert +design +desk +despair +destroy +detail +detect +develop +device +devote +diagram +dial +diamond +diary +dice +diesel +diet +differ +digital +dignity +dilemma +dinner +dinosaur +direct +dirt +disagree +discover +disease +dish +dismiss +disorder +display +distance +divert +divide +divorce +dizzy +doctor +document +dog +doll +dolphin +domain +donate +donkey +donor +door +dose +double +dove +draft +dragon +drama +drastic +draw +dream +dress +drift +drill +drink +drip +drive +drop +drum +dry +duck +dumb +dune +during +dust +dutch +duty +dwarf +dynamic +eager +eagle +early +earn +earth +easily +east +easy +echo +ecology +economy +edge +edit +educate +effort +egg +eight +either +elbow +elder +electric +elegant +element +elephant +elevator +elite +else +embark +embody +embrace +emerge +emotion +employ +empower +empty +enable +enact +end +endless +endorse +enemy +energy +enforce +engage +engine +enhance +enjoy +enlist +enough +enrich +enroll +ensure +enter +entire +entry +envelope +episode +equal +equip +era +erase +erode +erosion +error +erupt +escape +essay +essence +estate +eternal +ethics +evidence +evil +evoke +evolve +exact +example +excess +exchange +excite +exclude +excuse +execute +exercise +exhaust +exhibit +exile +exist +exit +exotic +expand +expect +expire +explain +expose +express +extend +extra +eye +eyebrow +fabric +face +faculty +fade +faint +faith +fall +false +fame +family +famous +fan +fancy +fantasy +farm +fashion +fat +fatal +father +fatigue +fault +favorite +feature +february +federal +fee +feed +feel +female +fence +festival +fetch +fever +few +fiber +fiction +field +figure +file +film +filter +final +find +fine +finger +finish +fire +firm +first +fiscal +fish +fit +fitness +fix +flag +flame +flash +flat +flavor +flee +flight +flip +float +flock +floor +flower +fluid +flush +fly +foam +focus +fog +foil +fold +follow +food +foot +force +forest +forget +fork +fortune +forum +forward +fossil +foster +found +fox +fragile +frame +frequent +fresh +friend +fringe +frog +front +frost +frown +frozen +fruit +fuel +fun +funny +furnace +fury +future +gadget +gain +galaxy +gallery +game +gap +garage +garbage +garden +garlic +garment +gas +gasp +gate +gather +gauge +gaze +general +genius +genre +gentle +genuine +gesture +ghost +giant +gift +giggle +ginger +giraffe +girl +give +glad +glance +glare +glass +glide +glimpse +globe +gloom +glory +glove +glow +glue +goat +goddess +gold +good +goose +gorilla +gospel +gossip +govern +gown +grab +grace +grain +grant +grape +grass +gravity +great +green +grid +grief +grit +grocery +group +grow +grunt +guard +guess +guide +guilt +guitar +gun +gym +habit +hair +half +hammer +hamster +hand +happy +harbor +hard +harsh +harvest +hat +have +hawk +hazard +head +health +heart +heavy +hedgehog +height +hello +helmet +help +hen +hero +hidden +high +hill +hint +hip +hire +history +hobby +hockey +hold +hole +holiday +hollow +home +honey +hood +hope +horn +horror +horse +hospital +host +hotel +hour +hover +hub +huge +human +humble +humor +hundred +hungry +hunt +hurdle +hurry +hurt +husband +hybrid +ice +icon +idea +identify +idle +ignore +ill +illegal +illness +image +imitate +immense +immune +impact +impose +improve +impulse +inch +include +income +increase +index +indicate +indoor +industry +infant +inflict +inform +inhale +inherit +initial +inject +injury +inmate +inner +innocent +input +inquiry +insane +insect +inside +inspire +install +intact +interest +into +invest +invite +involve +iron +island +isolate +issue +item +ivory +jacket +jaguar +jar +jazz +jealous +jeans +jelly +jewel +job +join +joke +journey +joy +judge +juice +jump +jungle +junior +junk +just +kangaroo +keen +keep +ketchup +key +kick +kid +kidney +kind +kingdom +kiss +kit +kitchen +kite +kitten +kiwi +knee +knife +knock +know +lab +label +labor +ladder +lady +lake +lamp +language +laptop +large +later +latin +laugh +laundry +lava +law +lawn +lawsuit +layer +lazy +leader +leaf +learn +leave +lecture +left +leg +legal +legend +leisure +lemon +lend +length +lens +leopard +lesson +letter +level +liar +liberty +library +license +life +lift +light +like +limb +limit +link +lion +liquid +list +little +live +lizard +load +loan +lobster +local +lock +logic +lonely +long +loop +lottery +loud +lounge +love +loyal +lucky +luggage +lumber +lunar +lunch +luxury +lyrics +machine +mad +magic +magnet +maid +mail +main +major +make +mammal +man +manage +mandate +mango +mansion +manual +maple +marble +march +margin +marine +market +marriage +mask +mass +master +match +material +math +matrix +matter +maximum +maze +meadow +mean +measure +meat +mechanic +medal +media +melody +melt +member +memory +mention +menu +mercy +merge +merit +merry +mesh +message +metal +method +middle +midnight +milk +million +mimic +mind +minimum +minor +minute +miracle +mirror +misery +miss +mistake +mix +mixed +mixture +mobile +model +modify +mom +moment +monitor +monkey +monster +month +moon +moral +more +morning +mosquito +mother +motion +motor +mountain +mouse +move +movie +much +muffin +mule +multiply +muscle +museum +mushroom +music +must +mutual +myself +mystery +myth +naive +name +napkin +narrow +nasty +nation +nature +near +neck +need +negative +neglect +neither +nephew +nerve +nest +net +network +neutral +never +news +next +nice +night +noble +noise +nominee +noodle +normal +north +nose +notable +note +nothing +notice +novel +now +nuclear +number +nurse +nut +oak +obey +object +oblige +obscure +observe +obtain +obvious +occur +ocean +october +odor +off +offer +office +often +oil +okay +old +olive +olympic +omit +once +one +onion +online +only +open +opera +opinion +oppose +option +orange +orbit +orchard +order +ordinary +organ +orient +original +orphan +ostrich +other +outdoor +outer +output +outside +oval +oven +over +own +owner +oxygen +oyster +ozone +pact +paddle +page +pair +palace +palm +panda +panel +panic +panther +paper +parade +parent +park +parrot +party +pass +patch +path +patient +patrol +pattern +pause +pave +payment +peace +peanut +pear +peasant +pelican +pen +penalty +pencil +people +pepper +perfect +permit +person +pet +phone +photo +phrase +physical +piano +picnic +picture +piece +pig +pigeon +pill +pilot +pink +pioneer +pipe +pistol +pitch +pizza +place +planet +plastic +plate +play +please +pledge +pluck +plug +plunge +poem +poet +point +polar +pole +police +pond +pony +pool +popular +portion +position +possible +post +potato +pottery +poverty +powder +power +practice +praise +predict +prefer +prepare +present +pretty +prevent +price +pride +primary +print +priority +prison +private +prize +problem +process +produce +profit +program +project +promote +proof +property +prosper +protect +proud +provide +public +pudding +pull +pulp +pulse +pumpkin +punch +pupil +puppy +purchase +purity +purpose +purse +push +put +puzzle +pyramid +quality +quantum +quarter +question +quick +quit +quiz +quote +rabbit +raccoon +race +rack +radar +radio +rail +rain +raise +rally +ramp +ranch +random +range +rapid +rare +rate +rather +raven +raw +razor +ready +real +reason +rebel +rebuild +recall +receive +recipe +record +recycle +reduce +reflect +reform +refuse +region +regret +regular +reject +relax +release +relief +rely +remain +remember +remind +remove +render +renew +rent +reopen +repair +repeat +replace +report +require +rescue +resemble +resist +resource +response +result +retire +retreat +return +reunion +reveal +review +reward +rhythm +rib +ribbon +rice +rich +ride +ridge +rifle +right +rigid +ring +riot +ripple +risk +ritual +rival +river +road +roast +robot +robust +rocket +romance +roof +rookie +room +rose +rotate +rough +round +route +royal +rubber +rude +rug +rule +run +runway +rural +sad +saddle +sadness +safe +sail +salad +salmon +salon +salt +salute +same +sample +sand +satisfy +satoshi +sauce +sausage +save +say +scale +scan +scare +scatter +scene +scheme +school +science +scissors +scorpion +scout +scrap +screen +script +scrub +sea +search +season +seat +second +secret +section +security +seed +seek +segment +select +sell +seminar +senior +sense +sentence +series +service +session +settle +setup +seven +shadow +shaft +shallow +share +shed +shell +sheriff +shield +shift +shine +ship +shiver +shock +shoe +shoot +shop +short +shoulder +shove +shrimp +shrug +shuffle +shy +sibling +sick +side +siege +sight +sign +silent +silk +silly +silver +similar +simple +since +sing +siren +sister +situate +six +size +skate +sketch +ski +skill +skin +skirt +skull +slab +slam +sleep +slender +slice +slide +slight +slim +slogan +slot +slow +slush +small +smart +smile +smoke +smooth +snack +snake +snap +sniff +snow +soap +soccer +social +sock +soda +soft +solar +soldier +solid +solution +solve +someone +song +soon +sorry +sort +soul +sound +soup +source +south +space +spare +spatial +spawn +speak +special +speed +spell +spend +sphere +spice +spider +spike +spin +spirit +split +spoil +sponsor +spoon +sport +spot +spray +spread +spring +spy +square +squeeze +squirrel +stable +stadium +staff +stage +stairs +stamp +stand +start +state +stay +steak +steel +stem +step +stereo +stick +still +sting +stock +stomach +stone +stool +story +stove +strategy +street +strike +strong +struggle +student +stuff +stumble +style +subject +submit +subway +success +such +sudden +suffer +sugar +suggest +suit +summer +sun +sunny +sunset +super +supply +supreme +sure +surface +surge +surprise +surround +survey +suspect +sustain +swallow +swamp +swap +swarm +swear +sweet +swift +swim +swing +switch +sword +symbol +symptom +syrup +system +table +tackle +tag +tail +talent +talk +tank +tape +target +task +taste +tattoo +taxi +teach +team +tell +ten +tenant +tennis +tent +term +test +text +thank +that +theme +then +theory +there +they +thing +this +thought +three +thrive +throw +thumb +thunder +ticket +tide +tiger +tilt +timber +time +tiny +tip +tired +tissue +title +toast +tobacco +today +toddler +toe +together +toilet +token +tomato +tomorrow +tone +tongue +tonight +tool +tooth +top +topic +topple +torch +tornado +tortoise +toss +total +tourist +toward +tower +town +toy +track +trade +traffic +tragic +train +transfer +trap +trash +travel +tray +treat +tree +trend +trial +tribe +trick +trigger +trim +trip +trophy +trouble +truck +true +truly +trumpet +trust +truth +try +tube +tuition +tumble +tuna +tunnel +turkey +turn +turtle +twelve +twenty +twice +twin +twist +two +type +typical +ugly +umbrella +unable +unaware +uncle +uncover +under +undo +unfair +unfold +unhappy +uniform +unique +unit +universe +unknown +unlock +until +unusual +unveil +update +upgrade +uphold +upon +upper +upset +urban +urge +usage +use +used +useful +useless +usual +utility +vacant +vacuum +vague +valid +valley +valve +van +vanish +vapor +various +vast +vault +vehicle +velvet +vendor +venture +venue +verb +verify +version +very +vessel +veteran +viable +vibrant +vicious +victory +video +view +village +vintage +violin +virtual +virus +visa +visit +visual +vital +vivid +vocal +voice +void +volcano +volume +vote +voyage +wage +wagon +wait +walk +wall +walnut +want +warfare +warm +warrior +wash +wasp +waste +water +wave +way +wealth +weapon +wear +weasel +weather +web +wedding +weekend +weird +welcome +west +wet +whale +what +wheat +wheel +when +where +whip +whisper +wide +width +wife +wild +will +win +window +wine +wing +wink +winner +winter +wire +wisdom +wise +wish +witness +wolf +woman +wonder +wood +wool +word +work +world +worry +worth +wrap +wreck +wrestle +wrist +write +wrong +yard +year +yellow +you +young +youth +zebra +zero +zone +zoo diff --git a/TangemSdk/TangemSdk/Crypto/CryptoUtils.swift b/TangemSdk/TangemSdk/Crypto/CryptoUtils.swift index 83227be8a..814364029 100644 --- a/TangemSdk/TangemSdk/Crypto/CryptoUtils.swift +++ b/TangemSdk/TangemSdk/Crypto/CryptoUtils.swift @@ -11,7 +11,7 @@ import CommonCrypto import CryptoKit @available(iOS 13.0, *) -public final class CryptoUtils { +public enum CryptoUtils { /** * Generates array of random bytes. diff --git a/TangemSdk/TangemSdkTests/ByteUtilsTest.swift b/TangemSdk/TangemSdkTests/ByteUtilsTest.swift index dcafc2494..91eced4c5 100644 --- a/TangemSdk/TangemSdkTests/ByteUtilsTest.swift +++ b/TangemSdk/TangemSdkTests/ByteUtilsTest.swift @@ -30,4 +30,14 @@ class ByteUtilsTests: XCTestCase { XCTAssertEqual(oneMoreByte!, UInt8(0x04)) inputStream.close() } + + func testParseBits() throws { + let testCases = ["10110111", "00000000", "11111111", "10000000", "00000001"] + + for testcase in testCases { + let byte = try XCTUnwrap(UInt8(testcase, radix: 2)) + let bits = byte.toBits().joined() + XCTAssertEqual(bits, testcase) + } + } } diff --git a/TangemSdk/TangemSdkTests/Seed/MnemonicTests.swift b/TangemSdk/TangemSdkTests/Seed/MnemonicTests.swift new file mode 100644 index 000000000..1337e9ff2 --- /dev/null +++ b/TangemSdk/TangemSdkTests/Seed/MnemonicTests.swift @@ -0,0 +1,76 @@ +// +// MnemonicTests.swift +// TangemSdkTests +// +// Created by Alexander Osokin on 06.03.2023. +// Copyright © 2023 Tangem AG. All rights reserved. +// + +import Foundation +import XCTest +@testable import TangemSdk + +@available(iOS 13.0, *) +class MnemonicTests: XCTestCase { + func testReadWords() { + let langs: [Wordlist] = [.en] + + for lang in langs { + XCTAssertTrue(lang.words.count > 0) + } + } + + func testMnemonicGenerationBase() throws { + let entropyLengthArray: [EntropyLength] = [.bits128, .bits160, .bits192, .bits224, .bits256] + let wordLists: [Wordlist] = [.en] + + let bip39 = BIP39() + + for entropyLength in entropyLengthArray { + for wordlist in wordLists { + let mnemonic = try bip39.generateMnemonic(entropyLength: entropyLength, wordlist: wordlist) + XCTAssertEqual(mnemonic.count, entropyLength.wordsCount) + } + } + } + + func testMnemonicGenerationByEnVectors() throws { + guard let allVectors = try getTestVectors(), + let vectors = allVectors[Constants.englishTestVectors] as? [[String]] else { + XCTFail("Failed to parse test vectors file.") + return + } + + let bip39 = BIP39() + + for vector in vectors { + let entropy = vector[0] + let expectedMnemonic = vector[1] + let mnemonic = (try bip39.generateMnemonic(from: Data(hexString: entropy), wordlist: .en)).joined(separator: " ") + XCTAssertEqual(mnemonic, expectedMnemonic) + } + } + + private func getTestVectors() throws -> [String: Any]? { + guard let url = Bundle(for: MnemonicTests.self).url(forResource: "seed_test_vectors", withExtension: "json") else { + return nil + } + + let data = try Data(contentsOf: url) + let options: JSONSerialization.ReadingOptions = [.allowFragments, .mutableContainers, .mutableLeaves] + + guard let dictionary = + try JSONSerialization.jsonObject(with: data, options: options) as? [String: Any] else { + return nil + } + + return dictionary + } +} + +@available(iOS 13.0, *) +private extension MnemonicTests { + enum Constants { + static let englishTestVectors = "english" + } +} diff --git a/TangemSdk/TangemSdkTests/Seed/seed_test_vectors.json b/TangemSdk/TangemSdkTests/Seed/seed_test_vectors.json new file mode 100644 index 000000000..5c02e37c1 --- /dev/null +++ b/TangemSdk/TangemSdkTests/Seed/seed_test_vectors.json @@ -0,0 +1,148 @@ +{ + "english": [ + [ + "00000000000000000000000000000000", + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + "c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04", + "xprv9s21ZrQH143K3h3fDYiay8mocZ3afhfULfb5GX8kCBdno77K4HiA15Tg23wpbeF1pLfs1c5SPmYHrEpTuuRhxMwvKDwqdKiGJS9XFKzUsAF" + ], + [ + "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f", + "legal winner thank year wave sausage worth useful legal winner thank yellow", + "2e8905819b8723fe2c1d161860e5ee1830318dbf49a83bd451cfb8440c28bd6fa457fe1296106559a3c80937a1c1069be3a3a5bd381ee6260e8d9739fce1f607", + "xprv9s21ZrQH143K2gA81bYFHqU68xz1cX2APaSq5tt6MFSLeXnCKV1RVUJt9FWNTbrrryem4ZckN8k4Ls1H6nwdvDTvnV7zEXs2HgPezuVccsq" + ], + [ + "80808080808080808080808080808080", + "letter advice cage absurd amount doctor acoustic avoid letter advice cage above", + "d71de856f81a8acc65e6fc851a38d4d7ec216fd0796d0a6827a3ad6ed5511a30fa280f12eb2e47ed2ac03b5c462a0358d18d69fe4f985ec81778c1b370b652a8", + "xprv9s21ZrQH143K2shfP28KM3nr5Ap1SXjz8gc2rAqqMEynmjt6o1qboCDpxckqXavCwdnYds6yBHZGKHv7ef2eTXy461PXUjBFQg6PrwY4Gzq" + ], + [ + "ffffffffffffffffffffffffffffffff", + "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong", + "ac27495480225222079d7be181583751e86f571027b0497b5b5d11218e0a8a13332572917f0f8e5a589620c6f15b11c61dee327651a14c34e18231052e48c069", + "xprv9s21ZrQH143K2V4oox4M8Zmhi2Fjx5XK4Lf7GKRvPSgydU3mjZuKGCTg7UPiBUD7ydVPvSLtg9hjp7MQTYsW67rZHAXeccqYqrsx8LcXnyd" + ], + [ + "000000000000000000000000000000000000000000000000", + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent", + "035895f2f481b1b0f01fcf8c289c794660b289981a78f8106447707fdd9666ca06da5a9a565181599b79f53b844d8a71dd9f439c52a3d7b3e8a79c906ac845fa", + "xprv9s21ZrQH143K3mEDrypcZ2usWqFgzKB6jBBx9B6GfC7fu26X6hPRzVjzkqkPvDqp6g5eypdk6cyhGnBngbjeHTe4LsuLG1cCmKJka5SMkmU" + ], + [ + "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f", + "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal will", + "f2b94508732bcbacbcc020faefecfc89feafa6649a5491b8c952cede496c214a0c7b3c392d168748f2d4a612bada0753b52a1c7ac53c1e93abd5c6320b9e95dd", + "xprv9s21ZrQH143K3Lv9MZLj16np5GzLe7tDKQfVusBni7toqJGcnKRtHSxUwbKUyUWiwpK55g1DUSsw76TF1T93VT4gz4wt5RM23pkaQLnvBh7" + ], + [ + "808080808080808080808080808080808080808080808080", + "letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter always", + "107d7c02a5aa6f38c58083ff74f04c607c2d2c0ecc55501dadd72d025b751bc27fe913ffb796f841c49b1d33b610cf0e91d3aa239027f5e99fe4ce9e5088cd65", + "xprv9s21ZrQH143K3VPCbxbUtpkh9pRG371UCLDz3BjceqP1jz7XZsQ5EnNkYAEkfeZp62cDNj13ZTEVG1TEro9sZ9grfRmcYWLBhCocViKEJae" + ], + [ + "ffffffffffffffffffffffffffffffffffffffffffffffff", + "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo when", + "0cd6e5d827bb62eb8fc1e262254223817fd068a74b5b449cc2f667c3f1f985a76379b43348d952e2265b4cd129090758b3e3c2c49103b5051aac2eaeb890a528", + "xprv9s21ZrQH143K36Ao5jHRVhFGDbLP6FCx8BEEmpru77ef3bmA928BxsqvVM27WnvvyfWywiFN8K6yToqMaGYfzS6Db1EHAXT5TuyCLBXUfdm" + ], + [ + "0000000000000000000000000000000000000000000000000000000000000000", + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art", + "bda85446c68413707090a52022edd26a1c9462295029f2e60cd7c4f2bbd3097170af7a4d73245cafa9c3cca8d561a7c3de6f5d4a10be8ed2a5e608d68f92fcc8", + "xprv9s21ZrQH143K32qBagUJAMU2LsHg3ka7jqMcV98Y7gVeVyNStwYS3U7yVVoDZ4btbRNf4h6ibWpY22iRmXq35qgLs79f312g2kj5539ebPM" + ], + [ + "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f", + "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title", + "bc09fca1804f7e69da93c2f2028eb238c227f2e9dda30cd63699232578480a4021b146ad717fbb7e451ce9eb835f43620bf5c514db0f8add49f5d121449d3e87", + "xprv9s21ZrQH143K3Y1sd2XVu9wtqxJRvybCfAetjUrMMco6r3v9qZTBeXiBZkS8JxWbcGJZyio8TrZtm6pkbzG8SYt1sxwNLh3Wx7to5pgiVFU" + ], + [ + "8080808080808080808080808080808080808080808080808080808080808080", + "letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless", + "c0c519bd0e91a2ed54357d9d1ebef6f5af218a153624cf4f2da911a0ed8f7a09e2ef61af0aca007096df430022f7a2b6fb91661a9589097069720d015e4e982f", + "xprv9s21ZrQH143K3CSnQNYC3MqAAqHwxeTLhDbhF43A4ss4ciWNmCY9zQGvAKUSqVUf2vPHBTSE1rB2pg4avopqSiLVzXEU8KziNnVPauTqLRo" + ], + [ + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote", + "dd48c104698c30cfe2b6142103248622fb7bb0ff692eebb00089b32d22484e1613912f0a5b694407be899ffd31ed3992c456cdf60f5d4564b8ba3f05a69890ad", + "xprv9s21ZrQH143K2WFF16X85T2QCpndrGwx6GueB72Zf3AHwHJaknRXNF37ZmDrtHrrLSHvbuRejXcnYxoZKvRquTPyp2JiNG3XcjQyzSEgqCB" + ], + [ + "9e885d952ad362caeb4efe34a8e91bd2", + "ozone drill grab fiber curtain grace pudding thank cruise elder eight picnic", + "274ddc525802f7c828d8ef7ddbcdc5304e87ac3535913611fbbfa986d0c9e5476c91689f9c8a54fd55bd38606aa6a8595ad213d4c9c9f9aca3fb217069a41028", + "xprv9s21ZrQH143K2oZ9stBYpoaZ2ktHj7jLz7iMqpgg1En8kKFTXJHsjxry1JbKH19YrDTicVwKPehFKTbmaxgVEc5TpHdS1aYhB2s9aFJBeJH" + ], + [ + "6610b25967cdcca9d59875f5cb50b0ea75433311869e930b", + "gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow fog", + "628c3827a8823298ee685db84f55caa34b5cc195a778e52d45f59bcf75aba68e4d7590e101dc414bc1bbd5737666fbbef35d1f1903953b66624f910feef245ac", + "xprv9s21ZrQH143K3uT8eQowUjsxrmsA9YUuQQK1RLqFufzybxD6DH6gPY7NjJ5G3EPHjsWDrs9iivSbmvjc9DQJbJGatfa9pv4MZ3wjr8qWPAK" + ], + [ + "68a79eaca2324873eacc50cb9c6eca8cc68ea5d936f98787c60c7ebc74e6ce7c", + "hamster diagram private dutch cause delay private meat slide toddler razor book happy fancy gospel tennis maple dilemma loan word shrug inflict delay length", + "64c87cde7e12ecf6704ab95bb1408bef047c22db4cc7491c4271d170a1b213d20b385bc1588d9c7b38f1b39d415665b8a9030c9ec653d75e65f847d8fc1fc440", + "xprv9s21ZrQH143K2XTAhys3pMNcGn261Fi5Ta2Pw8PwaVPhg3D8DWkzWQwjTJfskj8ofb81i9NP2cUNKxwjueJHHMQAnxtivTA75uUFqPFeWzk" + ], + [ + "c0ba5a8e914111210f2bd131f3d5e08d", + "scheme spot photo card baby mountain device kick cradle pact join borrow", + "ea725895aaae8d4c1cf682c1bfd2d358d52ed9f0f0591131b559e2724bb234fca05aa9c02c57407e04ee9dc3b454aa63fbff483a8b11de949624b9f1831a9612", + "xprv9s21ZrQH143K3FperxDp8vFsFycKCRcJGAFmcV7umQmcnMZaLtZRt13QJDsoS5F6oYT6BB4sS6zmTmyQAEkJKxJ7yByDNtRe5asP2jFGhT6" + ], + [ + "6d9be1ee6ebd27a258115aad99b7317b9c8d28b6d76431c3", + "horn tenant knee talent sponsor spell gate clip pulse soap slush warm silver nephew swap uncle crack brave", + "fd579828af3da1d32544ce4db5c73d53fc8acc4ddb1e3b251a31179cdb71e853c56d2fcb11aed39898ce6c34b10b5382772db8796e52837b54468aeb312cfc3d", + "xprv9s21ZrQH143K3R1SfVZZLtVbXEB9ryVxmVtVMsMwmEyEvgXN6Q84LKkLRmf4ST6QrLeBm3jQsb9gx1uo23TS7vo3vAkZGZz71uuLCcywUkt" + ], + [ + "9f6a2878b2520799a44ef18bc7df394e7061a224d2c33cd015b157d746869863", + "panda eyebrow bullet gorilla call smoke muffin taste mesh discover soft ostrich alcohol speed nation flash devote level hobby quick inner drive ghost inside", + "72be8e052fc4919d2adf28d5306b5474b0069df35b02303de8c1729c9538dbb6fc2d731d5f832193cd9fb6aeecbc469594a70e3dd50811b5067f3b88b28c3e8d", + "xprv9s21ZrQH143K2WNnKmssvZYM96VAr47iHUQUTUyUXH3sAGNjhJANddnhw3i3y3pBbRAVk5M5qUGFr4rHbEWwXgX4qrvrceifCYQJbbFDems" + ], + [ + "23db8160a31d3e0dca3688ed941adbf3", + "cat swing flag economy stadium alone churn speed unique patch report train", + "deb5f45449e615feff5640f2e49f933ff51895de3b4381832b3139941c57b59205a42480c52175b6efcffaa58a2503887c1e8b363a707256bdd2b587b46541f5", + "xprv9s21ZrQH143K4G28omGMogEoYgDQuigBo8AFHAGDaJdqQ99QKMQ5J6fYTMfANTJy6xBmhvsNZ1CJzRZ64PWbnTFUn6CDV2FxoMDLXdk95DQ" + ], + [ + "8197a4a47f0425faeaa69deebc05ca29c0a5b5cc76ceacc0", + "light rule cinnamon wrap drastic word pride squirrel upgrade then income fatal apart sustain crack supply proud access", + "4cbdff1ca2db800fd61cae72a57475fdc6bab03e441fd63f96dabd1f183ef5b782925f00105f318309a7e9c3ea6967c7801e46c8a58082674c860a37b93eda02", + "xprv9s21ZrQH143K3wtsvY8L2aZyxkiWULZH4vyQE5XkHTXkmx8gHo6RUEfH3Jyr6NwkJhvano7Xb2o6UqFKWHVo5scE31SGDCAUsgVhiUuUDyh" + ], + [ + "066dca1a2bb7e8a1db2832148ce9933eea0f3ac9548d793112d9a95c9407efad", + "all hour make first leader extend hole alien behind guard gospel lava path output census museum junior mass reopen famous sing advance salt reform", + "26e975ec644423f4a4c4f4215ef09b4bd7ef924e85d1d17c4cf3f136c2863cf6df0a475045652c57eb5fb41513ca2a2d67722b77e954b4b3fc11f7590449191d", + "xprv9s21ZrQH143K3rEfqSM4QZRVmiMuSWY9wugscmaCjYja3SbUD3KPEB1a7QXJoajyR2T1SiXU7rFVRXMV9XdYVSZe7JoUXdP4SRHTxsT1nzm" + ], + [ + "f30f8c1da665478f49b001d94c5fc452", + "vessel ladder alter error federal sibling chat ability sun glass valve picture", + "2aaa9242daafcee6aa9d7269f17d4efe271e1b9a529178d7dc139cd18747090bf9d60295d0ce74309a78852a9caadf0af48aae1c6253839624076224374bc63f", + "xprv9s21ZrQH143K2QWV9Wn8Vvs6jbqfF1YbTCdURQW9dLFKDovpKaKrqS3SEWsXCu6ZNky9PSAENg6c9AQYHcg4PjopRGGKmdD313ZHszymnps" + ], + [ + "c10ec20dc3cd9f652c7fac2f1230f7a3c828389a14392f05", + "scissors invite lock maple supreme raw rapid void congress muscle digital elegant little brisk hair mango congress clump", + "7b4a10be9d98e6cba265566db7f136718e1398c71cb581e1b2f464cac1ceedf4f3e274dc270003c670ad8d02c4558b2f8e39edea2775c9e232c7cb798b069e88", + "xprv9s21ZrQH143K4aERa2bq7559eMCCEs2QmmqVjUuzfy5eAeDX4mqZffkYwpzGQRE2YEEeLVRoH4CSHxianrFaVnMN2RYaPUZJhJx8S5j6puX" + ], + [ + "f585c11aec520db57dd353c69554b21a89b20fb0650966fa0a9d6f74fd989d8f", + "void come effort suffer camp survey warrior heavy shoot primary clutch crush open amazing screen patrol group space point ten exist slush involve unfold", + "01f5bced59dec48e362f2c45b5de68b9fd6c92c6634f44d6d40aab69056506f0e35524a518034ddc1192e1dacd32c1ed3eaa3c3b131c88ed8e7e54c49a5d0998", + "xprv9s21ZrQH143K39rnQJknpH1WEPFJrzmAqqasiDcVrNuk926oizzJDDQkdiTvNPr2FYDYzWgiMiC63YmfPAa2oPyNB23r2g7d1yiK6WpqaQS" + ] + ] +} From 28b39980fd427e56bb33bc290a124cc0c8785f12 Mon Sep 17 00:00:00 2001 From: Alexander Osokin Date: Mon, 6 Mar 2023 18:55:20 +0500 Subject: [PATCH 02/24] IOS-3059 Some optimizations --- TangemSdk/TangemSdk/Common/HDWallet/BIP39/BIP39.swift | 3 ++- TangemSdk/TangemSdk/Common/HDWallet/BIP39/Wordlist.swift | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/TangemSdk/TangemSdk/Common/HDWallet/BIP39/BIP39.swift b/TangemSdk/TangemSdk/Common/HDWallet/BIP39/BIP39.swift index f4b5751a5..b66cf51bb 100644 --- a/TangemSdk/TangemSdk/Common/HDWallet/BIP39/BIP39.swift +++ b/TangemSdk/TangemSdk/Common/HDWallet/BIP39/BIP39.swift @@ -49,8 +49,9 @@ public struct BIP39 { } let allWords = wordlist.words + let maxWordIndex = allWords.count - guard indexes.allSatisfy({ $0 < allWords.count }) else { + guard indexes.allSatisfy({ $0 < maxWordIndex }) else { throw MnemonicError.mnenmonicCreationFailed } diff --git a/TangemSdk/TangemSdk/Common/HDWallet/BIP39/Wordlist.swift b/TangemSdk/TangemSdk/Common/HDWallet/BIP39/Wordlist.swift index c2067a48f..af312989d 100644 --- a/TangemSdk/TangemSdk/Common/HDWallet/BIP39/Wordlist.swift +++ b/TangemSdk/TangemSdk/Common/HDWallet/BIP39/Wordlist.swift @@ -31,6 +31,7 @@ public enum Wordlist { let content = try String(contentsOfFile: path, encoding: .utf8) let words = content.trim().components(separatedBy: "\n") + guard words.count == 2048 else { throw MnemonicError.invalidWordCount } From 22b4481a19e48f04f4be7a8fca9354d55f43646a Mon Sep 17 00:00:00 2001 From: Alexander Osokin Date: Tue, 7 Mar 2023 18:05:12 +0500 Subject: [PATCH 03/24] IOS-3059 Add seed generation and parsing --- TangemSdk/TangemSdk.xcodeproj/project.pbxproj | 16 +- .../TangemSdk/Common/Extensions/Data+.swift | 34 +++- .../TangemSdk/Common/Extensions/String+.swift | 18 +- .../Common/HDWallet/BIP39/BIP39.swift | 167 ++++++++++++++++-- .../Common/HDWallet/BIP39/EntropyLength.swift | 6 +- .../Common/HDWallet/BIP39/MnemonicError.swift | 5 + .../Common/HDWallet/BIP39/Wordlist.swift | 2 +- .../TangemSdkTests/Seed/MnemonicTests.swift | 75 ++++++-- .../Seed/mnemonic_invalid_test_vectors.json | 13 ++ .../Seed/mnemonic_valid_test_vectors.json | 56 ++++++ .../Seed/seed_test_vectors.json | 1 + 11 files changed, 349 insertions(+), 44 deletions(-) create mode 100644 TangemSdk/TangemSdkTests/Seed/mnemonic_invalid_test_vectors.json create mode 100644 TangemSdk/TangemSdkTests/Seed/mnemonic_valid_test_vectors.json diff --git a/TangemSdk/TangemSdk.xcodeproj/project.pbxproj b/TangemSdk/TangemSdk.xcodeproj/project.pbxproj index 0ac3f3c6d..f2e5cdf78 100644 --- a/TangemSdk/TangemSdk.xcodeproj/project.pbxproj +++ b/TangemSdk/TangemSdk.xcodeproj/project.pbxproj @@ -296,7 +296,9 @@ DC1244B329B60B6F0037BC05 /* BIP39.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244B229B60B6F0037BC05 /* BIP39.swift */; }; DC1244B529B60E480037BC05 /* english.txt in Resources */ = {isa = PBXBuildFile; fileRef = DC1244B429B60E480037BC05 /* english.txt */; }; DC1244B929B610550037BC05 /* MnemonicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244B829B610550037BC05 /* MnemonicTests.swift */; }; - DC1244BD29B61DCB0037BC05 /* seed_test_vectors.json in Resources */ = {isa = PBXBuildFile; fileRef = DC1244BC29B61DCB0037BC05 /* seed_test_vectors.json */; }; + DC1244C129B766920037BC05 /* seed_test_vectors.json in Resources */ = {isa = PBXBuildFile; fileRef = DC1244C029B766920037BC05 /* seed_test_vectors.json */; }; + DC1244C329B766B70037BC05 /* mnemonic_valid_test_vectors.json in Resources */ = {isa = PBXBuildFile; fileRef = DC1244C229B766B70037BC05 /* mnemonic_valid_test_vectors.json */; }; + DC1244C529B769400037BC05 /* mnemonic_invalid_test_vectors.json in Resources */ = {isa = PBXBuildFile; fileRef = DC1244C429B769400037BC05 /* mnemonic_invalid_test_vectors.json */; }; DC59CB0429AF597900EC14E1 /* Wordlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC59CB0329AF597900EC14E1 /* Wordlist.swift */; }; DC59CB0A29AF6F9C00EC14E1 /* EntropyLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC59CB0929AF6F9C00EC14E1 /* EntropyLength.swift */; }; DC59CB0C29AF706100EC14E1 /* MnemonicError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC59CB0B29AF706100EC14E1 /* MnemonicError.swift */; }; @@ -616,7 +618,9 @@ DC1244B229B60B6F0037BC05 /* BIP39.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BIP39.swift; sourceTree = ""; }; DC1244B429B60E480037BC05 /* english.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = english.txt; sourceTree = ""; }; DC1244B829B610550037BC05 /* MnemonicTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MnemonicTests.swift; sourceTree = ""; }; - DC1244BC29B61DCB0037BC05 /* seed_test_vectors.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = seed_test_vectors.json; sourceTree = ""; }; + DC1244C029B766920037BC05 /* seed_test_vectors.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = seed_test_vectors.json; sourceTree = ""; }; + DC1244C229B766B70037BC05 /* mnemonic_valid_test_vectors.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = mnemonic_valid_test_vectors.json; sourceTree = ""; }; + DC1244C429B769400037BC05 /* mnemonic_invalid_test_vectors.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = mnemonic_invalid_test_vectors.json; sourceTree = ""; }; DC59CB0329AF597900EC14E1 /* Wordlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wordlist.swift; sourceTree = ""; }; DC59CB0929AF6F9C00EC14E1 /* EntropyLength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntropyLength.swift; sourceTree = ""; }; DC59CB0B29AF706100EC14E1 /* MnemonicError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MnemonicError.swift; sourceTree = ""; }; @@ -1401,7 +1405,9 @@ isa = PBXGroup; children = ( DC1244B829B610550037BC05 /* MnemonicTests.swift */, - DC1244BC29B61DCB0037BC05 /* seed_test_vectors.json */, + DC1244C029B766920037BC05 /* seed_test_vectors.json */, + DC1244C229B766B70037BC05 /* mnemonic_valid_test_vectors.json */, + DC1244C429B769400037BC05 /* mnemonic_invalid_test_vectors.json */, ); path = Seed; sourceTree = ""; @@ -1587,8 +1593,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - DC1244BD29B61DCB0037BC05 /* seed_test_vectors.json in Resources */, 5D54408A2682260000F7D05B /* SetAccessCode.json in Resources */, + DC1244C529B769400037BC05 /* mnemonic_invalid_test_vectors.json in Resources */, 5D46F537274D68010004681F /* DeriveWalletPublicKey.json in Resources */, 5D54408E268226B600F7D05B /* Depersonalize.json in Resources */, 5D3217462684B1DB000C3AAF /* v3.05ada.json in Resources */, @@ -1601,8 +1607,10 @@ 5D544094268243F700F7D05B /* Card.json in Resources */, 5D5440982682497100F7D05B /* v4.json in Resources */, 5D4B127726D3CEE7006E173C /* ReadFiles.json in Resources */, + DC1244C129B766920037BC05 /* seed_test_vectors.json in Resources */, 5D3B9F0326BD678500532CC7 /* SignHashes.json in Resources */, 5D2BDF8626DD4869002F7E19 /* TestParseRequest.json in Resources */, + DC1244C329B766B70037BC05 /* mnemonic_valid_test_vectors.json in Resources */, 5D5440922682297A00F7D05B /* CreateWallet.json in Resources */, 5D54408C2682269400F7D05B /* SetPasscode.json in Resources */, 5D4B127D26D3D351006E173C /* WriteFiles.json in Resources */, diff --git a/TangemSdk/TangemSdk/Common/Extensions/Data+.swift b/TangemSdk/TangemSdk/Common/Extensions/Data+.swift index 925fb9402..be3ced7ff 100644 --- a/TangemSdk/TangemSdk/Common/Extensions/Data+.swift +++ b/TangemSdk/TangemSdk/Common/Extensions/Data+.swift @@ -82,7 +82,30 @@ extension Data { public init(_ byte: Byte) { self = Data([byte]) } - + + init?(bitsString: String) { + let byteLength = 8 + + guard bitsString.count % byteLength == 0 else { + return nil + } + + let binaryBytes = Array(bitsString).chunked(into: byteLength) + + var bytes = [UInt8]() + bytes.reserveCapacity(bitsString.count / byteLength) + + for binaryByte in binaryBytes { + guard let byte = UInt8(String(binaryByte), radix: 2) else { + return nil + } + + bytes.append(byte) + } + + self = Data(bytes) + } + @available(iOS 13.0, *) public func getSha256() -> Data { let digest = SHA256.hash(data: self) @@ -149,8 +172,13 @@ extension Data { } @available(iOS 13.0, *) - public func pbkdf2sha256(salt: Data, rounds: Int) throws -> Data { - return try pbkdf2(hash: CCPBKDFAlgorithm(kCCPRFHmacAlgSHA256), salt: salt, keyByteCount: 32, rounds: rounds) + public func pbkdf2sha256(salt: Data, rounds: Int, keyByteCount: Int = 32) throws -> Data { + return try pbkdf2(hash: CCPBKDFAlgorithm(kCCPRFHmacAlgSHA256), salt: salt, keyByteCount: keyByteCount, rounds: rounds) + } + + @available(iOS 13.0, *) + public func pbkdf2sha512(salt: Data, rounds: Int, keyByteCount: Int = 64) throws -> Data { + return try pbkdf2(hash: CCPBKDFAlgorithm(kCCPRFHmacAlgSHA512), salt: salt, keyByteCount: keyByteCount, rounds: rounds) } //SO14443A diff --git a/TangemSdk/TangemSdk/Common/Extensions/String+.swift b/TangemSdk/TangemSdk/Common/Extensions/String+.swift index 3aa161d21..35fdf999d 100644 --- a/TangemSdk/TangemSdk/Common/Extensions/String+.swift +++ b/TangemSdk/TangemSdk/Common/Extensions/String+.swift @@ -58,18 +58,12 @@ public extension String { internal func trim() -> String { return trimmingCharacters(in: .whitespacesAndNewlines) } - - internal func camelCaseToSnakeCase() -> String { - let acronymPattern = "([A-Z]+)([A-Z][a-z]|[0-9])" - let normalPattern = "([a-z0-9])([A-Z])" - return self.processCamelCaseRegex(pattern: acronymPattern)? - .processCamelCaseRegex(pattern: normalPattern)?.lowercased() ?? self.lowercased() - } - - private func processCamelCaseRegex(pattern: String) -> String? { - let regex = try? NSRegularExpression(pattern: pattern, options: []) - let range = NSRange(location: 0, length: count) - return regex?.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: "$1_$2") + + internal func zeroPadding(toLength newLength: Int) -> String { + guard count < newLength else { return self } + + let prefix = Array(repeating: "0", count: newLength - count).joined() + return prefix + self } } diff --git a/TangemSdk/TangemSdk/Common/HDWallet/BIP39/BIP39.swift b/TangemSdk/TangemSdk/Common/HDWallet/BIP39/BIP39.swift index b66cf51bb..508b9408c 100644 --- a/TangemSdk/TangemSdk/Common/HDWallet/BIP39/BIP39.swift +++ b/TangemSdk/TangemSdk/Common/HDWallet/BIP39/BIP39.swift @@ -12,9 +12,9 @@ import Foundation public struct BIP39 { /// Generate a mnemonic. /// - Parameters: - /// - entropyLength: An entropy length to use. Default is 128 bit. - /// - wordlist: A wordlist to use. Default is english. - /// - Returns: Generated mnemonic + /// - entropyLength: The entropy length to use. Default is 128 bit. + /// - wordlist: The wordlist to use. Default is english. + /// - Returns: The generated mnemonic splitted to components public func generateMnemonic(entropyLength: EntropyLength = .bits128, wordlist: Wordlist = .en) throws -> [String] { guard entropyLength.rawValue % 32 == 0 else { throw MnemonicError.mnenmonicCreationFailed @@ -25,19 +25,129 @@ public struct BIP39 { return try generateMnemonic(from: entropyData, wordlist: wordlist) } + /// Generate a determenistic seed + /// - Parameters: + /// - mnemonic: The mnemonic to use + /// - passphrase: The passphrase to use. Default is no passphrase (empty). + /// - Returns: The generated seed + public func generateSeed(from mnemonicComponents: [String], passphrase: String = "") throws -> Data { + try validate(mnemonicComponents: mnemonicComponents) + + let mnemonicString = convertToMnemonicString(mnemonicComponents) + let normalizedMnemonic = try normalizedData(from: mnemonicString) + let normalizedSalt = try normalizedData(from: Constants.seedSaltPrefix + passphrase) + let seed = try normalizedMnemonic.pbkdf2sha512(salt: normalizedSalt, rounds: 2048) + return seed + } + + public func convertToMnemonicString(_ mnemonicComponents: [String]) -> String { + return mnemonicComponents.joined(separator: " ") + } + + public func parse(mnemonicString: String) throws -> [String] { + let regex = try NSRegularExpression(pattern: "[a-zA-Z]+") + let range = NSRange(location: 0, length: mnemonicString.count) + let matches = regex.matches(in: mnemonicString, range: range) + let components = matches.compactMap { result -> String? in + guard result.numberOfRanges > 0, + let stringRange = Range(result.range(at: 0), in: mnemonicString) else { + return nil + } + + return String(mnemonicString[stringRange]).trim().lowercased() + } + + try validate(mnemonicComponents: components) + return components + } + + private static func extractCaptureGroupString( + from result: NSTextCheckingResult, + at index: Int, + in text: String + ) -> String? { + guard index < result.numberOfRanges, + let stringRange = Range( + result.range(at: index), + in: text + ) else { + return nil + } + + return String(text[stringRange]) + } + + public func validate(mnemonicComponents: [String]) throws { + // Validate words count + guard !mnemonicComponents.isEmpty else { + throw MnemonicError.wrongWordCount + } + + guard let entropyLength = EntropyLength.allCases.first(where: { $0.wordsCount == mnemonicComponents.count }) else { + throw MnemonicError.wrongWordCount + } + + // Validate wordlist by the first word + let wordlist = try getWordlist(by: mnemonicComponents[0]) + + // Validate all the words + var invalidWords = Set() + + // Generate an indices array inplace + var concatenatedBits = "" + + for word in mnemonicComponents { + guard let wordIndex = wordlist.firstIndex(of: word) else { + invalidWords.insert(word) + continue + } + + let indexBits = String(wordIndex, radix: 2).zeroPadding(toLength: 11) + concatenatedBits.append(contentsOf: indexBits) + } + + guard invalidWords.isEmpty else { + throw MnemonicError.invalidWords(words: Array(invalidWords)) + } + + // Validate checksum + + let checksumBitsCount = mnemonicComponents.count / 3 + guard checksumBitsCount == entropyLength.cheksumBitsCount else { + throw MnemonicError.invalidCheksum + } + + let entropyBitsCount = concatenatedBits.count - checksumBitsCount + let entropyBits = String(concatenatedBits.prefix(entropyBitsCount)) + let checksumBits = String(concatenatedBits.suffix(checksumBitsCount)) + + guard let entropyData = Data(bitsString: entropyBits) else { + throw MnemonicError.invalidCheksum + } + + let calculatedChecksumBits = entropyData + .getSha256() + .toBits() + .prefix(entropyLength.cheksumBitsCount) + .joined() + + guard calculatedChecksumBits == checksumBits else { + throw MnemonicError.invalidCheksum + } + } + /// Generate a mnemonic from data. Useful for testing purposes. /// - Parameters: - /// - data: Entropy data in hex format - /// - wordlist: A wordlist to use. - /// - Returns: Generated mnemonic + /// - data: The entropy data in hex format + /// - wordlist: The wordlist to use. + /// - Returns: The generated mnemonic func generateMnemonic(from entropyData: Data, wordlist: Wordlist) throws -> [String] { guard let entropyLength = EntropyLength(rawValue: entropyData.count * 8) else { throw MnemonicError.invalidEntropyLength } let entropyHashBits = entropyData.getSha256().toBits() - let checksumBitLength = entropyLength.rawValue / 32 - let entropyChecksumBits = entropyHashBits.prefix(checksumBitLength) + let entropyChecksumBits = entropyHashBits.prefix(entropyLength.cheksumBitsCount) let entropyBits = entropyData.toBits() let concatenatedBits = entropyBits + entropyChecksumBits @@ -51,11 +161,46 @@ public struct BIP39 { let allWords = wordlist.words let maxWordIndex = allWords.count - guard indexes.allSatisfy({ $0 < maxWordIndex }) else { - throw MnemonicError.mnenmonicCreationFailed + let words = try indexes.map { index in + guard index < maxWordIndex else { + throw MnemonicError.mnenmonicCreationFailed + } + + return allWords[index] + } - let words = indexes.map { allWords[$0] } return words } + + private func normalizedData(from string: String) throws -> Data { + let normalizedString = string.decomposedStringWithCompatibilityMapping + + guard let data = normalizedString.data(using: .utf8) else { + throw MnemonicError.normalizationFailed + } + + return data + } + + private func getWordlist(by word: String) throws -> [String] { + for list in Wordlist.allCases { + let words = list.words + + if words.contains(word) { + return words + } + } + + throw MnemonicError.unsupportedLanguage + } +} + +// MARK: - Constants + +@available(iOS 13.0, *) +private extension BIP39 { + enum Constants { + static let seedSaltPrefix = "mnemonic" + } } diff --git a/TangemSdk/TangemSdk/Common/HDWallet/BIP39/EntropyLength.swift b/TangemSdk/TangemSdk/Common/HDWallet/BIP39/EntropyLength.swift index 5aaddac97..d69319f80 100644 --- a/TangemSdk/TangemSdk/Common/HDWallet/BIP39/EntropyLength.swift +++ b/TangemSdk/TangemSdk/Common/HDWallet/BIP39/EntropyLength.swift @@ -8,7 +8,7 @@ import Foundation -public enum EntropyLength: Int { +public enum EntropyLength: Int, CaseIterable { case bits128 = 128 case bits160 = 160 case bits192 = 192 @@ -24,4 +24,8 @@ public enum EntropyLength: Int { case .bits256: return 24 } } + + var cheksumBitsCount: Int { + rawValue / 32 + } } diff --git a/TangemSdk/TangemSdk/Common/HDWallet/BIP39/MnemonicError.swift b/TangemSdk/TangemSdk/Common/HDWallet/BIP39/MnemonicError.swift index 970d2db13..b15aea2d8 100644 --- a/TangemSdk/TangemSdk/Common/HDWallet/BIP39/MnemonicError.swift +++ b/TangemSdk/TangemSdk/Common/HDWallet/BIP39/MnemonicError.swift @@ -12,5 +12,10 @@ public enum MnemonicError: Error { case invalidEntropyLength case invalidWordCount case invalidWordsFile + case invalidCheksum case mnenmonicCreationFailed + case normalizationFailed + case wrongWordCount + case unsupportedLanguage + case invalidWords(words: [String]) } diff --git a/TangemSdk/TangemSdk/Common/HDWallet/BIP39/Wordlist.swift b/TangemSdk/TangemSdk/Common/HDWallet/BIP39/Wordlist.swift index af312989d..dc75cf63b 100644 --- a/TangemSdk/TangemSdk/Common/HDWallet/BIP39/Wordlist.swift +++ b/TangemSdk/TangemSdk/Common/HDWallet/BIP39/Wordlist.swift @@ -9,7 +9,7 @@ import Foundation @available(iOS 13.0, *) -public enum Wordlist { +public enum Wordlist: CaseIterable { case en /// This var reads a big array from a file diff --git a/TangemSdk/TangemSdkTests/Seed/MnemonicTests.swift b/TangemSdk/TangemSdkTests/Seed/MnemonicTests.swift index 1337e9ff2..8ce146953 100644 --- a/TangemSdk/TangemSdkTests/Seed/MnemonicTests.swift +++ b/TangemSdk/TangemSdkTests/Seed/MnemonicTests.swift @@ -13,7 +13,7 @@ import XCTest @available(iOS 13.0, *) class MnemonicTests: XCTestCase { func testReadWords() { - let langs: [Wordlist] = [.en] + let langs = Wordlist.allCases for lang in langs { XCTAssertTrue(lang.words.count > 0) @@ -21,8 +21,8 @@ class MnemonicTests: XCTestCase { } func testMnemonicGenerationBase() throws { - let entropyLengthArray: [EntropyLength] = [.bits128, .bits160, .bits192, .bits224, .bits256] - let wordLists: [Wordlist] = [.en] + let entropyLengthArray = EntropyLength.allCases + let wordLists = Wordlist.allCases let bip39 = BIP39() @@ -34,8 +34,8 @@ class MnemonicTests: XCTestCase { } } - func testMnemonicGenerationByEnVectors() throws { - guard let allVectors = try getTestVectors(), + func testMnemonicByEnVectors() throws { + guard let allVectors = try getTestVectors(from: Constants.seedTestVectorsFilename), let vectors = allVectors[Constants.englishTestVectors] as? [[String]] else { XCTFail("Failed to parse test vectors file.") return @@ -46,21 +46,68 @@ class MnemonicTests: XCTestCase { for vector in vectors { let entropy = vector[0] let expectedMnemonic = vector[1] - let mnemonic = (try bip39.generateMnemonic(from: Data(hexString: entropy), wordlist: .en)).joined(separator: " ") - XCTAssertEqual(mnemonic, expectedMnemonic) + let expectedSeed = vector[2] + + let mnemonic = try bip39.generateMnemonic(from: Data(hexString: entropy), wordlist: .en) + let mnemonicString = bip39.convertToMnemonicString(mnemonic) + XCTAssertEqual(mnemonicString, expectedMnemonic) + + let seed = try bip39.generateSeed(from: mnemonic, passphrase: Constants.passphrase) + XCTAssertEqual(seed.hexString.lowercased(), expectedSeed) + } + } + + func testParseMnemonic() throws { + guard let allVectors = try getTestVectors(from: Constants.mnemonicValidTestVectorsFilename), + let vectors = allVectors[Constants.englishTestVectors] as? [[String]] else { + XCTFail("Failed to parse test vectors file.") + return } + + let bip39 = BIP39() + + for vector in vectors { + let mnemonicToParse = vector[0] + let expectedMnemonic = vector[1] + + let parsedMnemonic = try bip39.parse(mnemonicString: mnemonicToParse) + let parsedMnemonicString = bip39.convertToMnemonicString(parsedMnemonic) + XCTAssertEqual(parsedMnemonicString, expectedMnemonic) + } + } + + func testParseInvalidMnemonic() throws { + guard let allVectors = try getTestVectors(from: Constants.mnemonicInvalidTestVectorsFilename), + let vectors = allVectors[Constants.englishTestVectors] as? [[String]], + let firstVector = vectors.first else { + XCTFail("Failed to parse test vectors file.") + return + } + + let bip39 = BIP39() + + for cases in firstVector { + XCTAssertThrowsError(try bip39.parse(mnemonicString: cases)) + } + } + + func testSwapWords() throws { + let bip39 = BIP39() + let valid = "legal winner thank year wave sausage worth useful legal winner thank yellow" + var components = valid.split(separator: " ") + components.swapAt(3, 4) + let invalid = components.joined(separator: " ") + XCTAssertThrowsError(try bip39.parse(mnemonicString: invalid)) } - private func getTestVectors() throws -> [String: Any]? { - guard let url = Bundle(for: MnemonicTests.self).url(forResource: "seed_test_vectors", withExtension: "json") else { + private func getTestVectors(from filename: String) throws -> [String: Any]? { + guard let url = Bundle(for: MnemonicTests.self).url(forResource: filename, withExtension: "json") else { return nil } let data = try Data(contentsOf: url) - let options: JSONSerialization.ReadingOptions = [.allowFragments, .mutableContainers, .mutableLeaves] - guard let dictionary = - try JSONSerialization.jsonObject(with: data, options: options) as? [String: Any] else { + guard let dictionary = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { return nil } @@ -72,5 +119,9 @@ class MnemonicTests: XCTestCase { private extension MnemonicTests { enum Constants { static let englishTestVectors = "english" + static let passphrase = "TREZOR" + static let seedTestVectorsFilename = "seed_test_vectors" + static let mnemonicValidTestVectorsFilename = "mnemonic_valid_test_vectors" + static let mnemonicInvalidTestVectorsFilename = "mnemonic_invalid_test_vectors" } } diff --git a/TangemSdk/TangemSdkTests/Seed/mnemonic_invalid_test_vectors.json b/TangemSdk/TangemSdkTests/Seed/mnemonic_invalid_test_vectors.json new file mode 100644 index 000000000..dc35f3565 --- /dev/null +++ b/TangemSdk/TangemSdkTests/Seed/mnemonic_invalid_test_vectors.json @@ -0,0 +1,13 @@ +{ + "english": [ + [ + "legal winner", + "pen winner pen tangem wave sausage worth useful legal winner thank yellow", + "ручка дом крыша солнце ручка носок карта замок забор дрова булка вещь", + "legal winner thank year wave sausage worth useful legal winner thank yellow yellow", + "legal winner thank year 亂 sausage worth useful legal winner thank yellow", + "", + "pear peasant pelican pen pear peasant pelican pen pear peasant pelican pen pear peasant pelican pen" + ] + ] +} diff --git a/TangemSdk/TangemSdkTests/Seed/mnemonic_valid_test_vectors.json b/TangemSdk/TangemSdkTests/Seed/mnemonic_valid_test_vectors.json new file mode 100644 index 000000000..d66399625 --- /dev/null +++ b/TangemSdk/TangemSdkTests/Seed/mnemonic_valid_test_vectors.json @@ -0,0 +1,56 @@ +{ + "english": [ + [ + "legal winner thank year wave sausage worth useful legal winner thank yellow", + "legal winner thank year wave sausage worth useful legal winner thank yellow", + ], + [ + "Legal winner thank year wave sausage worth useful legal winner thank yellow", + "legal winner thank year wave sausage worth useful legal winner thank yellow", + ], + [ + "Legal winner thank YEAR WaVe sausage worth useful legal winner thank yellow", + "legal winner thank year wave sausage worth useful legal winner thank yellow", + ], + [ + " Legal\t\twinner\t\tthank\t\tYEAR\t\tWaVe\n\nsausage\t\nworth useful\tlegal\twinner thank yellow", + "legal winner thank year wave sausage worth useful legal winner thank yellow", + ], + [ + "1. legal 2. winner 3. thank 4. year 5. wave 6. sausage 7. worth 8. useful 9. legal 10. winner 11. thank 12. yellow", + "legal winner thank year wave sausage worth useful legal winner thank yellow", + ], + [ + "1) legal 2) winner 3) thank 4) year 5) wave 6) sausage 7) worth 8) useful 9) legal 10) winner 11) thank 12) yellow", + "legal winner thank year wave sausage worth useful legal winner thank yellow", + ], + [ + "legal;winner;thank;year;wave;sausage;worth;useful;legal;winner;thank;yellow", + "legal winner thank year wave sausage worth useful legal winner thank yellow", + ], + [ + "legal-winner-thank-year-wave-sausage-worth-useful-legal-winner-thank-yellow", + "legal winner thank year wave sausage worth useful legal winner thank yellow", + ], + [ + "legal,winner,thank,year,wave,sausage,worth,useful,legal,winner,thank,yellow", + "legal winner thank year wave sausage worth useful legal winner thank yellow", + ], + [ + "legal, winner, thank, year, wave, sausage, worth, useful, legal, winner, thank, yellow", + "legal winner thank year wave sausage worth useful legal winner thank yellow", + ], + [ + "legal, winner, thank, year,\nwave, sausage, worth, useful,\nlegal, winner, thank, yellow", + "legal winner thank year wave sausage worth useful legal winner thank yellow", + ], + [ + " legal winner thank year wave sausage worth useful legal winner thank yellow ", + "legal winner thank year wave sausage worth useful legal winner thank yellow", + ], + [ + "legal\nwinner\nthank\nyear\nwave\nsausage\nworth\nuseful\nlegal\nwinner\nthank\nyellow", + "legal winner thank year wave sausage worth useful legal winner thank yellow", + ], + ] +} diff --git a/TangemSdk/TangemSdkTests/Seed/seed_test_vectors.json b/TangemSdk/TangemSdkTests/Seed/seed_test_vectors.json index 5c02e37c1..3b506ea5f 100644 --- a/TangemSdk/TangemSdkTests/Seed/seed_test_vectors.json +++ b/TangemSdk/TangemSdkTests/Seed/seed_test_vectors.json @@ -146,3 +146,4 @@ ] ] } + From 277aba717f082528c694e545dc724638efc66c8f Mon Sep 17 00:00:00 2001 From: Alexander Osokin Date: Tue, 7 Mar 2023 18:09:57 +0500 Subject: [PATCH 04/24] IOS-3059 Move files --- TangemSdk/TangemSdk.xcodeproj/project.pbxproj | 2 +- TangemSdk/TangemSdk/Common/{HDWallet => }/BIP39/BIP39.swift | 0 .../TangemSdk/Common/{HDWallet => }/BIP39/EntropyLength.swift | 0 TangemSdk/TangemSdk/Common/{HDWallet => }/BIP39/Mnemonic.swift | 0 .../TangemSdk/Common/{HDWallet => }/BIP39/MnemonicError.swift | 0 TangemSdk/TangemSdk/Common/{HDWallet => }/BIP39/Wordlist.swift | 0 .../TangemSdk/Common/{HDWallet => }/BIP39/Wordlists/english.txt | 0 7 files changed, 1 insertion(+), 1 deletion(-) rename TangemSdk/TangemSdk/Common/{HDWallet => }/BIP39/BIP39.swift (100%) rename TangemSdk/TangemSdk/Common/{HDWallet => }/BIP39/EntropyLength.swift (100%) rename TangemSdk/TangemSdk/Common/{HDWallet => }/BIP39/Mnemonic.swift (100%) rename TangemSdk/TangemSdk/Common/{HDWallet => }/BIP39/MnemonicError.swift (100%) rename TangemSdk/TangemSdk/Common/{HDWallet => }/BIP39/Wordlist.swift (100%) rename TangemSdk/TangemSdk/Common/{HDWallet => }/BIP39/Wordlists/english.txt (100%) diff --git a/TangemSdk/TangemSdk.xcodeproj/project.pbxproj b/TangemSdk/TangemSdk.xcodeproj/project.pbxproj index f2e5cdf78..564204742 100644 --- a/TangemSdk/TangemSdk.xcodeproj/project.pbxproj +++ b/TangemSdk/TangemSdk.xcodeproj/project.pbxproj @@ -696,7 +696,6 @@ 5D170AEF26B42C88000D4F36 /* HDWallet */ = { isa = PBXGroup; children = ( - DC59CB0129AF582800EC14E1 /* BIP39 */, 5D2B798426BBED1D002A78D4 /* BIP32 */, 5D14091B26B9692500B811A1 /* BIP44.swift */, 5D14091D26B9886800B811A1 /* HDWalletError.swift */, @@ -1253,6 +1252,7 @@ 5DF7CFCA233D034E00461F4D /* Common */ = { isa = PBXGroup; children = ( + DC59CB0129AF582800EC14E1 /* BIP39 */, 5D170AEF26B42C88000D4F36 /* HDWallet */, B0A9436525651E2600A7958E /* Card */, 5D6508242673935700A8D45B /* OptionSet */, diff --git a/TangemSdk/TangemSdk/Common/HDWallet/BIP39/BIP39.swift b/TangemSdk/TangemSdk/Common/BIP39/BIP39.swift similarity index 100% rename from TangemSdk/TangemSdk/Common/HDWallet/BIP39/BIP39.swift rename to TangemSdk/TangemSdk/Common/BIP39/BIP39.swift diff --git a/TangemSdk/TangemSdk/Common/HDWallet/BIP39/EntropyLength.swift b/TangemSdk/TangemSdk/Common/BIP39/EntropyLength.swift similarity index 100% rename from TangemSdk/TangemSdk/Common/HDWallet/BIP39/EntropyLength.swift rename to TangemSdk/TangemSdk/Common/BIP39/EntropyLength.swift diff --git a/TangemSdk/TangemSdk/Common/HDWallet/BIP39/Mnemonic.swift b/TangemSdk/TangemSdk/Common/BIP39/Mnemonic.swift similarity index 100% rename from TangemSdk/TangemSdk/Common/HDWallet/BIP39/Mnemonic.swift rename to TangemSdk/TangemSdk/Common/BIP39/Mnemonic.swift diff --git a/TangemSdk/TangemSdk/Common/HDWallet/BIP39/MnemonicError.swift b/TangemSdk/TangemSdk/Common/BIP39/MnemonicError.swift similarity index 100% rename from TangemSdk/TangemSdk/Common/HDWallet/BIP39/MnemonicError.swift rename to TangemSdk/TangemSdk/Common/BIP39/MnemonicError.swift diff --git a/TangemSdk/TangemSdk/Common/HDWallet/BIP39/Wordlist.swift b/TangemSdk/TangemSdk/Common/BIP39/Wordlist.swift similarity index 100% rename from TangemSdk/TangemSdk/Common/HDWallet/BIP39/Wordlist.swift rename to TangemSdk/TangemSdk/Common/BIP39/Wordlist.swift diff --git a/TangemSdk/TangemSdk/Common/HDWallet/BIP39/Wordlists/english.txt b/TangemSdk/TangemSdk/Common/BIP39/Wordlists/english.txt similarity index 100% rename from TangemSdk/TangemSdk/Common/HDWallet/BIP39/Wordlists/english.txt rename to TangemSdk/TangemSdk/Common/BIP39/Wordlists/english.txt From 644e33e152bdc180a6e860c3f7b927432e981485 Mon Sep 17 00:00:00 2001 From: Alexander Osokin Date: Thu, 9 Mar 2023 21:35:00 +0500 Subject: [PATCH 05/24] IOS-3059 Implement keys generation --- TangemSdk/TangemSdk.xcodeproj/project.pbxproj | 30 ++++++--- TangemSdk/TangemSdk/Common/BIP39/BIP39.swift | 23 ++++--- .../TangemSdk/Common/BIP39/Mnemonic.swift | 17 ++++- .../TangemSdk/Common/Extensions/UInt64+.swift | 20 ++++++ .../Common/HDWallet/BIP32/BIP32.swift | 63 +++++++++++++++++++ .../HDWallet/BIP32/ExtendedPrivateKey.swift | 21 +++++++ .../Common/HDWallet/HDWalletError.swift | 2 + .../TangemSdk/Crypto/Secp256k1Utils.swift | 7 +++ .../TangemSdk/Crypto/Secp256r1Utils.swift | 18 ++++++ TangemSdk/TangemSdkTests/BIP32Tests.swift | 58 +++++++++++++++++ .../MnemonicTests.swift => BIP39Tests.swift} | 8 +-- .../TangemSdkTests/CryptoUtilsTests.swift | 17 +++++ .../BIP39}/mnemonic_invalid_test_vectors.json | 0 .../BIP39}/mnemonic_valid_test_vectors.json | 0 .../BIP39}/seed_test_vectors.json | 0 15 files changed, 262 insertions(+), 22 deletions(-) create mode 100644 TangemSdk/TangemSdk/Common/Extensions/UInt64+.swift create mode 100644 TangemSdk/TangemSdk/Common/HDWallet/BIP32/ExtendedPrivateKey.swift create mode 100644 TangemSdk/TangemSdk/Crypto/Secp256r1Utils.swift create mode 100644 TangemSdk/TangemSdkTests/BIP32Tests.swift rename TangemSdk/TangemSdkTests/{Seed/MnemonicTests.swift => BIP39Tests.swift} (95%) rename TangemSdk/TangemSdkTests/{Seed => Jsons/BIP39}/mnemonic_invalid_test_vectors.json (100%) rename TangemSdk/TangemSdkTests/{Seed => Jsons/BIP39}/mnemonic_valid_test_vectors.json (100%) rename TangemSdk/TangemSdkTests/{Seed => Jsons/BIP39}/seed_test_vectors.json (100%) diff --git a/TangemSdk/TangemSdk.xcodeproj/project.pbxproj b/TangemSdk/TangemSdk.xcodeproj/project.pbxproj index 564204742..1d24ab5bd 100644 --- a/TangemSdk/TangemSdk.xcodeproj/project.pbxproj +++ b/TangemSdk/TangemSdk.xcodeproj/project.pbxproj @@ -295,10 +295,14 @@ DA6C752A292682650070EEFD /* LAContext+.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA6C7529292682650070EEFD /* LAContext+.swift */; }; DC1244B329B60B6F0037BC05 /* BIP39.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244B229B60B6F0037BC05 /* BIP39.swift */; }; DC1244B529B60E480037BC05 /* english.txt in Resources */ = {isa = PBXBuildFile; fileRef = DC1244B429B60E480037BC05 /* english.txt */; }; - DC1244B929B610550037BC05 /* MnemonicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244B829B610550037BC05 /* MnemonicTests.swift */; }; + DC1244B929B610550037BC05 /* BIP39Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244B829B610550037BC05 /* BIP39Tests.swift */; }; DC1244C129B766920037BC05 /* seed_test_vectors.json in Resources */ = {isa = PBXBuildFile; fileRef = DC1244C029B766920037BC05 /* seed_test_vectors.json */; }; DC1244C329B766B70037BC05 /* mnemonic_valid_test_vectors.json in Resources */ = {isa = PBXBuildFile; fileRef = DC1244C229B766B70037BC05 /* mnemonic_valid_test_vectors.json */; }; DC1244C529B769400037BC05 /* mnemonic_invalid_test_vectors.json in Resources */ = {isa = PBXBuildFile; fileRef = DC1244C429B769400037BC05 /* mnemonic_invalid_test_vectors.json */; }; + DC1244C729B776D40037BC05 /* ExtendedPrivateKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244C629B776D40037BC05 /* ExtendedPrivateKey.swift */; }; + DC1244C929B778750037BC05 /* BIP32Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244C829B778750037BC05 /* BIP32Tests.swift */; }; + DC1244CB29B9ECAA0037BC05 /* UInt64+.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244CA29B9ECAA0037BC05 /* UInt64+.swift */; }; + DC1244CD29B9F6D70037BC05 /* Secp256r1Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244CC29B9F6D70037BC05 /* Secp256r1Utils.swift */; }; DC59CB0429AF597900EC14E1 /* Wordlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC59CB0329AF597900EC14E1 /* Wordlist.swift */; }; DC59CB0A29AF6F9C00EC14E1 /* EntropyLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC59CB0929AF6F9C00EC14E1 /* EntropyLength.swift */; }; DC59CB0C29AF706100EC14E1 /* MnemonicError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC59CB0B29AF706100EC14E1 /* MnemonicError.swift */; }; @@ -617,10 +621,14 @@ DADB544F298BAFBC00491102 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; DC1244B229B60B6F0037BC05 /* BIP39.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BIP39.swift; sourceTree = ""; }; DC1244B429B60E480037BC05 /* english.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = english.txt; sourceTree = ""; }; - DC1244B829B610550037BC05 /* MnemonicTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MnemonicTests.swift; sourceTree = ""; }; + DC1244B829B610550037BC05 /* BIP39Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BIP39Tests.swift; sourceTree = ""; }; DC1244C029B766920037BC05 /* seed_test_vectors.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = seed_test_vectors.json; sourceTree = ""; }; DC1244C229B766B70037BC05 /* mnemonic_valid_test_vectors.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = mnemonic_valid_test_vectors.json; sourceTree = ""; }; DC1244C429B769400037BC05 /* mnemonic_invalid_test_vectors.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = mnemonic_invalid_test_vectors.json; sourceTree = ""; }; + DC1244C629B776D40037BC05 /* ExtendedPrivateKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtendedPrivateKey.swift; sourceTree = ""; }; + DC1244C829B778750037BC05 /* BIP32Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BIP32Tests.swift; sourceTree = ""; }; + DC1244CA29B9ECAA0037BC05 /* UInt64+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UInt64+.swift"; sourceTree = ""; }; + DC1244CC29B9F6D70037BC05 /* Secp256r1Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Secp256r1Utils.swift; sourceTree = ""; }; DC59CB0329AF597900EC14E1 /* Wordlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wordlist.swift; sourceTree = ""; }; DC59CB0929AF6F9C00EC14E1 /* EntropyLength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntropyLength.swift; sourceTree = ""; }; DC59CB0B29AF706100EC14E1 /* MnemonicError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MnemonicError.swift; sourceTree = ""; }; @@ -710,6 +718,7 @@ 5D73FC2826B8140200DF1BB4 /* DerivationPath.swift */, 5D73FC2C26B8261400DF1BB4 /* DerivationNode.swift */, 5D14091F26BAABD400B811A1 /* BIP32.swift */, + DC1244C629B776D40037BC05 /* ExtendedPrivateKey.swift */, ); path = BIP32; sourceTree = ""; @@ -792,6 +801,7 @@ 5D38D0672679040C0052F67C /* Jsons */ = { isa = PBXGroup; children = ( + DC1244BB29B61DAC0037BC05 /* BIP39 */, 5D4B127426D3CCE5006E173C /* Files */, 5D3217442684B193000C3AAF /* Personalize */, 5D46F15E26811D4000DC6447 /* SignHash.json */, @@ -1203,7 +1213,6 @@ 5DA80CA7231D247A00A50A10 /* TangemSdkTests */ = { isa = PBXGroup; children = ( - DC1244BB29B61DAC0037BC05 /* Seed */, 5D38D0672679040C0052F67C /* Jsons */, 5DA80CA8231D247A00A50A10 /* CryptoUtilsTests.swift */, 5DA80CAA231D247A00A50A10 /* Info.plist */, @@ -1211,6 +1220,8 @@ 5D713B2C236C3F6400E4F6FC /* StringUtilsTest.swift */, 5D713B2E236C53E300E4F6FC /* ByteUtilsTest.swift */, 5DA79429236C64D100B33DB5 /* IntUtilsTests.swift */, + DC1244B829B610550037BC05 /* BIP39Tests.swift */, + DC1244C829B778750037BC05 /* BIP32Tests.swift */, 5D149479268625A400C0D923 /* CommonTests.swift */, 5D6795B1237AEFB60075A330 /* ApduTests.swift */, 5D437EEA237BE980009C82A8 /* TlvTests.swift */, @@ -1286,6 +1297,7 @@ 5DF5FB1B244F2C15002DB244 /* IssuerDataVerifier.swift */, 5D27C8A727736F190095409C /* Secp256k1Key.swift */, 5D27C8A927737D200095409C /* Secp256k1Signature.swift */, + DC1244CC29B9F6D70037BC05 /* Secp256r1Utils.swift */, ); path = Crypto; sourceTree = ""; @@ -1328,6 +1340,7 @@ 5DDA5DA226E95F2F00199274 /* Result+RAPDU.swift */, 5D04F9E126EF731B00D15F75 /* Publisher+.swift */, DA6C7529292682650070EEFD /* LAContext+.swift */, + DC1244CA29B9ECAA0037BC05 /* UInt64+.swift */, ); path = Extensions; sourceTree = ""; @@ -1401,15 +1414,14 @@ path = Deserialization; sourceTree = ""; }; - DC1244BB29B61DAC0037BC05 /* Seed */ = { + DC1244BB29B61DAC0037BC05 /* BIP39 */ = { isa = PBXGroup; children = ( - DC1244B829B610550037BC05 /* MnemonicTests.swift */, DC1244C029B766920037BC05 /* seed_test_vectors.json */, DC1244C229B766B70037BC05 /* mnemonic_valid_test_vectors.json */, DC1244C429B769400037BC05 /* mnemonic_invalid_test_vectors.json */, ); - path = Seed; + path = BIP39; sourceTree = ""; }; DC59CB0129AF582800EC14E1 /* BIP39 */ = { @@ -1671,7 +1683,9 @@ 5D23C42426CE749F00A1A280 /* FloatingTextField.swift in Sources */, B00BC5C4260B80E600F0647D /* Track.swift in Sources */, 5D14947F2686458100C0D923 /* GenericPasswordConvertible.swift in Sources */, + DC1244CD29B9F6D70037BC05 /* Secp256r1Utils.swift in Sources */, 5D86CBDB24A1102D00FB5BA7 /* Issuer.swift in Sources */, + DC1244C729B776D40037BC05 /* ExtendedPrivateKey.swift in Sources */, 5D944758264EC52400EA9CD2 /* JSONRPC.swift in Sources */, 5DDD6C7925D30C2D00E48D7B /* ConsoleLogger.swift in Sources */, 5D0F8D0026C6A29B002E84A4 /* ActivityIndicatorView.swift in Sources */, @@ -1741,6 +1755,7 @@ 5DEFA99526D7F40300E5CDE4 /* FinalizeBackupCardTask.swift in Sources */, 5D445B7226E295A700F6F0FE /* AuthorizeResetPinToken.swift in Sources */, B0EC6500260110530088F03D /* WalletDeserializer.swift in Sources */, + DC1244CB29B9ECAA0037BC05 /* UInt64+.swift in Sources */, 5D379C2B268FA4D200C7F473 /* CompletionResult.swift in Sources */, 5D86CBDD24A1105A00FB5BA7 /* Manufacturer.swift in Sources */, B0D3C48B26088ECD0038A5C9 /* Array+Tlv.swift in Sources */, @@ -1849,7 +1864,8 @@ 5DA7942A236C64D100B33DB5 /* IntUtilsTests.swift in Sources */, 5DA80CA9231D247A00A50A10 /* CryptoUtilsTests.swift in Sources */, 5D713B2D236C3F6400E4F6FC /* StringUtilsTest.swift in Sources */, - DC1244B929B610550037BC05 /* MnemonicTests.swift in Sources */, + DC1244C929B778750037BC05 /* BIP32Tests.swift in Sources */, + DC1244B929B610550037BC05 /* BIP39Tests.swift in Sources */, 5DD127A224F3D1A0009ACA29 /* JsonTests.swift in Sources */, 5DAD449E236B2435006C38F8 /* DataExtensionTests.swift in Sources */, 5D6795B2237AEFB60075A330 /* ApduTests.swift in Sources */, diff --git a/TangemSdk/TangemSdk/Common/BIP39/BIP39.swift b/TangemSdk/TangemSdk/Common/BIP39/BIP39.swift index 508b9408c..9dfc3ab19 100644 --- a/TangemSdk/TangemSdk/Common/BIP39/BIP39.swift +++ b/TangemSdk/TangemSdk/Common/BIP39/BIP39.swift @@ -9,13 +9,13 @@ import Foundation @available(iOS 13.0, *) -public struct BIP39 { +struct BIP39 { /// Generate a mnemonic. /// - Parameters: /// - entropyLength: The entropy length to use. Default is 128 bit. /// - wordlist: The wordlist to use. Default is english. /// - Returns: The generated mnemonic splitted to components - public func generateMnemonic(entropyLength: EntropyLength = .bits128, wordlist: Wordlist = .en) throws -> [String] { + func generateMnemonic(entropyLength: EntropyLength = .bits128, wordlist: Wordlist = .en) throws -> [String] { guard entropyLength.rawValue % 32 == 0 else { throw MnemonicError.mnenmonicCreationFailed } @@ -30,7 +30,7 @@ public struct BIP39 { /// - mnemonic: The mnemonic to use /// - passphrase: The passphrase to use. Default is no passphrase (empty). /// - Returns: The generated seed - public func generateSeed(from mnemonicComponents: [String], passphrase: String = "") throws -> Data { + func generateSeed(from mnemonicComponents: [String], passphrase: String = "") throws -> Data { try validate(mnemonicComponents: mnemonicComponents) let mnemonicString = convertToMnemonicString(mnemonicComponents) @@ -40,11 +40,11 @@ public struct BIP39 { return seed } - public func convertToMnemonicString(_ mnemonicComponents: [String]) -> String { + func convertToMnemonicString(_ mnemonicComponents: [String]) -> String { return mnemonicComponents.joined(separator: " ") } - public func parse(mnemonicString: String) throws -> [String] { + func parse(mnemonicString: String) throws -> [String] { let regex = try NSRegularExpression(pattern: "[a-zA-Z]+") let range = NSRange(location: 0, length: mnemonicString.count) let matches = regex.matches(in: mnemonicString, range: range) @@ -77,7 +77,7 @@ public struct BIP39 { return String(text[stringRange]) } - public func validate(mnemonicComponents: [String]) throws { + func validate(mnemonicComponents: [String]) throws { // Validate words count guard !mnemonicComponents.isEmpty else { throw MnemonicError.wrongWordCount @@ -88,7 +88,7 @@ public struct BIP39 { } // Validate wordlist by the first word - let wordlist = try getWordlist(by: mnemonicComponents[0]) + let wordlist = try getWordlist(by: mnemonicComponents[0]).1 // Validate all the words var invalidWords = Set() @@ -136,6 +136,11 @@ public struct BIP39 { } } + // Validate wordlist by the first word + func parseWordlist(from mnemonicComponents: [String]) throws -> Wordlist { + return try getWordlist(by: mnemonicComponents[0]).0 + } + /// Generate a mnemonic from data. Useful for testing purposes. /// - Parameters: /// - data: The entropy data in hex format @@ -183,12 +188,12 @@ public struct BIP39 { return data } - private func getWordlist(by word: String) throws -> [String] { + private func getWordlist(by word: String) throws -> (Wordlist, [String]) { for list in Wordlist.allCases { let words = list.words if words.contains(word) { - return words + return (list, words) } } diff --git a/TangemSdk/TangemSdk/Common/BIP39/Mnemonic.swift b/TangemSdk/TangemSdk/Common/BIP39/Mnemonic.swift index 553e4155c..d6a3e1957 100644 --- a/TangemSdk/TangemSdk/Common/BIP39/Mnemonic.swift +++ b/TangemSdk/TangemSdk/Common/BIP39/Mnemonic.swift @@ -9,8 +9,21 @@ import Foundation @available(iOS 13.0, *) -public enum Mnemonic { - public static func generateMnemonic(entropyLength: EntropyLength, wordlist: Wordlist) throws { +public struct Mnemonic { + public let mnemonicComponents: [String] + public let wordlist: Wordlist + public var mnemonic: String { bip39.convertToMnemonicString(mnemonicComponents) } + + private let bip39 = BIP39() + + public init(with entropy: EntropyLength = .bits128, wordList: Wordlist = .en) throws { + mnemonicComponents = try bip39.generateMnemonic(entropyLength: entropy, wordlist: wordList) + self.wordlist = wordList + } + + public init(with mnemonic: String) throws { + mnemonicComponents = try bip39.parse(mnemonicString: mnemonic) + self.wordlist = try bip39.parseWordlist(from: mnemonicComponents) } } diff --git a/TangemSdk/TangemSdk/Common/Extensions/UInt64+.swift b/TangemSdk/TangemSdk/Common/Extensions/UInt64+.swift new file mode 100644 index 000000000..21b7a4a59 --- /dev/null +++ b/TangemSdk/TangemSdk/Common/Extensions/UInt64+.swift @@ -0,0 +1,20 @@ +// +// UInt64+.swift +// TangemSdk +// +// Created by Alexander Osokin on 09.03.2023. +// Copyright © 2023 Tangem AG. All rights reserved. +// + +import Foundation + +extension UInt64 { + /// Convert hex data to Integer + /// - Parameter hexData: length bytes + public init(hexData: Data) { + let value = hexData.reduce(0) { v, byte in + return v << 8 | UInt64(byte) + } + self = value + } +} diff --git a/TangemSdk/TangemSdk/Common/HDWallet/BIP32/BIP32.swift b/TangemSdk/TangemSdk/Common/HDWallet/BIP32/BIP32.swift index a14d650f4..72cb491aa 100644 --- a/TangemSdk/TangemSdk/Common/HDWallet/BIP32/BIP32.swift +++ b/TangemSdk/TangemSdk/Common/HDWallet/BIP32/BIP32.swift @@ -7,8 +7,49 @@ // import Foundation +import CryptoKit struct BIP32 { + @available(iOS 13.0, *) + func makeMasterKey(from seed: Data, curve: EllipticCurve) throws -> ExtendedPrivateKey { + // The seed must be between 128 and 512 bits + guard 16...64 ~= seed.count else { + throw HDWalletError.invalidSeed + } + + guard let keyData = curve.hmacKey.rawValue.data(using: .utf8) else { + throw HDWalletError.invalidHMACKey + } + + let symmetricKey = SymmetricKey(data: keyData) + let authenticationCode = HMAC.authenticationCode(for: seed, using: symmetricKey) + let i = Data(authenticationCode) + let iL = Data(i.prefix(32)) + let iR = Data(i.suffix(32)) + + if !isPrivateKeyValid(iL, curve: curve) { + return try makeMasterKey(from: i, curve: curve) + } + + return ExtendedPrivateKey(privateKey: iL, chainCode: iR) + } + + // Verify the key + // https://github.com/satoshilabs/slips/blob/master/slip-0010.md + @available(iOS 13.0, *) + private func isPrivateKeyValid(_ privateKey: Data, curve: EllipticCurve) -> Bool { + switch curve { + case .secp256k1: + return Secp256k1Utils().isPrivateKeyValid(privateKey) + case .secp256r1: + return Secp256r1Utils().isPrivateKeyValid(privateKey) + default: + return true + } + } +} + +extension BIP32 { enum Constants { static let hardenedOffset: UInt32 = .init(0x80000000) static let hardenedSymbol: String = "'" @@ -16,5 +57,27 @@ struct BIP32 { static let masterKeySymbol: String = "m" static let separatorSymbol: Character = "/" } + + enum HMACKey: String { + case secp256k1 = "Bitcoin seed" + case secp256r1 = "Nist256p1 seed" + case ed25519 = "ed25519 seed" + } } +@available(iOS 13.0, *) +fileprivate extension EllipticCurve { + var hmacKey: BIP32.HMACKey { + switch self { + case .secp256k1: + return .secp256k1 + case .ed25519: + return .ed25519 + case .secp256r1: + return .secp256r1 + case .bls12381_G2, .bls12381_G2_AUG, .bls12381_G2_POP: + // https://eips.ethereum.org/EIPS/eip-2333#derive_master_sk + fatalError("not applicable for this curve") + } + } +} diff --git a/TangemSdk/TangemSdk/Common/HDWallet/BIP32/ExtendedPrivateKey.swift b/TangemSdk/TangemSdk/Common/HDWallet/BIP32/ExtendedPrivateKey.swift new file mode 100644 index 000000000..c82f8b397 --- /dev/null +++ b/TangemSdk/TangemSdk/Common/HDWallet/BIP32/ExtendedPrivateKey.swift @@ -0,0 +1,21 @@ +// +// ExtendedPrivateKey.swift +// TangemSdk +// +// Created by Alexander Osokin on 07.03.2023. +// Copyright © 2023 Tangem AG. All rights reserved. +// + +import Foundation + +@available(iOS 13.0, *) +/// BIP32 extended private key +public struct ExtendedPrivateKey: Equatable, Hashable, JSONStringConvertible, Codable { + public let privateKey: Data + public let chainCode: Data + + public init(privateKey: Data, chainCode: Data) { + self.privateKey = privateKey + self.chainCode = chainCode + } +} diff --git a/TangemSdk/TangemSdk/Common/HDWallet/HDWalletError.swift b/TangemSdk/TangemSdk/Common/HDWallet/HDWalletError.swift index 548653910..33008ea27 100644 --- a/TangemSdk/TangemSdk/Common/HDWallet/HDWalletError.swift +++ b/TangemSdk/TangemSdk/Common/HDWallet/HDWalletError.swift @@ -12,6 +12,8 @@ enum HDWalletError: String, Error, LocalizedError { case hardenedNotSupported case wrongPath case wrongIndex + case invalidSeed + case invalidHMACKey var errorDescription: String? { return rawValue diff --git a/TangemSdk/TangemSdk/Crypto/Secp256k1Utils.swift b/TangemSdk/TangemSdk/Crypto/Secp256k1Utils.swift index 8824a60e2..34bb293aa 100644 --- a/TangemSdk/TangemSdk/Crypto/Secp256k1Utils.swift +++ b/TangemSdk/TangemSdk/Crypto/Secp256k1Utils.swift @@ -117,6 +117,13 @@ public final class Secp256k1Utils { return try serializePublicKey(&publicKey, compressed: compressed) } + + func isPrivateKeyValid(_ privateKey: Data) -> Bool { + guard !privateKey.isEmpty else { return false } + + let privateKey = privateKey.toBytes + return secp256k1_ec_seckey_verify(context, privateKey) == 1 + } func serializeDer(_ signature: inout secp256k1_ecdsa_signature) throws -> Data { var length: Int = 128 diff --git a/TangemSdk/TangemSdk/Crypto/Secp256r1Utils.swift b/TangemSdk/TangemSdk/Crypto/Secp256r1Utils.swift new file mode 100644 index 000000000..9db25bf1d --- /dev/null +++ b/TangemSdk/TangemSdk/Crypto/Secp256r1Utils.swift @@ -0,0 +1,18 @@ +// +// Secp256r1Utils.swift +// TangemSdk +// +// Created by Alexander Osokin on 09.03.2023. +// Copyright © 2023 Tangem AG. All rights reserved. +// + +import Foundation +import CryptoKit + +@available(iOS 13.0, *) +class Secp256r1Utils { + func isPrivateKeyValid(_ privateKey: Data) -> Bool { + let key = try? P256.Signing.PrivateKey(rawRepresentation: privateKey) + return key != nil + } +} diff --git a/TangemSdk/TangemSdkTests/BIP32Tests.swift b/TangemSdk/TangemSdkTests/BIP32Tests.swift new file mode 100644 index 000000000..02e57638e --- /dev/null +++ b/TangemSdk/TangemSdkTests/BIP32Tests.swift @@ -0,0 +1,58 @@ +// +// BIP32Tests.swift +// TangemSdkTests +// +// Created by Alexander Osokin on 07.03.2023. +// Copyright © 2023 Tangem AG. All rights reserved. +// + +import Foundation +import XCTest +@testable import TangemSdk + +@available(iOS 13.0, *) +class BIP32Tests: XCTestCase { + func testSecp256k1MasterKeyGeneration() throws { + let bip32 = BIP32() + let masterKey = try bip32.makeMasterKey(from: Data(hexString: "000102030405060708090a0b0c0d0e0f"), curve: .secp256k1) + XCTAssertEqual(masterKey.privateKey.hexString.lowercased(), "e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35".lowercased()) + XCTAssertEqual(masterKey.chainCode.hexString.lowercased(), "873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508".lowercased()) + + let masterKey2 = try bip32.makeMasterKey(from: Data(hexString: "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"), curve: .secp256k1) + XCTAssertEqual(masterKey2.privateKey.hexString.lowercased(), "4b03d6fc340455b363f51020ad3ecca4f0850280cf436c70c727923f6db46c3e".lowercased()) + XCTAssertEqual(masterKey2.chainCode.hexString.lowercased(), "60499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd9689".lowercased()) + } + + func testSecp256r1MasterKeyGeneration() throws { + let bip32 = BIP32() + let masterKey = try bip32.makeMasterKey(from: Data(hexString: "000102030405060708090a0b0c0d0e0f"), curve: .secp256r1) + XCTAssertEqual(masterKey.privateKey.hexString.lowercased(), "612091aaa12e22dd2abef664f8a01a82cae99ad7441b7ef8110424915c268bc2".lowercased()) + XCTAssertEqual(masterKey.chainCode.hexString.lowercased(), "beeb672fe4621673f722f38529c07392fecaa61015c80c34f29ce8b41b3cb6ea".lowercased()) + + let masterKey2 = try bip32.makeMasterKey(from: Data(hexString: "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"), curve: .secp256r1) + XCTAssertEqual(masterKey2.privateKey.hexString.lowercased(), "eaa31c2e46ca2962227cf21d73a7ef0ce8b31c756897521eb6c7b39796633357".lowercased()) + XCTAssertEqual(masterKey2.chainCode.hexString.lowercased(), "96cd4465a9644e31528eda3592aa35eb39a9527769ce1855beafc1b81055e75d".lowercased()) + } + + func testEd25519MasterKeyGeneration() throws { + let bip32 = BIP32() + let masterKey = try bip32.makeMasterKey(from: Data(hexString: "000102030405060708090a0b0c0d0e0f"), curve: .ed25519) + XCTAssertEqual(masterKey.privateKey.hexString.lowercased(), "2b4be7f19ee27bbf30c667b642d5f4aa69fd169872f8fc3059c08ebae2eb19e7".lowercased()) + XCTAssertEqual(masterKey.chainCode.hexString.lowercased(), "90046a93de5380a72b5e45010748567d5ea02bbf6522f979e05c0d8d8ca9fffb".lowercased()) + + let masterKey2 = try bip32.makeMasterKey(from: Data(hexString: "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"), curve: .ed25519) + XCTAssertEqual(masterKey2.privateKey.hexString.lowercased(), "171cb88b1b3c1db25add599712e36245d75bc65a1a5c9e18d76f9f2b1eab4012".lowercased()) + XCTAssertEqual(masterKey2.chainCode.hexString.lowercased(), "ef70a74db9c3a5af931b5fe73ed8e1a53464133654fd55e7a66f8570b8e33c3b".lowercased()) + } + + func testSecp256r1MasterKeyGenerationRetry() throws { + let bip32 = BIP32() + let masterKey = try bip32.makeMasterKey(from: Data(hexString: "a7305bc8df8d0951f0cb224c0e95d7707cbdf2c6ce7e8d481fec69c7ff5e9446"), curve: .secp256r1) + XCTAssertEqual(masterKey.privateKey.hexString.lowercased(), "3b8c18469a4634517d6d0b65448f8e6c62091b45540a1743c5846be55d47d88f".lowercased()) + XCTAssertEqual(masterKey.chainCode.hexString.lowercased(), "7762f9729fed06121fd13f326884c82f59aa95c57ac492ce8c9654e60efd130c".lowercased()) + } +} + +@available(iOS 13.0, *) +private extension BIP32Tests { +} diff --git a/TangemSdk/TangemSdkTests/Seed/MnemonicTests.swift b/TangemSdk/TangemSdkTests/BIP39Tests.swift similarity index 95% rename from TangemSdk/TangemSdkTests/Seed/MnemonicTests.swift rename to TangemSdk/TangemSdkTests/BIP39Tests.swift index 8ce146953..18c2722d7 100644 --- a/TangemSdk/TangemSdkTests/Seed/MnemonicTests.swift +++ b/TangemSdk/TangemSdkTests/BIP39Tests.swift @@ -1,5 +1,5 @@ // -// MnemonicTests.swift +// BIP39Tests.swift // TangemSdkTests // // Created by Alexander Osokin on 06.03.2023. @@ -11,7 +11,7 @@ import XCTest @testable import TangemSdk @available(iOS 13.0, *) -class MnemonicTests: XCTestCase { +class BIP39Tests: XCTestCase { func testReadWords() { let langs = Wordlist.allCases @@ -101,7 +101,7 @@ class MnemonicTests: XCTestCase { } private func getTestVectors(from filename: String) throws -> [String: Any]? { - guard let url = Bundle(for: MnemonicTests.self).url(forResource: filename, withExtension: "json") else { + guard let url = Bundle(for: type(of: self)).url(forResource: filename, withExtension: "json") else { return nil } @@ -116,7 +116,7 @@ class MnemonicTests: XCTestCase { } @available(iOS 13.0, *) -private extension MnemonicTests { +private extension BIP39Tests { enum Constants { static let englishTestVectors = "english" static let passphrase = "TREZOR" diff --git a/TangemSdk/TangemSdkTests/CryptoUtilsTests.swift b/TangemSdk/TangemSdkTests/CryptoUtilsTests.swift index 24b0c6ccd..2897c80df 100644 --- a/TangemSdk/TangemSdkTests/CryptoUtilsTests.swift +++ b/TangemSdk/TangemSdkTests/CryptoUtilsTests.swift @@ -152,6 +152,23 @@ class CryptoUtilsTests: XCTestCase { XCTAssertEqual(unmarshalled?.s.hexString, "4D5A1F20E671A6CC57D2A46FC28488C833B4337B5C37089B99BBC16707459BA1") XCTAssertEqual(unmarshalled?.v.hexString, "1C") } + + func testSecp256k1PrivateKeyValidation() { + let utils = Secp256k1Utils() + + XCTAssertFalse(utils.isPrivateKeyValid(Data())) + XCTAssertFalse(utils.isPrivateKeyValid(Data(repeating: UInt8(0), count: 32))) + } + + func testSecp256r1PrivateKeyValidation() { + let utils = Secp256r1Utils() + + XCTAssertFalse(utils.isPrivateKeyValid(Data())) + XCTAssertFalse(utils.isPrivateKeyValid(Data(repeating: UInt8(0), count: 32))) + XCTAssertFalse(utils.isPrivateKeyValid(Data(hexString: "FFFFFFFFFE92BF972115EB5008573E60811CA5A79B40EAAF9036189360F47413"))) + XCTAssertFalse(utils.isPrivateKeyValid(Data(hexString: "FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC4FC632551"))) + XCTAssertTrue(utils.isPrivateKeyValid(Data(hexString: "FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632550"))) + } } diff --git a/TangemSdk/TangemSdkTests/Seed/mnemonic_invalid_test_vectors.json b/TangemSdk/TangemSdkTests/Jsons/BIP39/mnemonic_invalid_test_vectors.json similarity index 100% rename from TangemSdk/TangemSdkTests/Seed/mnemonic_invalid_test_vectors.json rename to TangemSdk/TangemSdkTests/Jsons/BIP39/mnemonic_invalid_test_vectors.json diff --git a/TangemSdk/TangemSdkTests/Seed/mnemonic_valid_test_vectors.json b/TangemSdk/TangemSdkTests/Jsons/BIP39/mnemonic_valid_test_vectors.json similarity index 100% rename from TangemSdk/TangemSdkTests/Seed/mnemonic_valid_test_vectors.json rename to TangemSdk/TangemSdkTests/Jsons/BIP39/mnemonic_valid_test_vectors.json diff --git a/TangemSdk/TangemSdkTests/Seed/seed_test_vectors.json b/TangemSdk/TangemSdkTests/Jsons/BIP39/seed_test_vectors.json similarity index 100% rename from TangemSdk/TangemSdkTests/Seed/seed_test_vectors.json rename to TangemSdk/TangemSdkTests/Jsons/BIP39/seed_test_vectors.json From 31b5a0acd57f776bf21b81569981d2fe513fd4bc Mon Sep 17 00:00:00 2001 From: Alexander Osokin Date: Thu, 9 Mar 2023 21:36:56 +0500 Subject: [PATCH 06/24] IOS-3059 Clean --- TangemSdk/TangemSdk/Common/BIP39/BIP39.swift | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/TangemSdk/TangemSdk/Common/BIP39/BIP39.swift b/TangemSdk/TangemSdk/Common/BIP39/BIP39.swift index 9dfc3ab19..a41aa1ae0 100644 --- a/TangemSdk/TangemSdk/Common/BIP39/BIP39.swift +++ b/TangemSdk/TangemSdk/Common/BIP39/BIP39.swift @@ -61,22 +61,6 @@ struct BIP39 { return components } - private static func extractCaptureGroupString( - from result: NSTextCheckingResult, - at index: Int, - in text: String - ) -> String? { - guard index < result.numberOfRanges, - let stringRange = Range( - result.range(at: index), - in: text - ) else { - return nil - } - - return String(text[stringRange]) - } - func validate(mnemonicComponents: [String]) throws { // Validate words count guard !mnemonicComponents.isEmpty else { From c0931ed4641fff572f9a579f509ea714b000f327 Mon Sep 17 00:00:00 2001 From: Alexander Osokin Date: Fri, 10 Mar 2023 10:31:55 +0500 Subject: [PATCH 07/24] IOS-3059 Add seed generation into Mnemonic --- TangemSdk/TangemSdk/Common/BIP39/Mnemonic.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/TangemSdk/TangemSdk/Common/BIP39/Mnemonic.swift b/TangemSdk/TangemSdk/Common/BIP39/Mnemonic.swift index d6a3e1957..2966eeadf 100644 --- a/TangemSdk/TangemSdk/Common/BIP39/Mnemonic.swift +++ b/TangemSdk/TangemSdk/Common/BIP39/Mnemonic.swift @@ -26,4 +26,8 @@ public struct Mnemonic { mnemonicComponents = try bip39.parse(mnemonicString: mnemonic) self.wordlist = try bip39.parseWordlist(from: mnemonicComponents) } + + public func generateSeed(with passphrase: String = "") throws -> Data { + return try bip39.generateSeed(from: mnemonicComponents, passphrase: passphrase) + } } From 253807de0f6b7fbd17e41513a9a34edd02e8331b Mon Sep 17 00:00:00 2001 From: Alexander Osokin Date: Fri, 10 Mar 2023 16:25:02 +0500 Subject: [PATCH 08/24] IOS-3059 Configure podspec --- TangemSdk.podspec | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TangemSdk.podspec b/TangemSdk.podspec index e41f00a45..b8858308f 100644 --- a/TangemSdk.podspec +++ b/TangemSdk.podspec @@ -53,6 +53,7 @@ Tangem is a Swiss-based secure hardware wallet manufacturer that enables blockch s.weak_frameworks = 'CoreNFC', 'CryptoKit', 'Combine', 'SwiftUI' s.resource_bundles = { 'TangemSdk' => ['TangemSdk/TangemSdk/**/*.lproj/*.strings', - 'TangemSdk/TangemSdk/Haptics/*.ahap']} + 'TangemSdk/TangemSdk/Haptics/*.ahap', + 'TangemSdk/TangemSdk/Common/BIP39/Wordlists/*.txt']} end From 084ea0ce7505cb354484a19bb541615ef5d031cb Mon Sep 17 00:00:00 2001 From: Alexander Osokin Date: Fri, 10 Mar 2023 23:56:10 +0500 Subject: [PATCH 09/24] IOS-3059 Complete bip32 with tests --- TangemSdk/TangemSdk.xcodeproj/project.pbxproj | 52 ++- .../TangemSdk/Common/Extensions/Byte+.swift | 20 + .../TangemSdk/Common/Extensions/Data+.swift | 19 + .../HDWallet/BIP32/ExtendedPrivateKey.swift | 21 - .../HDWallet/BIP32/ExtendedPublicKey.swift | 61 --- .../{Common => Crypto}/BIP39/BIP39.swift | 0 .../BIP39/EntropyLength.swift | 0 .../{Common => Crypto}/BIP39/Mnemonic.swift | 0 .../BIP39/MnemonicError.swift | 0 .../{Common => Crypto}/BIP39/Wordlist.swift | 0 .../BIP39/Wordlists/english.txt | 0 TangemSdk/TangemSdk/Crypto/Base58.swift | 181 ++++++++ TangemSdk/TangemSdk/Crypto/CryptoUtils.swift | 37 ++ .../HDWallet/BIP32/BIP32.swift | 18 +- .../HDWallet/BIP32/DerivationNode.swift | 0 .../HDWallet/BIP32/DerivationPath.swift | 0 .../ExtendedKeySerializable.swift | 16 + .../ExtendedKeySerializationError.swift | 18 + .../ExtendedKeySerializer.swift | 34 ++ .../HDWallet/BIP32/ExtendedPrivateKey.swift | 126 ++++++ .../HDWallet/BIP32/ExtendedPublicKey.swift | 155 +++++++ .../{Common => Crypto}/HDWallet/BIP44.swift | 0 .../HDWallet/HDWalletError.swift | 0 TangemSdk/TangemSdk/Crypto/NetworkType.swift | 15 + TangemSdk/TangemSdk/Crypto/Ripemd160.swift | 396 ++++++++++++++++++ .../TangemSdk/Crypto/Secp256r1Utils.swift | 18 - TangemSdk/TangemSdk/Crypto/WIF.swift | 54 +++ TangemSdk/TangemSdkTests/BIP32Tests.swift | 83 +++- TangemSdk/TangemSdkTests/BIP39Tests.swift | 5 + .../TangemSdkTests/CryptoUtilsTests.swift | 17 +- .../TangemSdkTests/ExtendedKeyTests.swift | 136 ++++++ .../Jsons/DeriveWalletPublicKey.json | 5 +- .../Jsons/DeriveWalletPublicKeys.json | 10 +- TangemSdk/TangemSdkTests/WIFTests.swift | 22 + 34 files changed, 1381 insertions(+), 138 deletions(-) delete mode 100644 TangemSdk/TangemSdk/Common/HDWallet/BIP32/ExtendedPrivateKey.swift delete mode 100644 TangemSdk/TangemSdk/Common/HDWallet/BIP32/ExtendedPublicKey.swift rename TangemSdk/TangemSdk/{Common => Crypto}/BIP39/BIP39.swift (100%) rename TangemSdk/TangemSdk/{Common => Crypto}/BIP39/EntropyLength.swift (100%) rename TangemSdk/TangemSdk/{Common => Crypto}/BIP39/Mnemonic.swift (100%) rename TangemSdk/TangemSdk/{Common => Crypto}/BIP39/MnemonicError.swift (100%) rename TangemSdk/TangemSdk/{Common => Crypto}/BIP39/Wordlist.swift (100%) rename TangemSdk/TangemSdk/{Common => Crypto}/BIP39/Wordlists/english.txt (100%) create mode 100644 TangemSdk/TangemSdk/Crypto/Base58.swift rename TangemSdk/TangemSdk/{Common => Crypto}/HDWallet/BIP32/BIP32.swift (79%) rename TangemSdk/TangemSdk/{Common => Crypto}/HDWallet/BIP32/DerivationNode.swift (100%) rename TangemSdk/TangemSdk/{Common => Crypto}/HDWallet/BIP32/DerivationPath.swift (100%) create mode 100644 TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedKeySerialization/ExtendedKeySerializable.swift create mode 100644 TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedKeySerialization/ExtendedKeySerializationError.swift create mode 100644 TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedKeySerialization/ExtendedKeySerializer.swift create mode 100644 TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedPrivateKey.swift create mode 100644 TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedPublicKey.swift rename TangemSdk/TangemSdk/{Common => Crypto}/HDWallet/BIP44.swift (100%) rename TangemSdk/TangemSdk/{Common => Crypto}/HDWallet/HDWalletError.swift (100%) create mode 100644 TangemSdk/TangemSdk/Crypto/NetworkType.swift create mode 100644 TangemSdk/TangemSdk/Crypto/Ripemd160.swift delete mode 100644 TangemSdk/TangemSdk/Crypto/Secp256r1Utils.swift create mode 100644 TangemSdk/TangemSdk/Crypto/WIF.swift create mode 100644 TangemSdk/TangemSdkTests/ExtendedKeyTests.swift create mode 100644 TangemSdk/TangemSdkTests/WIFTests.swift diff --git a/TangemSdk/TangemSdk.xcodeproj/project.pbxproj b/TangemSdk/TangemSdk.xcodeproj/project.pbxproj index 784c53952..f375137fa 100644 --- a/TangemSdk/TangemSdk.xcodeproj/project.pbxproj +++ b/TangemSdk/TangemSdk.xcodeproj/project.pbxproj @@ -302,7 +302,15 @@ DC1244C729B776D40037BC05 /* ExtendedPrivateKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244C629B776D40037BC05 /* ExtendedPrivateKey.swift */; }; DC1244C929B778750037BC05 /* BIP32Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244C829B778750037BC05 /* BIP32Tests.swift */; }; DC1244CB29B9ECAA0037BC05 /* UInt64+.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244CA29B9ECAA0037BC05 /* UInt64+.swift */; }; - DC1244CD29B9F6D70037BC05 /* Secp256r1Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244CC29B9F6D70037BC05 /* Secp256r1Utils.swift */; }; + DC1244D829BB65970037BC05 /* ExtendedKeySerializationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244D429BB65970037BC05 /* ExtendedKeySerializationError.swift */; }; + DC1244DA29BB65970037BC05 /* ExtendedKeySerializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244D629BB65970037BC05 /* ExtendedKeySerializable.swift */; }; + DC1244DC29BB66840037BC05 /* ExtendedKeyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244DB29BB66840037BC05 /* ExtendedKeyTests.swift */; }; + DC1244DE29BB677C0037BC05 /* Ripemd160.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244DD29BB677C0037BC05 /* Ripemd160.swift */; }; + DC1244E029BB6D460037BC05 /* Base58.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244DF29BB6D460037BC05 /* Base58.swift */; }; + DC1244E229BB7B390037BC05 /* WIF.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244E129BB7B390037BC05 /* WIF.swift */; }; + DC1244E429BB806E0037BC05 /* WIFTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244E329BB806E0037BC05 /* WIFTests.swift */; }; + DC1244E629BB8E580037BC05 /* NetworkType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244E529BB8E580037BC05 /* NetworkType.swift */; }; + DC1244E829BB9E0C0037BC05 /* ExtendedKeySerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244E729BB9E0C0037BC05 /* ExtendedKeySerializer.swift */; }; DC59CB0429AF597900EC14E1 /* Wordlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC59CB0329AF597900EC14E1 /* Wordlist.swift */; }; DC59CB0A29AF6F9C00EC14E1 /* EntropyLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC59CB0929AF6F9C00EC14E1 /* EntropyLength.swift */; }; DC59CB0C29AF706100EC14E1 /* MnemonicError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC59CB0B29AF706100EC14E1 /* MnemonicError.swift */; }; @@ -628,7 +636,15 @@ DC1244C629B776D40037BC05 /* ExtendedPrivateKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtendedPrivateKey.swift; sourceTree = ""; }; DC1244C829B778750037BC05 /* BIP32Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BIP32Tests.swift; sourceTree = ""; }; DC1244CA29B9ECAA0037BC05 /* UInt64+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UInt64+.swift"; sourceTree = ""; }; - DC1244CC29B9F6D70037BC05 /* Secp256r1Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Secp256r1Utils.swift; sourceTree = ""; }; + DC1244D429BB65970037BC05 /* ExtendedKeySerializationError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtendedKeySerializationError.swift; sourceTree = ""; }; + DC1244D629BB65970037BC05 /* ExtendedKeySerializable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtendedKeySerializable.swift; sourceTree = ""; }; + DC1244DB29BB66840037BC05 /* ExtendedKeyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtendedKeyTests.swift; sourceTree = ""; }; + DC1244DD29BB677C0037BC05 /* Ripemd160.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ripemd160.swift; sourceTree = ""; }; + DC1244DF29BB6D460037BC05 /* Base58.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Base58.swift; sourceTree = ""; }; + DC1244E129BB7B390037BC05 /* WIF.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WIF.swift; sourceTree = ""; }; + DC1244E329BB806E0037BC05 /* WIFTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WIFTests.swift; sourceTree = ""; }; + DC1244E529BB8E580037BC05 /* NetworkType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkType.swift; sourceTree = ""; }; + DC1244E729BB9E0C0037BC05 /* ExtendedKeySerializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtendedKeySerializer.swift; sourceTree = ""; }; DC59CB0329AF597900EC14E1 /* Wordlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wordlist.swift; sourceTree = ""; }; DC59CB0929AF6F9C00EC14E1 /* EntropyLength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntropyLength.swift; sourceTree = ""; }; DC59CB0B29AF706100EC14E1 /* MnemonicError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MnemonicError.swift; sourceTree = ""; }; @@ -714,6 +730,7 @@ 5D2B798426BBED1D002A78D4 /* BIP32 */ = { isa = PBXGroup; children = ( + DC1244D229BB65970037BC05 /* ExtendedKeySerialization */, 5D170AF026B42CA3000D4F36 /* ExtendedPublicKey.swift */, 5D73FC2826B8140200DF1BB4 /* DerivationPath.swift */, 5D73FC2C26B8261400DF1BB4 /* DerivationNode.swift */, @@ -1222,6 +1239,8 @@ 5DA79429236C64D100B33DB5 /* IntUtilsTests.swift */, DC1244B829B610550037BC05 /* BIP39Tests.swift */, DC1244C829B778750037BC05 /* BIP32Tests.swift */, + DC1244DB29BB66840037BC05 /* ExtendedKeyTests.swift */, + DC1244E329BB806E0037BC05 /* WIFTests.swift */, 5D149479268625A400C0D923 /* CommonTests.swift */, 5D6795B1237AEFB60075A330 /* ApduTests.swift */, 5D437EEA237BE980009C82A8 /* TlvTests.swift */, @@ -1263,8 +1282,6 @@ 5DF7CFCA233D034E00461F4D /* Common */ = { isa = PBXGroup; children = ( - DC59CB0129AF582800EC14E1 /* BIP39 */, - 5D170AEF26B42C88000D4F36 /* HDWallet */, B0A9436525651E2600A7958E /* Card */, 5D6508242673935700A8D45B /* OptionSet */, 5D650820266E387C00A8D45B /* Core */, @@ -1291,13 +1308,18 @@ 5DFFC1A2234DC969001C2F35 /* Crypto */ = { isa = PBXGroup; children = ( + DC59CB0129AF582800EC14E1 /* BIP39 */, + 5D170AEF26B42C88000D4F36 /* HDWallet */, 5D83F36E247EC275005E7A35 /* secp256k1 */, 5DFFC1A0234DC715001C2F35 /* CryptoUtils.swift */, 5D26CEE4243C902C00994CC0 /* Secp256k1Utils.swift */, 5DF5FB1B244F2C15002DB244 /* IssuerDataVerifier.swift */, 5D27C8A727736F190095409C /* Secp256k1Key.swift */, 5D27C8A927737D200095409C /* Secp256k1Signature.swift */, - DC1244CC29B9F6D70037BC05 /* Secp256r1Utils.swift */, + DC1244DD29BB677C0037BC05 /* Ripemd160.swift */, + DC1244DF29BB6D460037BC05 /* Base58.swift */, + DC1244E129BB7B390037BC05 /* WIF.swift */, + DC1244E529BB8E580037BC05 /* NetworkType.swift */, ); path = Crypto; sourceTree = ""; @@ -1424,6 +1446,16 @@ path = BIP39; sourceTree = ""; }; + DC1244D229BB65970037BC05 /* ExtendedKeySerialization */ = { + isa = PBXGroup; + children = ( + DC1244D429BB65970037BC05 /* ExtendedKeySerializationError.swift */, + DC1244D629BB65970037BC05 /* ExtendedKeySerializable.swift */, + DC1244E729BB9E0C0037BC05 /* ExtendedKeySerializer.swift */, + ); + path = ExtendedKeySerialization; + sourceTree = ""; + }; DC59CB0129AF582800EC14E1 /* BIP39 */ = { isa = PBXGroup; children = ( @@ -1705,7 +1737,6 @@ 5D23C42426CE749F00A1A280 /* FloatingTextField.swift in Sources */, B00BC5C4260B80E600F0647D /* Track.swift in Sources */, 5D14947F2686458100C0D923 /* GenericPasswordConvertible.swift in Sources */, - DC1244CD29B9F6D70037BC05 /* Secp256r1Utils.swift in Sources */, 5D86CBDB24A1102D00FB5BA7 /* Issuer.swift in Sources */, DC1244C729B776D40037BC05 /* ExtendedPrivateKey.swift in Sources */, 5D944758264EC52400EA9CD2 /* JSONRPC.swift in Sources */, @@ -1733,6 +1764,7 @@ DC59CB0C29AF706100EC14E1 /* MnemonicError.swift in Sources */, 5DE43A6226D5157900ECA36A /* ReadBackupDataCommand.swift in Sources */, B006971825FFABA10040D203 /* InteractionMode.swift in Sources */, + DC1244E029BB6D460037BC05 /* Base58.swift in Sources */, 5DDD6C5625D2D14000E48D7B /* TlvLogging.swift in Sources */, 5D58202024221E060057EF40 /* CardSession.swift in Sources */, B0390F92256590250061E5ED /* EllipticCurve.swift in Sources */, @@ -1752,16 +1784,19 @@ 5DDD6C6C25D30B0D00E48D7B /* SuccessResponse.swift in Sources */, DC59CB0429AF597900EC14E1 /* Wordlist.swift in Sources */, 5D8666622731687A0095CC82 /* ResetCodesViewModel.swift in Sources */, + DC1244DE29BB677C0037BC05 /* Ripemd160.swift in Sources */, 5D6831D423DB31AB0095BB1D /* PurgeWalletCommand.swift in Sources */, DA216CC0282E4D86003585B9 /* AccessCodeRepository.swift in Sources */, 5DCAA7872383DDC200249F42 /* ReadIssuerDataCommand.swift in Sources */, B0390F7B25658EEE0061E5ED /* CardSettings.swift in Sources */, + DC1244E829BB9E0C0037BC05 /* ExtendedKeySerializer.swift in Sources */, 5DA5B61C233E12B30058C720 /* Tlv.swift in Sources */, 5D0E56032757C11F00D46F54 /* CardDataResponse.swift in Sources */, 5D06F6AD27565441006A15B9 /* StartPrimaryCardLinkingTask.swift in Sources */, 5D2F3EE526CBDAA100779CAC /* KeyboardAdaptive.swift in Sources */, 5D7D5FB223449D4000058D69 /* SessionEnvironment.swift in Sources */, 5D2FE06324DD82750086B5E8 /* AttestCardKeyCommand.swift in Sources */, + DC1244E229BB7B390037BC05 /* WIF.swift in Sources */, 5D539ECB276CDD8600AB8B53 /* DeriveMultipleWalletPublicKeysTask.swift in Sources */, DA6C752A292682650070EEFD /* LAContext+.swift in Sources */, 5DE43A6026D511CE00ECA36A /* LinkBackupCardsCommand.swift in Sources */, @@ -1864,16 +1899,19 @@ 5DE7DD302695DCD300472205 /* CardFilter.swift in Sources */, 5D4B128326D40C77006E173C /* StartPrimaryCardLinkingCommand.swift in Sources */, 5D14092026BAABD400B811A1 /* BIP32.swift in Sources */, + DC1244D829BB65970037BC05 /* ExtendedKeySerializationError.swift in Sources */, 5DB4406F234B750200AC39F1 /* String+.swift in Sources */, 5D2FE06524DD82BA0086B5E8 /* VerifyCardRequest.swift in Sources */, 5DFFC4A2233BA5E0004964E8 /* Byte+.swift in Sources */, B06947BF2534570B0056A887 /* DeleteFilesTask.swift in Sources */, + DC1244DA29BB65970037BC05 /* ExtendedKeySerializable.swift in Sources */, 5DD6762C26CD621500D8C909 /* UIScreen+.swift in Sources */, 5D445B7826E29A2800F6F0FE /* SignResetPinToken.swift in Sources */, 5DEBB2A625DC0FF400D1734F /* OnlineCardVerifier.swift in Sources */, 5D7F4E52249006DB00A1700D /* Bundle+.swift in Sources */, 5D445B7A26E29C2300F6F0FE /* ResetPinCommand.swift in Sources */, 5D86665C273166810095CC82 /* ResetCodesViewState.swift in Sources */, + DC1244E629BB8E580037BC05 /* NetworkType.swift in Sources */, 5D46F158268105BF00DC6447 /* SignHashesCommand.swift in Sources */, 5D85A80624069C840038A2D0 /* ReadIssuerExtraDataCommand.swift in Sources */, ); @@ -1887,8 +1925,10 @@ 5DA80CA9231D247A00A50A10 /* CryptoUtilsTests.swift in Sources */, 5D713B2D236C3F6400E4F6FC /* StringUtilsTest.swift in Sources */, DC1244C929B778750037BC05 /* BIP32Tests.swift in Sources */, + DC1244E429BB806E0037BC05 /* WIFTests.swift in Sources */, DC1244B929B610550037BC05 /* BIP39Tests.swift in Sources */, 5DD127A224F3D1A0009ACA29 /* JsonTests.swift in Sources */, + DC1244DC29BB66840037BC05 /* ExtendedKeyTests.swift in Sources */, 5DAD449E236B2435006C38F8 /* DataExtensionTests.swift in Sources */, 5D6795B2237AEFB60075A330 /* ApduTests.swift in Sources */, 5D6F51FE265845D5007CD7E2 /* JSONRPCTests.swift in Sources */, diff --git a/TangemSdk/TangemSdk/Common/Extensions/Byte+.swift b/TangemSdk/TangemSdk/Common/Extensions/Byte+.swift index 09a8d4a45..0cb0ed8c1 100644 --- a/TangemSdk/TangemSdk/Common/Extensions/Byte+.swift +++ b/TangemSdk/TangemSdk/Common/Extensions/Byte+.swift @@ -7,6 +7,7 @@ // import Foundation +import CryptoKit public typealias Byte = UInt8 @@ -46,3 +47,22 @@ extension UInt16 { return String(format: "%02X", self) } } + +extension Array where Element == UInt8 { + @available(iOS 13.0, *) + public func getSha256() -> Data { + let digest = SHA256.hash(data: self) + return Data(digest) + } + + @available(iOS 13.0, *) + public func getSha512() -> Data { + let digest = SHA512.hash(data: self) + return Data(digest) + } + + @available(iOS 13.0, *) + public func getDoubleSha256() -> Data { + return getSha256().getSha256() + } +} diff --git a/TangemSdk/TangemSdk/Common/Extensions/Data+.swift b/TangemSdk/TangemSdk/Common/Extensions/Data+.swift index be3ced7ff..5612c9541 100644 --- a/TangemSdk/TangemSdk/Common/Extensions/Data+.swift +++ b/TangemSdk/TangemSdk/Common/Extensions/Data+.swift @@ -38,6 +38,20 @@ extension Data { let calendar = Calendar.current return calendar.date(from: components) } + + @available(iOS 13.0, *) + public var sha256Ripemd160: Data { + var md = RIPEMD160() + let hash = getSha256() + md.update(data: hash) + return md.finalize() + } + + public var ripemd160: Data { + var md = RIPEMD160() + md.update(data: self) + return md.finalize() + } public init(hexString: String) { self = Data() @@ -117,6 +131,11 @@ extension Data { let digest = SHA512.hash(data: self) return Data(digest) } + + @available(iOS 13.0, *) + public func getDoubleSha256() -> Data { + return getSha256().getSha256() + } public var toBytes: [Byte] { return Array(self) diff --git a/TangemSdk/TangemSdk/Common/HDWallet/BIP32/ExtendedPrivateKey.swift b/TangemSdk/TangemSdk/Common/HDWallet/BIP32/ExtendedPrivateKey.swift deleted file mode 100644 index c82f8b397..000000000 --- a/TangemSdk/TangemSdk/Common/HDWallet/BIP32/ExtendedPrivateKey.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// ExtendedPrivateKey.swift -// TangemSdk -// -// Created by Alexander Osokin on 07.03.2023. -// Copyright © 2023 Tangem AG. All rights reserved. -// - -import Foundation - -@available(iOS 13.0, *) -/// BIP32 extended private key -public struct ExtendedPrivateKey: Equatable, Hashable, JSONStringConvertible, Codable { - public let privateKey: Data - public let chainCode: Data - - public init(privateKey: Data, chainCode: Data) { - self.privateKey = privateKey - self.chainCode = chainCode - } -} diff --git a/TangemSdk/TangemSdk/Common/HDWallet/BIP32/ExtendedPublicKey.swift b/TangemSdk/TangemSdk/Common/HDWallet/BIP32/ExtendedPublicKey.swift deleted file mode 100644 index 6863f4ef9..000000000 --- a/TangemSdk/TangemSdk/Common/HDWallet/BIP32/ExtendedPublicKey.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// ExtendedPublicKey.swift -// TangemSdk -// -// Created by Alexander Osokin on 30.07.2021. -// Copyright © 2021 Tangem AG. All rights reserved. -// - -import Foundation -import CryptoKit - -@available(iOS 13.0, *) -/// BIP32 extended public key for `secp256k1`. -public struct ExtendedPublicKey: Equatable, Hashable, JSONStringConvertible, Codable { - /// Compressed `secp256k1` key - public let publicKey: Data - public let chainCode: Data - - public init(publicKey: Data, chainCode: Data) { - self.publicKey = publicKey - self.chainCode = chainCode - } - - /// This function performs CKDpub((Kpar, cpar), i) → (Ki, ci) to compute a child extended public key from the parent extended public key. - /// It is only defined for non-hardened child keys. `secp256k1` only - public func derivePublicKey(node: DerivationNode) throws -> ExtendedPublicKey { - guard publicKey.count == 33 else { //secp256k1 only - throw TangemSdkError.unsupportedCurve - } - - let index = node.index - - //We can derive only non-hardened keys - guard index < BIP32.Constants.hardenedOffset else { - throw HDWalletError.hardenedNotSupported - } - - //let I = HMAC-SHA512(Key = cpar, Data = serP(Kpar) || ser32(i)). - let data = publicKey + index.bytes4 - let hmac = HMAC.authenticationCode(for: data, using: SymmetricKey(data: chainCode)) - let digest = Data(hmac) - - let secp256k1 = Secp256k1Utils() - let ki = try secp256k1.createPublicKey(privateKey: digest[0..<32], compressed: true) - let derivedPublicKey = try secp256k1.sum(compressedPubKey1: ki, compressedPubKey2: publicKey) - let derivedChainCode = digest[32..<64] - return ExtendedPublicKey(publicKey: derivedPublicKey, chainCode: derivedChainCode) - } - - /// This function performs CKDpub((Kpar, cpar), i) → (Ki, ci) to compute a child extended public key from the parent extended public key. - /// It is only defined for non-hardened child keys. `secp256k1` only - public func derivePublicKey(path derivationPath: DerivationPath) throws -> ExtendedPublicKey { - var key: ExtendedPublicKey = self - - for node in derivationPath.nodes { - key = try key.derivePublicKey(node: node) - } - - return key - } -} diff --git a/TangemSdk/TangemSdk/Common/BIP39/BIP39.swift b/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift similarity index 100% rename from TangemSdk/TangemSdk/Common/BIP39/BIP39.swift rename to TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift diff --git a/TangemSdk/TangemSdk/Common/BIP39/EntropyLength.swift b/TangemSdk/TangemSdk/Crypto/BIP39/EntropyLength.swift similarity index 100% rename from TangemSdk/TangemSdk/Common/BIP39/EntropyLength.swift rename to TangemSdk/TangemSdk/Crypto/BIP39/EntropyLength.swift diff --git a/TangemSdk/TangemSdk/Common/BIP39/Mnemonic.swift b/TangemSdk/TangemSdk/Crypto/BIP39/Mnemonic.swift similarity index 100% rename from TangemSdk/TangemSdk/Common/BIP39/Mnemonic.swift rename to TangemSdk/TangemSdk/Crypto/BIP39/Mnemonic.swift diff --git a/TangemSdk/TangemSdk/Common/BIP39/MnemonicError.swift b/TangemSdk/TangemSdk/Crypto/BIP39/MnemonicError.swift similarity index 100% rename from TangemSdk/TangemSdk/Common/BIP39/MnemonicError.swift rename to TangemSdk/TangemSdk/Crypto/BIP39/MnemonicError.swift diff --git a/TangemSdk/TangemSdk/Common/BIP39/Wordlist.swift b/TangemSdk/TangemSdk/Crypto/BIP39/Wordlist.swift similarity index 100% rename from TangemSdk/TangemSdk/Common/BIP39/Wordlist.swift rename to TangemSdk/TangemSdk/Crypto/BIP39/Wordlist.swift diff --git a/TangemSdk/TangemSdk/Common/BIP39/Wordlists/english.txt b/TangemSdk/TangemSdk/Crypto/BIP39/Wordlists/english.txt similarity index 100% rename from TangemSdk/TangemSdk/Common/BIP39/Wordlists/english.txt rename to TangemSdk/TangemSdk/Crypto/BIP39/Wordlists/english.txt diff --git a/TangemSdk/TangemSdk/Crypto/Base58.swift b/TangemSdk/TangemSdk/Crypto/Base58.swift new file mode 100644 index 000000000..04bea6208 --- /dev/null +++ b/TangemSdk/TangemSdk/Crypto/Base58.swift @@ -0,0 +1,181 @@ +// +// Base58.swift +// TangemSdk +// +// Created by Alex Vlasov. +// Copyright © 2018 Alex Vlasov. All rights reserved. + +import Foundation + +fileprivate enum Base58 { + private static let base58Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + + // Encode + static func base58FromBytes(_ bytes: [UInt8]) -> String { + var bytes = bytes + var zerosCount = 0 + var length = 0 + + for b in bytes { + if b != 0 { break } + zerosCount += 1 + } + + bytes.removeFirst(zerosCount) + + let size = bytes.count * 138 / 100 + 1 + + var base58: [UInt8] = Array(repeating: 0, count: size) + for b in bytes { + var carry = Int(b) + var i = 0 + + for j in 0...base58.count-1 where carry != 0 || i < length { + carry += 256 * Int(base58[base58.count - j - 1]) + base58[base58.count - j - 1] = UInt8(carry % 58) + carry /= 58 + i += 1 + } + + assert(carry == 0) + + length = i + } + + // skip leading zeros + var zerosToRemove = 0 + var str = "" + for b in base58 { + if b != 0 { break } + zerosToRemove += 1 + } + base58.removeFirst(zerosToRemove) + + while 0 < zerosCount { + str = "\(str)1" + zerosCount -= 1 + } + + for b in base58 { + str = "\(str)\(base58Alphabet[String.Index(utf16Offset: Int(b), in: base58Alphabet)])" + } + + return str + } + + // Decode + static func bytesFromBase58(_ base58: String) -> [UInt8] { + // remove leading and trailing whitespaces + let string = base58.trimmingCharacters(in: CharacterSet.whitespaces) + guard !string.isEmpty else { return [] } + + // count leading ASCII "1"'s [decodes directly to binary zero bytes] + var leadingZeros = 0 + for c in string { + if c != "1" { break } + leadingZeros += 1 + } + + // calculate the size of the decoded output, rounded up + let size = (string.lengthOfBytes(using: String.Encoding.utf8) - leadingZeros) * 733 / 1000 + 1 + + // allocate a buffer large enough for the decoded output + var base58: [UInt8] = Array(repeating: 0, count: size + leadingZeros) + + // decode what remains of the data + var length = 0 + for c in string where c != " " { + // search for base58 character + guard let base58Index = base58Alphabet.firstIndex(of: c) else { return [] } + + var carry = base58Index.utf16Offset(in: base58Alphabet) + var i = 0 + for j in 0...base58.count where carry != 0 || i < length { + carry += 58 * Int(base58[base58.count - j - 1]) + base58[base58.count - j - 1] = UInt8(carry % 256) + carry /= 256 + i += 1 + } + + assert(carry == 0) + length = i + } + + // calculate how many leading zero bytes we have + var totalZeros = 0 + for b in base58 { + if b != 0 { break } + totalZeros += 1 + } + // remove the excess zero bytes + base58.removeFirst(totalZeros - leadingZeros) + + return base58 + } +} + +// MARK: - Data+ + +extension Data { + public var base58EncodedString: String { + let bytes = Array(self) + return bytes.base58EncodedString + } + + @available(iOS 13.0, *) + public var base58CheckEncodedString: String { + let bytes = Array(self) + return bytes.base58CheckEncodedString + } +} + +// MARK: - Array+ + +extension Array where Element == UInt8 { + public var base58EncodedString: String { + guard !self.isEmpty else { return "" } + + return Base58.base58FromBytes(self) + } + + @available(iOS 13.0, *) + public var base58CheckEncodedString: String { + guard !self.isEmpty else { return "" } + + let checksum = self.getDoubleSha256().prefix(4) + let bytes = self + checksum + return Base58.base58FromBytes(bytes) + } +} + +// MARK: - String+ + +extension String { + public var base58DecodedData: Data { + let bytes = Base58.bytesFromBase58(self) + + return Data(bytes) + } + + @available(iOS 13.0, *) + public var base58CheckDecodedData: Data? { + guard let bytes = base58CheckDecodedBytes else { return nil } + + return Data(bytes) + } + + @available(iOS 13.0, *) + public var base58CheckDecodedBytes: [UInt8]? { + let bytes = Base58.bytesFromBase58(self) + + guard bytes.count >= 4 else { return nil } + + let checksum = Array(bytes.suffix(4)) + let bytesWithoutCheck = Array(bytes.dropLast(4)) + let calculatedChecksum = Array(bytesWithoutCheck.getDoubleSha256().prefix(4)) + + if checksum != calculatedChecksum { return nil } + + return bytesWithoutCheck + } +} diff --git a/TangemSdk/TangemSdk/Crypto/CryptoUtils.swift b/TangemSdk/TangemSdk/Crypto/CryptoUtils.swift index 548b662e0..486b847d3 100644 --- a/TangemSdk/TangemSdk/Crypto/CryptoUtils.swift +++ b/TangemSdk/TangemSdk/Crypto/CryptoUtils.swift @@ -62,6 +62,43 @@ public enum CryptoUtils { throw TangemSdkError.unsupportedCurve } } + + public static func isPrivateKeyValid(_ privateKey: Data, curve: EllipticCurve) throws -> Bool { + switch curve { + case .secp256k1: + return Secp256k1Utils().isPrivateKeyValid(privateKey) + case .ed25519: + let key = try? Curve25519.Signing.PrivateKey(rawRepresentation: privateKey) + return key != nil + case .secp256r1: + let key = try? P256.Signing.PrivateKey(rawRepresentation: privateKey) + return key != nil + case .bls12381_G2, .bls12381_G2_AUG, .bls12381_G2_POP: + // TODO: Add support for BLS keys. + throw TangemSdkError.unsupportedCurve + } + } + + public static func makePublicKey(from privateKey: Data, curve: EllipticCurve) throws -> Data { + switch curve { + case .secp256k1: + return try Secp256k1Utils().createPublicKey(privateKey: privateKey, compressed: true) + case .ed25519: + let key = try Curve25519.Signing.PrivateKey(rawRepresentation: privateKey) + return key.publicKey.rawRepresentation + case .secp256r1: + let key = try P256.Signing.PrivateKey(rawRepresentation: privateKey) + return key.publicKey.rawRepresentation + case .bls12381_G2, .bls12381_G2_AUG, .bls12381_G2_POP: + // TODO: Add support for BLS keys. + throw TangemSdkError.unsupportedCurve + } + } + + func createPublicKey(privateKey: Data) throws -> Data { + let key = try P256.Signing.PrivateKey(rawRepresentation: privateKey) + return key.publicKey.rawRepresentation + } /** * Helper function to verify that the data was signed with a private key that corresponds diff --git a/TangemSdk/TangemSdk/Common/HDWallet/BIP32/BIP32.swift b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/BIP32.swift similarity index 79% rename from TangemSdk/TangemSdk/Common/HDWallet/BIP32/BIP32.swift rename to TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/BIP32.swift index 72cb491aa..31ed0ce05 100644 --- a/TangemSdk/TangemSdk/Common/HDWallet/BIP32/BIP32.swift +++ b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/BIP32.swift @@ -27,26 +27,14 @@ struct BIP32 { let iL = Data(i.prefix(32)) let iR = Data(i.suffix(32)) - if !isPrivateKeyValid(iL, curve: curve) { + // Verify the key + // https://github.com/satoshilabs/slips/blob/master/slip-0010.md + if curve != .ed25519, !(try CryptoUtils.isPrivateKeyValid(iL, curve: curve)) { return try makeMasterKey(from: i, curve: curve) } return ExtendedPrivateKey(privateKey: iL, chainCode: iR) } - - // Verify the key - // https://github.com/satoshilabs/slips/blob/master/slip-0010.md - @available(iOS 13.0, *) - private func isPrivateKeyValid(_ privateKey: Data, curve: EllipticCurve) -> Bool { - switch curve { - case .secp256k1: - return Secp256k1Utils().isPrivateKeyValid(privateKey) - case .secp256r1: - return Secp256r1Utils().isPrivateKeyValid(privateKey) - default: - return true - } - } } extension BIP32 { diff --git a/TangemSdk/TangemSdk/Common/HDWallet/BIP32/DerivationNode.swift b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/DerivationNode.swift similarity index 100% rename from TangemSdk/TangemSdk/Common/HDWallet/BIP32/DerivationNode.swift rename to TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/DerivationNode.swift diff --git a/TangemSdk/TangemSdk/Common/HDWallet/BIP32/DerivationPath.swift b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/DerivationPath.swift similarity index 100% rename from TangemSdk/TangemSdk/Common/HDWallet/BIP32/DerivationPath.swift rename to TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/DerivationPath.swift diff --git a/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedKeySerialization/ExtendedKeySerializable.swift b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedKeySerialization/ExtendedKeySerializable.swift new file mode 100644 index 000000000..cbef23166 --- /dev/null +++ b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedKeySerialization/ExtendedKeySerializable.swift @@ -0,0 +1,16 @@ +// +// ExtendedKeySerializer.swift +// TangemSdk +// +// Created by Alexander Osokin on 13.01.2023. +// Copyright © 2023 Tangem AG. All rights reserved. +// + +import Foundation + +/// https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#serialization-format +@available(iOS 13.0, *) +protocol ExtendedKeySerializable { + init(from extendedKeyString: String, networkType: NetworkType) throws + func serialize(for networkType: NetworkType) throws -> String +} diff --git a/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedKeySerialization/ExtendedKeySerializationError.swift b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedKeySerialization/ExtendedKeySerializationError.swift new file mode 100644 index 000000000..4daa8c20c --- /dev/null +++ b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedKeySerialization/ExtendedKeySerializationError.swift @@ -0,0 +1,18 @@ +// +// ExtendedKeySerializationError.swift +// TangemSdk +// +// Created by Alexander Osokin on 16.01.2023. +// Copyright © 2023 Tangem AG. All rights reserved. +// + +import Foundation + +public enum ExtendedKeySerializationError: String, Error, LocalizedError { + case wrongLength + case decodingFailed + case wrongVersion + case wrongKey + + public var errorDescription: String? { rawValue } +} diff --git a/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedKeySerialization/ExtendedKeySerializer.swift b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedKeySerialization/ExtendedKeySerializer.swift new file mode 100644 index 000000000..8b91457da --- /dev/null +++ b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedKeySerialization/ExtendedKeySerializer.swift @@ -0,0 +1,34 @@ +// +// ExtendedKeySerializer.swift +// TangemSdk +// +// Created by Alexander Osokin on 10.03.2023. +// Copyright © 2023 Tangem AG. All rights reserved. +// + +import Foundation + +@available(iOS 13.0, *) +enum ExtendedKeySerializer { + enum Version { + case `public` + case `private` + + func getPrefix(for networkType: NetworkType) -> UInt32 { + switch (self, networkType) { + case (.public, .mainnet): + return 0x0488b21e + case (.public, .testnet): + return 0x043587cf + case (.private, .mainnet): + return 0x0488ADE4 + case (.private, .testnet): + return 0x04358394 + } + } + } + + enum Constants { + static let dataLength: Int = 78 + } +} diff --git a/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedPrivateKey.swift b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedPrivateKey.swift new file mode 100644 index 000000000..dd85494cc --- /dev/null +++ b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedPrivateKey.swift @@ -0,0 +1,126 @@ +// +// ExtendedPrivateKey.swift +// TangemSdk +// +// Created by Alexander Osokin on 07.03.2023. +// Copyright © 2023 Tangem AG. All rights reserved. +// + +import Foundation + +@available(iOS 13.0, *) +/// BIP32 extended private key +public struct ExtendedPrivateKey: Equatable, Hashable, JSONStringConvertible, Codable { + public let privateKey: Data + public let chainCode: Data + + public let depth: Int + public let parentFingerprint: Data + public let childNumber: UInt32 + + public init(privateKey: Data, chainCode: Data, depth: Int, parentFingerprint: Data, childNumber: UInt32) throws { + self.privateKey = privateKey + self.chainCode = chainCode + self.depth = depth + self.parentFingerprint = parentFingerprint + self.childNumber = childNumber + + if depth == 0 && (parentFingerprint.contains(where: { $0 != 0 }) || childNumber != 0) { + throw ExtendedKeySerializationError.wrongKey + } + } + + /// The master key + /// - Parameters: + /// - privateKey: privateKey + /// - chainCode: chainCode + public init(privateKey: Data, chainCode: Data) { + self.privateKey = privateKey + self.chainCode = chainCode + self.depth = 0 + self.parentFingerprint = Data(hexString: "0x00000000") + self.childNumber = 0 + } + + public func makePublicKey(for curve: EllipticCurve) throws -> ExtendedPublicKey { + let publicKey = try CryptoUtils.makePublicKey(from: privateKey, curve: curve) + + return try ExtendedPublicKey( + publicKey: publicKey, + chainCode: chainCode, + depth: depth, + parentFingerprint: parentFingerprint, + childNumber: childNumber + ) + } + + public func serializeToWIFCompressed(for networkType: NetworkType) -> String { + return WIF.encodeToWIFCompressed(privateKey, networkType: networkType) + } +} + +// MARK: - ExtendedKeySerializable + +@available(iOS 13.0, *) +extension ExtendedPrivateKey: ExtendedKeySerializable { + init(from extendedKeyString: String, networkType: NetworkType) throws { + guard let data = extendedKeyString.base58CheckDecodedData else { + throw ExtendedKeySerializationError.decodingFailed + } + + guard data.count == ExtendedKeySerializer.Constants.dataLength else { + throw ExtendedKeySerializationError.wrongLength + } + + let decodedVersion = UInt32(data.prefix(4).toInt()) + + let version = ExtendedKeySerializer.Version.private + + guard decodedVersion == version.getPrefix(for: networkType) else { + throw ExtendedKeySerializationError.wrongVersion + } + + let depth = data.dropFirst(4).prefix(1).toInt() + let parentFingerprint = data.dropFirst(5).prefix(4) + let childNumber = UInt32(data.dropFirst(9).prefix(4).toInt()) + let chainCode = data.dropFirst(13).prefix(32) + let privateKey = data.suffix(32) + let prefix = data.dropFirst(45).prefix(1) + + guard prefix == Data(UInt8(0)) else { + throw ExtendedKeySerializationError.decodingFailed + } + + guard Secp256k1Utils().isPrivateKeyValid(privateKey) else { + throw TangemSdkError.unsupportedCurve + } + + try self.init( + privateKey: privateKey, + chainCode: chainCode, + depth: depth, + parentFingerprint: parentFingerprint, + childNumber: childNumber + ) + } + + func serialize(for networkType: NetworkType) throws -> String { + var data = Data(capacity: ExtendedKeySerializer.Constants.dataLength) + + let version = ExtendedKeySerializer.Version.private + + data += version.getPrefix(for: networkType).bytes4 + data += depth.byte + data += parentFingerprint + data += childNumber.bytes4 + data += chainCode + data += Data(UInt8(0)) + privateKey + + guard data.count == ExtendedKeySerializer.Constants.dataLength else { + throw ExtendedKeySerializationError.wrongLength + } + + let resultString = Array(data).base58CheckEncodedString + return resultString + } +} diff --git a/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedPublicKey.swift b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedPublicKey.swift new file mode 100644 index 000000000..4790f1b92 --- /dev/null +++ b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedPublicKey.swift @@ -0,0 +1,155 @@ +// +// ExtendedPublicKey.swift +// TangemSdk +// +// Created by Alexander Osokin on 30.07.2021. +// Copyright © 2021 Tangem AG. All rights reserved. +// + +import Foundation +import CryptoKit + +@available(iOS 13.0, *) +/// BIP32 extended public key for `secp256k1`. +public struct ExtendedPublicKey: Equatable, Hashable, JSONStringConvertible, Codable { + /// Compressed `secp256k1` key + public let publicKey: Data + public let chainCode: Data + + public private(set) var depth: Int = 0 + public private(set) var parentFingerprint: Data = Data(hexString: "0x00000000") + public private(set) var childNumber: UInt32 = 0 + + public init(publicKey: Data, chainCode: Data, depth: Int, parentFingerprint: Data, childNumber: UInt32) throws { + self.depth = depth + self.parentFingerprint = parentFingerprint + self.childNumber = childNumber + self.chainCode = chainCode + self.publicKey = publicKey + + if depth == 0 && (parentFingerprint.contains(where: { $0 != 0 }) || childNumber != 0) { + throw ExtendedKeySerializationError.wrongKey + } + } + + /// The master key + /// - Parameters: + /// - publicKey: publicKey + /// - chainCode: chainCode + public init(publicKey: Data, chainCode: Data) { + self.publicKey = publicKey + self.chainCode = chainCode + } + + /// This function performs CKDpub((Kpar, cpar), i) → (Ki, ci) to compute a child extended public key from the parent extended public key. + /// It is only defined for non-hardened child keys. `secp256k1` only + public func derivePublicKey(node: DerivationNode) throws -> ExtendedPublicKey { + guard (try? Secp256k1Key(with: publicKey)) != nil else { + throw TangemSdkError.unsupportedCurve + } + + let index = node.index + + //We can derive only non-hardened keys + guard index < BIP32.Constants.hardenedOffset else { + throw HDWalletError.hardenedNotSupported + } + + //let I = HMAC-SHA512(Key = cpar, Data = serP(Kpar) || ser32(i)). + let data = publicKey + index.bytes4 + let hmac = HMAC.authenticationCode(for: data, using: SymmetricKey(data: chainCode)) + let digest = Data(hmac) + + let secp256k1 = Secp256k1Utils() + let ki = try secp256k1.createPublicKey(privateKey: digest[0..<32], compressed: true) + let derivedPublicKey = try secp256k1.sum(compressedPubKey1: ki, compressedPubKey2: publicKey) + let derivedChainCode = digest[32..<64] + + return try ExtendedPublicKey( + publicKey: derivedPublicKey, + chainCode: derivedChainCode, + depth: depth + 1, + parentFingerprint: publicKey.sha256Ripemd160.prefix(4), + childNumber: index + ) + } + + /// This function performs CKDpub((Kpar, cpar), i) → (Ki, ci) to compute a child extended public key from the parent extended public key. + /// It is only defined for non-hardened child keys. `secp256k1` only + public func derivePublicKey(path derivationPath: DerivationPath) throws -> ExtendedPublicKey { + var key: ExtendedPublicKey = self + + for node in derivationPath.nodes { + key = try key.derivePublicKey(node: node) + } + + return key + } +} + +// MARK: - ExtendedKeySerializable + +@available(iOS 13.0, *) +extension ExtendedPublicKey: ExtendedKeySerializable { + init(from extendedKeyString: String, networkType: NetworkType) throws { + guard let data = extendedKeyString.base58CheckDecodedData else { + throw ExtendedKeySerializationError.decodingFailed + } + + guard data.count == ExtendedKeySerializer.Constants.dataLength else { + throw ExtendedKeySerializationError.wrongLength + } + + let decodedVersion = UInt32(data.prefix(4).toInt()) + + let version = ExtendedKeySerializer.Version.public + + guard decodedVersion == version.getPrefix(for: networkType) else { + throw ExtendedKeySerializationError.wrongVersion + } + + let depth = data.dropFirst(4).prefix(1).toInt() + let parentFingerprint = data.dropFirst(5).prefix(4) + let childNumber = UInt32(data.dropFirst(9).prefix(4).toInt()) + let chainCode = data.dropFirst(13).prefix(32) + let compressedKey = data.suffix(33) + + guard let _ = try? Secp256k1Key(with: compressedKey) else { + throw TangemSdkError.unsupportedCurve + } + + try self.init( + publicKey: compressedKey, + chainCode: chainCode, + depth: depth, + parentFingerprint: parentFingerprint, + childNumber: childNumber + ) + } + + func serialize(for networkType: NetworkType) throws -> String { + guard let secpKey = try? Secp256k1Key(with: publicKey) else { + throw TangemSdkError.unsupportedCurve + } + + let compressedKey = try secpKey.compress() + + var data = Data(capacity: ExtendedKeySerializer.Constants.dataLength) + + let version = ExtendedKeySerializer.Version.public + + data += version.getPrefix(for: networkType).bytes4 + data += depth.byte + data += parentFingerprint + data += childNumber.bytes4 + data += chainCode + data += compressedKey + + guard data.count == ExtendedKeySerializer.Constants.dataLength else { + throw ExtendedKeySerializationError.wrongLength + } + + let resultString = Array(data).base58CheckEncodedString + return resultString + } +} diff --git a/TangemSdk/TangemSdk/Common/HDWallet/BIP44.swift b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP44.swift similarity index 100% rename from TangemSdk/TangemSdk/Common/HDWallet/BIP44.swift rename to TangemSdk/TangemSdk/Crypto/HDWallet/BIP44.swift diff --git a/TangemSdk/TangemSdk/Common/HDWallet/HDWalletError.swift b/TangemSdk/TangemSdk/Crypto/HDWallet/HDWalletError.swift similarity index 100% rename from TangemSdk/TangemSdk/Common/HDWallet/HDWalletError.swift rename to TangemSdk/TangemSdk/Crypto/HDWallet/HDWalletError.swift diff --git a/TangemSdk/TangemSdk/Crypto/NetworkType.swift b/TangemSdk/TangemSdk/Crypto/NetworkType.swift new file mode 100644 index 000000000..e2508b2d5 --- /dev/null +++ b/TangemSdk/TangemSdk/Crypto/NetworkType.swift @@ -0,0 +1,15 @@ +// +// NetworkType.swift +// TangemSdk +// +// Created by Alexander Osokin on 10.03.2023. +// Copyright © 2023 Tangem AG. All rights reserved. +// + +import Foundation + +@available(iOS 13.0, *) +public enum NetworkType { + case mainnet + case testnet +} diff --git a/TangemSdk/TangemSdk/Crypto/Ripemd160.swift b/TangemSdk/TangemSdk/Crypto/Ripemd160.swift new file mode 100644 index 000000000..9f2403ffb --- /dev/null +++ b/TangemSdk/TangemSdk/Crypto/Ripemd160.swift @@ -0,0 +1,396 @@ +// +// Ripemd160.swift +// RIPE Message Digest 160 Swift implementation +// +// Copyright (c) 2022 Miclaus Industries Corporation B.V. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +#if canImport(Foundation) +import Foundation + +/// RIPE Message Digest 160 Swift implementation +public struct RIPEMD160 { + // MARK: Internal Variables + /// Message Digest buffer + private var MDbuffer: (UInt32, UInt32, UInt32, UInt32, UInt32) + + /// Data buffer + private var buffer: Data + + /// Amount of bytes processed + private var count: Int64 + + // MARK: - Public Functions + /// Initialize a new instance of `RIPEMD160` + public init() { + MDbuffer = (0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0) + buffer = Data() + count = 0 + } + + /// Incrementally update the hash function with the given data. + /// - Parameter data: Input bytes + public mutating func update(data: Data) { + var X = [UInt32](repeating: 0, count: 16) + var pos = data.startIndex + var length = data.count + + // Process remaining bytes from last call: + if buffer.count > 0 && buffer.count + length >= 64 { + let amount = 64 - buffer.count + buffer.append(data[..= 64 { + X.withUnsafeMutableBytes { + _ = data[pos.. Data { + var X = [UInt32](repeating: 0, count: 16) + /* append the bit m_n == 1 */ + buffer.append(0x80) + X.withUnsafeMutableBytes { + _ = buffer.copyBytes(to: $0) + } + + if (count & 63) > 55 { + /* length goes to next block */ + compress(X) + X = [UInt32](repeating: 0, count: 16) + } + + /* append length in bits */ + let lswlen = UInt32(truncatingIfNeeded: count) + let mswlen = UInt32(UInt64(count) >> 32) + X[14] = lswlen << 3 + X[15] = (lswlen >> 29) | (mswlen << 3) + compress(X) + + buffer = Data() + let result = [MDbuffer.0, MDbuffer.1, MDbuffer.2, MDbuffer.3, MDbuffer.4] + return result.withUnsafeBytes { Data($0) } + } + + // MARK: - Internal Implementation + /// Helper functions (originally macros in rmd160.h) + /// - Parameter X: `UnsafePointer` + private mutating func compress(_ X: UnsafePointer) { + + /** ROL(x, n) cyclically rotates x over n bits to the left */ + /** x must be of an unsigned 32 bits type and 0 <= n < 32. */ + func ROL(_ x: UInt32, _ n: UInt32) -> UInt32 { + return (x << n) | ( x >> (32 - n)) + } + + /* the five basic functions F(), G() and H() */ + func F(_ x: UInt32, _ y: UInt32, _ z: UInt32) -> UInt32 { + return x ^ y ^ z + } + + func G(_ x: UInt32, _ y: UInt32, _ z: UInt32) -> UInt32 { + return (x & y) | (~x & z) + } + + func H(_ x: UInt32, _ y: UInt32, _ z: UInt32) -> UInt32 { + return (x | ~y) ^ z + } + + func I(_ x: UInt32, _ y: UInt32, _ z: UInt32) -> UInt32 { + return (x & z) | (y & ~z) + } + + func J(_ x: UInt32, _ y: UInt32, _ z: UInt32) -> UInt32 { + return x ^ (y | ~z) + } + + /* the ten basic operations FF() through III() */ + func FF(_ a: inout UInt32, _ b: UInt32, _ c: inout UInt32, _ d: UInt32, _ e: UInt32, _ x: UInt32, _ s: UInt32) { + a = a &+ F(b, c, d) &+ x + a = ROL(a, s) &+ e + c = ROL(c, 10) + } + + func GG(_ a: inout UInt32, _ b: UInt32, _ c: inout UInt32, _ d: UInt32, _ e: UInt32, _ x: UInt32, _ s: UInt32) { + a = a &+ G(b, c, d) &+ x &+ 0x5a827999 + a = ROL(a, s) &+ e + c = ROL(c, 10) + } + + func HH(_ a: inout UInt32, _ b: UInt32, _ c: inout UInt32, _ d: UInt32, _ e: UInt32, _ x: UInt32, _ s: UInt32) { + a = a &+ H(b, c, d) &+ x &+ 0x6ed9eba1 + a = ROL(a, s) &+ e + c = ROL(c, 10) + } + + func II(_ a: inout UInt32, _ b: UInt32, _ c: inout UInt32, _ d: UInt32, _ e: UInt32, _ x: UInt32, _ s: UInt32) { + a = a &+ I(b, c, d) &+ x &+ 0x8f1bbcdc + a = ROL(a, s) &+ e + c = ROL(c, 10) + } + + func JJ(_ a: inout UInt32, _ b: UInt32, _ c: inout UInt32, _ d: UInt32, _ e: UInt32, _ x: UInt32, _ s: UInt32) { + a = a &+ J(b, c, d) &+ x &+ 0xa953fd4e + a = ROL(a, s) &+ e + c = ROL(c, 10) + } + + func FFF(_ a: inout UInt32, _ b: UInt32, _ c: inout UInt32, _ d: UInt32, _ e: UInt32, _ x: UInt32, _ s: UInt32) { + a = a &+ F(b, c, d) &+ x + a = ROL(a, s) &+ e + c = ROL(c, 10) + } + + func GGG(_ a: inout UInt32, _ b: UInt32, _ c: inout UInt32, _ d: UInt32, _ e: UInt32, _ x: UInt32, _ s: UInt32) { + a = a &+ G(b, c, d) &+ x &+ 0x7a6d76e9 + a = ROL(a, s) &+ e + c = ROL(c, 10) + } + + func HHH(_ a: inout UInt32, _ b: UInt32, _ c: inout UInt32, _ d: UInt32, _ e: UInt32, _ x: UInt32, _ s: UInt32) { + a = a &+ H(b, c, d) &+ x &+ 0x6d703ef3 + a = ROL(a, s) &+ e + c = ROL(c, 10) + } + + func III(_ a: inout UInt32, _ b: UInt32, _ c: inout UInt32, _ d: UInt32, _ e: UInt32, _ x: UInt32, _ s: UInt32) { + a = a &+ I(b, c, d) &+ x &+ 0x5c4dd124 + a = ROL(a, s) &+ e + c = ROL(c, 10) + } + + func JJJ(_ a: inout UInt32, _ b: UInt32, _ c: inout UInt32, _ d: UInt32, _ e: UInt32, _ x: UInt32, _ s: UInt32) { + a = a &+ J(b, c, d) &+ x &+ 0x50a28be6 + a = ROL(a, s) &+ e + c = ROL(c, 10) + } + + /* The hashing function starts here */ + var (aa, bb, cc, dd, ee) = MDbuffer + var (aaa, bbb, ccc, ddd, eee) = MDbuffer + + /* Round 1 */ + FF(&aa, bb, &cc, dd, ee, X[ 0], 11) + FF(&ee, aa, &bb, cc, dd, X[ 1], 14) + FF(&dd, ee, &aa, bb, cc, X[ 2], 15) + FF(&cc, dd, &ee, aa, bb, X[ 3], 12) + FF(&bb, cc, &dd, ee, aa, X[ 4], 5) + FF(&aa, bb, &cc, dd, ee, X[ 5], 8) + FF(&ee, aa, &bb, cc, dd, X[ 6], 7) + FF(&dd, ee, &aa, bb, cc, X[ 7], 9) + FF(&cc, dd, &ee, aa, bb, X[ 8], 11) + FF(&bb, cc, &dd, ee, aa, X[ 9], 13) + FF(&aa, bb, &cc, dd, ee, X[10], 14) + FF(&ee, aa, &bb, cc, dd, X[11], 15) + FF(&dd, ee, &aa, bb, cc, X[12], 6) + FF(&cc, dd, &ee, aa, bb, X[13], 7) + FF(&bb, cc, &dd, ee, aa, X[14], 9) + FF(&aa, bb, &cc, dd, ee, X[15], 8) + + /* Round 2 */ + GG(&ee, aa, &bb, cc, dd, X[ 7], 7) + GG(&dd, ee, &aa, bb, cc, X[ 4], 6) + GG(&cc, dd, &ee, aa, bb, X[13], 8) + GG(&bb, cc, &dd, ee, aa, X[ 1], 13) + GG(&aa, bb, &cc, dd, ee, X[10], 11) + GG(&ee, aa, &bb, cc, dd, X[ 6], 9) + GG(&dd, ee, &aa, bb, cc, X[15], 7) + GG(&cc, dd, &ee, aa, bb, X[ 3], 15) + GG(&bb, cc, &dd, ee, aa, X[12], 7) + GG(&aa, bb, &cc, dd, ee, X[ 0], 12) + GG(&ee, aa, &bb, cc, dd, X[ 9], 15) + GG(&dd, ee, &aa, bb, cc, X[ 5], 9) + GG(&cc, dd, &ee, aa, bb, X[ 2], 11) + GG(&bb, cc, &dd, ee, aa, X[14], 7) + GG(&aa, bb, &cc, dd, ee, X[11], 13) + GG(&ee, aa, &bb, cc, dd, X[ 8], 12) + + /* Round 3 */ + HH(&dd, ee, &aa, bb, cc, X[ 3], 11) + HH(&cc, dd, &ee, aa, bb, X[10], 13) + HH(&bb, cc, &dd, ee, aa, X[14], 6) + HH(&aa, bb, &cc, dd, ee, X[ 4], 7) + HH(&ee, aa, &bb, cc, dd, X[ 9], 14) + HH(&dd, ee, &aa, bb, cc, X[15], 9) + HH(&cc, dd, &ee, aa, bb, X[ 8], 13) + HH(&bb, cc, &dd, ee, aa, X[ 1], 15) + HH(&aa, bb, &cc, dd, ee, X[ 2], 14) + HH(&ee, aa, &bb, cc, dd, X[ 7], 8) + HH(&dd, ee, &aa, bb, cc, X[ 0], 13) + HH(&cc, dd, &ee, aa, bb, X[ 6], 6) + HH(&bb, cc, &dd, ee, aa, X[13], 5) + HH(&aa, bb, &cc, dd, ee, X[11], 12) + HH(&ee, aa, &bb, cc, dd, X[ 5], 7) + HH(&dd, ee, &aa, bb, cc, X[12], 5) + + /* Round 4 */ + II(&cc, dd, &ee, aa, bb, X[ 1], 11) + II(&bb, cc, &dd, ee, aa, X[ 9], 12) + II(&aa, bb, &cc, dd, ee, X[11], 14) + II(&ee, aa, &bb, cc, dd, X[10], 15) + II(&dd, ee, &aa, bb, cc, X[ 0], 14) + II(&cc, dd, &ee, aa, bb, X[ 8], 15) + II(&bb, cc, &dd, ee, aa, X[12], 9) + II(&aa, bb, &cc, dd, ee, X[ 4], 8) + II(&ee, aa, &bb, cc, dd, X[13], 9) + II(&dd, ee, &aa, bb, cc, X[ 3], 14) + II(&cc, dd, &ee, aa, bb, X[ 7], 5) + II(&bb, cc, &dd, ee, aa, X[15], 6) + II(&aa, bb, &cc, dd, ee, X[14], 8) + II(&ee, aa, &bb, cc, dd, X[ 5], 6) + II(&dd, ee, &aa, bb, cc, X[ 6], 5) + II(&cc, dd, &ee, aa, bb, X[ 2], 12) + + /* Round 5 */ + JJ(&bb, cc, &dd, ee, aa, X[ 4], 9) + JJ(&aa, bb, &cc, dd, ee, X[ 0], 15) + JJ(&ee, aa, &bb, cc, dd, X[ 5], 5) + JJ(&dd, ee, &aa, bb, cc, X[ 9], 11) + JJ(&cc, dd, &ee, aa, bb, X[ 7], 6) + JJ(&bb, cc, &dd, ee, aa, X[12], 8) + JJ(&aa, bb, &cc, dd, ee, X[ 2], 13) + JJ(&ee, aa, &bb, cc, dd, X[10], 12) + JJ(&dd, ee, &aa, bb, cc, X[14], 5) + JJ(&cc, dd, &ee, aa, bb, X[ 1], 12) + JJ(&bb, cc, &dd, ee, aa, X[ 3], 13) + JJ(&aa, bb, &cc, dd, ee, X[ 8], 14) + JJ(&ee, aa, &bb, cc, dd, X[11], 11) + JJ(&dd, ee, &aa, bb, cc, X[ 6], 8) + JJ(&cc, dd, &ee, aa, bb, X[15], 5) + JJ(&bb, cc, &dd, ee, aa, X[13], 6) + + /* Parallel round 1 */ + JJJ(&aaa, bbb, &ccc, ddd, eee, X[ 5], 8) + JJJ(&eee, aaa, &bbb, ccc, ddd, X[14], 9) + JJJ(&ddd, eee, &aaa, bbb, ccc, X[ 7], 9) + JJJ(&ccc, ddd, &eee, aaa, bbb, X[ 0], 11) + JJJ(&bbb, ccc, &ddd, eee, aaa, X[ 9], 13) + JJJ(&aaa, bbb, &ccc, ddd, eee, X[ 2], 15) + JJJ(&eee, aaa, &bbb, ccc, ddd, X[11], 15) + JJJ(&ddd, eee, &aaa, bbb, ccc, X[ 4], 5) + JJJ(&ccc, ddd, &eee, aaa, bbb, X[13], 7) + JJJ(&bbb, ccc, &ddd, eee, aaa, X[ 6], 7) + JJJ(&aaa, bbb, &ccc, ddd, eee, X[15], 8) + JJJ(&eee, aaa, &bbb, ccc, ddd, X[ 8], 11) + JJJ(&ddd, eee, &aaa, bbb, ccc, X[ 1], 14) + JJJ(&ccc, ddd, &eee, aaa, bbb, X[10], 14) + JJJ(&bbb, ccc, &ddd, eee, aaa, X[ 3], 12) + JJJ(&aaa, bbb, &ccc, ddd, eee, X[12], 6) + + /* Parallel round 2 */ + III(&eee, aaa, &bbb, ccc, ddd, X[ 6], 9) + III(&ddd, eee, &aaa, bbb, ccc, X[11], 13) + III(&ccc, ddd, &eee, aaa, bbb, X[ 3], 15) + III(&bbb, ccc, &ddd, eee, aaa, X[ 7], 7) + III(&aaa, bbb, &ccc, ddd, eee, X[ 0], 12) + III(&eee, aaa, &bbb, ccc, ddd, X[13], 8) + III(&ddd, eee, &aaa, bbb, ccc, X[ 5], 9) + III(&ccc, ddd, &eee, aaa, bbb, X[10], 11) + III(&bbb, ccc, &ddd, eee, aaa, X[14], 7) + III(&aaa, bbb, &ccc, ddd, eee, X[15], 7) + III(&eee, aaa, &bbb, ccc, ddd, X[ 8], 12) + III(&ddd, eee, &aaa, bbb, ccc, X[12], 7) + III(&ccc, ddd, &eee, aaa, bbb, X[ 4], 6) + III(&bbb, ccc, &ddd, eee, aaa, X[ 9], 15) + III(&aaa, bbb, &ccc, ddd, eee, X[ 1], 13) + III(&eee, aaa, &bbb, ccc, ddd, X[ 2], 11) + + /* Parallel round 3 */ + HHH(&ddd, eee, &aaa, bbb, ccc, X[15], 9) + HHH(&ccc, ddd, &eee, aaa, bbb, X[ 5], 7) + HHH(&bbb, ccc, &ddd, eee, aaa, X[ 1], 15) + HHH(&aaa, bbb, &ccc, ddd, eee, X[ 3], 11) + HHH(&eee, aaa, &bbb, ccc, ddd, X[ 7], 8) + HHH(&ddd, eee, &aaa, bbb, ccc, X[14], 6) + HHH(&ccc, ddd, &eee, aaa, bbb, X[ 6], 6) + HHH(&bbb, ccc, &ddd, eee, aaa, X[ 9], 14) + HHH(&aaa, bbb, &ccc, ddd, eee, X[11], 12) + HHH(&eee, aaa, &bbb, ccc, ddd, X[ 8], 13) + HHH(&ddd, eee, &aaa, bbb, ccc, X[12], 5) + HHH(&ccc, ddd, &eee, aaa, bbb, X[ 2], 14) + HHH(&bbb, ccc, &ddd, eee, aaa, X[10], 13) + HHH(&aaa, bbb, &ccc, ddd, eee, X[ 0], 13) + HHH(&eee, aaa, &bbb, ccc, ddd, X[ 4], 7) + HHH(&ddd, eee, &aaa, bbb, ccc, X[13], 5) + + /* Parallel round 4 */ + GGG(&ccc, ddd, &eee, aaa, bbb, X[ 8], 15) + GGG(&bbb, ccc, &ddd, eee, aaa, X[ 6], 5) + GGG(&aaa, bbb, &ccc, ddd, eee, X[ 4], 8) + GGG(&eee, aaa, &bbb, ccc, ddd, X[ 1], 11) + GGG(&ddd, eee, &aaa, bbb, ccc, X[ 3], 14) + GGG(&ccc, ddd, &eee, aaa, bbb, X[11], 14) + GGG(&bbb, ccc, &ddd, eee, aaa, X[15], 6) + GGG(&aaa, bbb, &ccc, ddd, eee, X[ 0], 14) + GGG(&eee, aaa, &bbb, ccc, ddd, X[ 5], 6) + GGG(&ddd, eee, &aaa, bbb, ccc, X[12], 9) + GGG(&ccc, ddd, &eee, aaa, bbb, X[ 2], 12) + GGG(&bbb, ccc, &ddd, eee, aaa, X[13], 9) + GGG(&aaa, bbb, &ccc, ddd, eee, X[ 9], 12) + GGG(&eee, aaa, &bbb, ccc, ddd, X[ 7], 5) + GGG(&ddd, eee, &aaa, bbb, ccc, X[10], 15) + GGG(&ccc, ddd, &eee, aaa, bbb, X[14], 8) + + /* Parallel round 5 */ + FFF(&bbb, ccc, &ddd, eee, aaa, X[12] , 8) + FFF(&aaa, bbb, &ccc, ddd, eee, X[15] , 5) + FFF(&eee, aaa, &bbb, ccc, ddd, X[10] , 12) + FFF(&ddd, eee, &aaa, bbb, ccc, X[ 4] , 9) + FFF(&ccc, ddd, &eee, aaa, bbb, X[ 1] , 12) + FFF(&bbb, ccc, &ddd, eee, aaa, X[ 5] , 5) + FFF(&aaa, bbb, &ccc, ddd, eee, X[ 8] , 14) + FFF(&eee, aaa, &bbb, ccc, ddd, X[ 7] , 6) + FFF(&ddd, eee, &aaa, bbb, ccc, X[ 6] , 8) + FFF(&ccc, ddd, &eee, aaa, bbb, X[ 2] , 13) + FFF(&bbb, ccc, &ddd, eee, aaa, X[13] , 6) + FFF(&aaa, bbb, &ccc, ddd, eee, X[14] , 5) + FFF(&eee, aaa, &bbb, ccc, ddd, X[ 0] , 15) + FFF(&ddd, eee, &aaa, bbb, ccc, X[ 3] , 13) + FFF(&ccc, ddd, &eee, aaa, bbb, X[ 9] , 11) + FFF(&bbb, ccc, &ddd, eee, aaa, X[11] , 11) + + /* Combine results */ + MDbuffer = (MDbuffer.1 &+ cc &+ ddd, + MDbuffer.2 &+ dd &+ eee, + MDbuffer.3 &+ ee &+ aaa, + MDbuffer.4 &+ aa &+ bbb, + MDbuffer.0 &+ bb &+ ccc) + } +} +#endif diff --git a/TangemSdk/TangemSdk/Crypto/Secp256r1Utils.swift b/TangemSdk/TangemSdk/Crypto/Secp256r1Utils.swift deleted file mode 100644 index 9db25bf1d..000000000 --- a/TangemSdk/TangemSdk/Crypto/Secp256r1Utils.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Secp256r1Utils.swift -// TangemSdk -// -// Created by Alexander Osokin on 09.03.2023. -// Copyright © 2023 Tangem AG. All rights reserved. -// - -import Foundation -import CryptoKit - -@available(iOS 13.0, *) -class Secp256r1Utils { - func isPrivateKeyValid(_ privateKey: Data) -> Bool { - let key = try? P256.Signing.PrivateKey(rawRepresentation: privateKey) - return key != nil - } -} diff --git a/TangemSdk/TangemSdk/Crypto/WIF.swift b/TangemSdk/TangemSdk/Crypto/WIF.swift new file mode 100644 index 000000000..717503645 --- /dev/null +++ b/TangemSdk/TangemSdk/Crypto/WIF.swift @@ -0,0 +1,54 @@ +// +// WIF.swift +// TangemSdk +// +// Created by Alexander Osokin on 10.03.2023. +// Copyright © 2023 Tangem AG. All rights reserved. +// + +import Foundation + +@available(iOS 13.0, *) +public enum WIF { + public static func encodeToWIFCompressed(_ privateKey: Data, networkType: NetworkType) -> String { + let extended = networkType.prefix + privateKey + Constants.compressedSuffix + return extended.base58CheckEncodedString + } + + public static func decodeWIFCompressed(_ string: String) -> Data? { + guard let decoded = string.base58CheckDecodedData else { return nil } + + var data = decoded.dropFirst() + + if !string.starts(with: Constants.uncompressedMainnetPrefix) + && !string.starts(with: Constants.uncompressedTestnetPrefix), + let lastByte = data.last, Data(lastByte) == Constants.compressedSuffix { + data = data.dropLast() // remove compressedSuffix + } + + return data + } +} + +@available(iOS 13.0, *) +fileprivate extension WIF { + enum Constants { + static let prefixMainnet = Data(hexString: "0x80") + static let prefixTestnet = Data(hexString: "0xEF") + static let compressedSuffix = Data(hexString: "0x01") + static let uncompressedMainnetPrefix = "5" + static let uncompressedTestnetPrefix = "9" + } +} + +@available(iOS 13.0, *) +fileprivate extension NetworkType { + var prefix: Data { + switch self { + case .mainnet: + return WIF.Constants.prefixMainnet + case .testnet: + return WIF.Constants.prefixTestnet + } + } +} diff --git a/TangemSdk/TangemSdkTests/BIP32Tests.swift b/TangemSdk/TangemSdkTests/BIP32Tests.swift index 02e57638e..d24ca61c0 100644 --- a/TangemSdk/TangemSdkTests/BIP32Tests.swift +++ b/TangemSdk/TangemSdkTests/BIP32Tests.swift @@ -51,8 +51,85 @@ class BIP32Tests: XCTestCase { XCTAssertEqual(masterKey.privateKey.hexString.lowercased(), "3b8c18469a4634517d6d0b65448f8e6c62091b45540a1743c5846be55d47d88f".lowercased()) XCTAssertEqual(masterKey.chainCode.hexString.lowercased(), "7762f9729fed06121fd13f326884c82f59aa95c57ac492ce8c9654e60efd130c".lowercased()) } -} -@available(iOS 13.0, *) -private extension BIP32Tests { + func testMetaMaskTWCompatible() throws { + let mnemonicPhrase = "scale wave venue cloth fruit empower afford one domain blouse romance artist" + let mnemonic = try Mnemonic(with: mnemonicPhrase) + let seed = try mnemonic.generateSeed() + XCTAssertEqual(seed.hexString.lowercased(), "d3eea633215dc4cb8ec2acd0d413adec1ebccb597ecf279886e584e9cb9ceb0788eb6f17a585acc12bc58fd586df6bbbdf39af955656f24215cceab174344e62") + + let extendedPrivateKey = try BIP32().makeMasterKey(from: seed, curve: .secp256k1) + + let pk = extendedPrivateKey.privateKey.hexString.lowercased() + XCTAssertEqual(pk, "589aeb596710f33d7ac31598ec10440a7df8808cf2c3d69ba670ff3fae66aafb") + + XCTAssertEqual(extendedPrivateKey.serializeToWIFCompressed(for: .mainnet), "KzBwvPW6L5iwJSiE5vgS52Y69bUxfwizW3wF4C4Xa3ba3pdd7j63") + } + + // https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#user-content-Test_Vectors + func testMasterVector1() throws { + let seed = Data(hexString: "000102030405060708090a0b0c0d0e0f") + let bip32 = BIP32() + + let mPriv = try bip32.makeMasterKey(from: seed, curve: .secp256k1) + let mPub = try mPriv.makePublicKey(for: .secp256k1) + + let xpriv = try mPriv.serialize(for: .mainnet) + XCTAssertEqual(xpriv, "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi") + + let xpub = try mPub.serialize(for: .mainnet) + XCTAssertEqual(xpub, "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8") + } + + // https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#user-content-Test_Vectors + func testMasterVector2() throws { + let seed = Data(hexString: "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542") + let bip32 = BIP32() + + let mPriv = try bip32.makeMasterKey(from: seed, curve: .secp256k1) + let mPub = try mPriv.makePublicKey(for: .secp256k1) + + let xpriv = try mPriv.serialize(for: .mainnet) + XCTAssertEqual(xpriv, "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U") + + let xpub = try mPub.serialize(for: .mainnet) + XCTAssertEqual(xpub, "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB") + + + // Chain m/0 + let derivedPub = try mPub.derivePublicKey(node: .nonHardened(0)) + let derivedXPub = try derivedPub.serialize(for: .mainnet) + // ext prv: xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt + XCTAssertEqual(derivedXPub, "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH") + } + + // https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#user-content-Test_Vectors + func testMasterVector3() throws { + let seed = Data(hexString: "4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be") + let bip32 = BIP32() + + let mPriv = try bip32.makeMasterKey(from: seed, curve: .secp256k1) + let mPub = try mPriv.makePublicKey(for: .secp256k1) + + let xpriv = try mPriv.serialize(for: .mainnet) + XCTAssertEqual(xpriv, "xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6") + + let xpub = try mPub.serialize(for: .mainnet) + XCTAssertEqual(xpub, "xpub661MyMwAqRbcEZVB4dScxMAdx6d4nFc9nvyvH3v4gJL378CSRZiYmhRoP7mBy6gSPSCYk6SzXPTf3ND1cZAceL7SfJ1Z3GC8vBgp2epUt13") + } + + // https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#user-content-Test_Vectors + func testMasterVector4() throws { + let seed = Data(hexString: "3ddd5602285899a946114506157c7997e5444528f3003f6134712147db19b678") + let bip32 = BIP32() + + let mPriv = try bip32.makeMasterKey(from: seed, curve: .secp256k1) + let mPub = try mPriv.makePublicKey(for: .secp256k1) + + let xpriv = try mPriv.serialize(for: .mainnet) + XCTAssertEqual(xpriv, "xprv9s21ZrQH143K48vGoLGRPxgo2JNkJ3J3fqkirQC2zVdk5Dgd5w14S7fRDyHH4dWNHUgkvsvNDCkvAwcSHNAQwhwgNMgZhLtQC63zxwhQmRv") + + let xpub = try mPub.serialize(for: .mainnet) + XCTAssertEqual(xpub, "xpub661MyMwAqRbcGczjuMoRm6dXaLDEhW1u34gKenbeYqAix21mdUKJyuyu5F1rzYGVxyL6tmgBUAEPrEz92mBXjByMRiJdba9wpnN37RLLAXa") + } } diff --git a/TangemSdk/TangemSdkTests/BIP39Tests.swift b/TangemSdk/TangemSdkTests/BIP39Tests.swift index 18c2722d7..40caf0845 100644 --- a/TangemSdk/TangemSdkTests/BIP39Tests.swift +++ b/TangemSdk/TangemSdkTests/BIP39Tests.swift @@ -47,6 +47,7 @@ class BIP39Tests: XCTestCase { let entropy = vector[0] let expectedMnemonic = vector[1] let expectedSeed = vector[2] + let expectedExtendedKey = vector[3] let mnemonic = try bip39.generateMnemonic(from: Data(hexString: entropy), wordlist: .en) let mnemonicString = bip39.convertToMnemonicString(mnemonic) @@ -54,6 +55,10 @@ class BIP39Tests: XCTestCase { let seed = try bip39.generateSeed(from: mnemonic, passphrase: Constants.passphrase) XCTAssertEqual(seed.hexString.lowercased(), expectedSeed) + + let key = try BIP32().makeMasterKey(from: seed, curve: .secp256k1) + let extendedKey = try key.serialize(for: .mainnet) + XCTAssertEqual(extendedKey, expectedExtendedKey) } } diff --git a/TangemSdk/TangemSdkTests/CryptoUtilsTests.swift b/TangemSdk/TangemSdkTests/CryptoUtilsTests.swift index 2897c80df..15e9d6ca1 100644 --- a/TangemSdk/TangemSdkTests/CryptoUtilsTests.swift +++ b/TangemSdk/TangemSdkTests/CryptoUtilsTests.swift @@ -160,16 +160,11 @@ class CryptoUtilsTests: XCTestCase { XCTAssertFalse(utils.isPrivateKeyValid(Data(repeating: UInt8(0), count: 32))) } - func testSecp256r1PrivateKeyValidation() { - let utils = Secp256r1Utils() - - XCTAssertFalse(utils.isPrivateKeyValid(Data())) - XCTAssertFalse(utils.isPrivateKeyValid(Data(repeating: UInt8(0), count: 32))) - XCTAssertFalse(utils.isPrivateKeyValid(Data(hexString: "FFFFFFFFFE92BF972115EB5008573E60811CA5A79B40EAAF9036189360F47413"))) - XCTAssertFalse(utils.isPrivateKeyValid(Data(hexString: "FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC4FC632551"))) - XCTAssertTrue(utils.isPrivateKeyValid(Data(hexString: "FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632550"))) + func testSecp256r1PrivateKeyValidation() throws { + XCTAssertFalse(try CryptoUtils.isPrivateKeyValid(Data(), curve: .secp256r1)) + XCTAssertFalse(try CryptoUtils.isPrivateKeyValid(Data(repeating: UInt8(0), count: 32), curve: .secp256r1)) + XCTAssertFalse(try CryptoUtils.isPrivateKeyValid(Data(hexString: "FFFFFFFFFE92BF972115EB5008573E60811CA5A79B40EAAF9036189360F47413"), curve: .secp256r1)) + XCTAssertFalse(try CryptoUtils.isPrivateKeyValid(Data(hexString: "FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC4FC632551"), curve: .secp256r1)) + XCTAssertTrue(try CryptoUtils.isPrivateKeyValid(Data(hexString: "FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632550"), curve: .secp256r1)) } } - - - diff --git a/TangemSdk/TangemSdkTests/ExtendedKeyTests.swift b/TangemSdk/TangemSdkTests/ExtendedKeyTests.swift new file mode 100644 index 000000000..0bfac678b --- /dev/null +++ b/TangemSdk/TangemSdkTests/ExtendedKeyTests.swift @@ -0,0 +1,136 @@ +// +// ExtendedKeyTests.swift.swift +// TangemSdkTests +// +// Created by Alexander Osokin on 13.01.2023. +// Copyright © 2023 Tangem AG. All rights reserved. +// + +import XCTest +import Foundation +@testable import TangemSdk + +@available(iOS 13.0, *) +class ExtendedKeyTests: XCTestCase { + func testRoundTripPub() throws { + let key = try ExtendedPublicKey( + publicKey: Data(hexString: "0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2"), + chainCode: Data(hexString: "873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508"), + depth: 3, + parentFingerprint: Data(hexString: "0x00000000"), + childNumber: 2147483648 + ) + + let xpubString = try key.serialize(for: .mainnet) + let deserializedKey = try ExtendedPublicKey(from: xpubString, networkType: .mainnet) + + XCTAssertEqual(key, deserializedKey) + } + + func testRoundTripPriv() throws { + let xpriv = "xprv9s21ZrQH143K3Dp5U6YoTum8c6rvMLxbEncwSjfnq12ShNzEhwbCmfvQDPNQTCsEcZJZcLrnf6rt6MCzsMiJYrhLGQwkK1uPCC5QsiAu4tW" + let key = try ExtendedPrivateKey(from: xpriv, networkType: .mainnet) + let serialized = try key.serialize(for: .mainnet) + XCTAssertEqual(xpriv, serialized) + } + + func testDerived() throws { + let parentKey = Data(hexString: "0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2") + let parentFingerprint = parentKey.sha256Ripemd160.prefix(4) + + let key = ExtendedPublicKey( + publicKey: Data(hexString: "0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2"), + chainCode: Data(hexString: "873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508") + ) + + let derivedKey = try key.derivePublicKey(node: .nonHardened(2)) + + XCTAssertEqual(derivedKey.parentFingerprint, parentFingerprint) + XCTAssertEqual(derivedKey.depth, 1) + XCTAssertEqual(derivedKey.childNumber, 2) + } + + func testInitMaster() throws { + let key = ExtendedPublicKey( + publicKey: Data(hexString: "0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2"), + chainCode: Data(hexString: "873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508") + ) + + XCTAssertEqual(key.depth, 0) + XCTAssertEqual(key.parentFingerprint, Data(hexString: "0x00000000")) + XCTAssertEqual(key.childNumber, 0) + } + + func testSerializeEdKey() { + let key = ExtendedPublicKey(publicKey: Data(hexString: "9FE5BB2CC7D83C1DA10845AFD8A34B141FD8FD72500B95B1547E12B9BB8AAC3D"), + chainCode: Data(hexString: "02fc9e5af0ac8d9b3cecfe2a888e2117ba3d089d8585886c9c826b6b22a98d12ea")) + XCTAssertThrowsError(try key.serialize(for: .mainnet)) + } + + func testSerialization() throws { + let mKeyString = "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8" + let mXpubKey = try ExtendedPublicKey(from: mKeyString, networkType: .mainnet) + + let key = try ExtendedPublicKey( + publicKey: Data(hexString: "035a784662a4a20a65bf6aab9ae98a6c068a81c52e4b032c0fb5400c706cfccc56"), + chainCode: Data(hexString: "47fdacbd0f1097043b78c63c20c34ef4ed9a111d980047ad16282c7ae6236141"), + depth: 1, + parentFingerprint: mXpubKey.publicKey.sha256Ripemd160.prefix(4), + childNumber: 2147483648 + ) + + let serialized = try key.serialize(for: .mainnet) + XCTAssertEqual(serialized, "xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw") + } + + // https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#user-content-Test_Vectors + func testBadKeys() { + // //(invalid pubkey 020000000000000000000000000000000000000000000000000000000000000007) + XCTAssertThrowsError(try ExtendedPublicKey(from: "xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6Q5JXayek4PRsn35jii4veMimro1xefsM58PgBMrvdYre8QyULY", networkType: .mainnet)) + + // (unknown extended key version) + XCTAssertThrowsError(try ExtendedPublicKey(from: "DMwo58pR1QLEFihHiXPVykYB6fJmsTeHvyTp7hRThAtCX8CvYzgPcn8XnmdfHPmHJiEDXkTiJTVV9rHEBUem2mwVbbNfvT2MTcAqj3nesx8uBf9", networkType:.mainnet)) + + // (unknown extended key version) + XCTAssertThrowsError(try ExtendedPrivateKey(from: "DMwo58pR1QLEFihHiXPVykYB6fJmsTeHvyTp7hRThAtCX8CvYzgPcn8XnmdfHGMQzT7ayAmfo4z3gY5KfbrZWZ6St24UVf2Qgo6oujFktLHdHY4", networkType:.mainnet)) + + // (zero depth with non-zero index) + XCTAssertThrowsError(try ExtendedPublicKey(from: "xpub661MyMwAuDcm6CRQ5N4qiHKrJ39Xe1R1NyfouMKTTWcguwVcfrZJaNvhpebzGerh7gucBvzEQWRugZDuDXjNDRmXzSZe4c7mnTK97pTvGS8", networkType: .mainnet)) + + // (zero depth with non-zero parent fingerprint) + XCTAssertThrowsError(try ExtendedPublicKey(from: "xpub661no6RGEX3uJkY4bNnPcw4URcQTrSibUZ4NqJEw5eBkv7ovTwgiT91XX27VbEXGENhYRCf7hyEbWrR3FewATdCEebj6znwMfQkhRYHRLpJ", networkType: .mainnet)) + + // (pubkey version / prvkey mismatch) + XCTAssertThrowsError(try ExtendedPublicKey(from: "xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6LBpB85b3D2yc8sfvZU521AAwdZafEz7mnzBBsz4wKY5fTtTQBm", networkType: .mainnet)) + + // (prvkey version / pubkey mismatch) + XCTAssertThrowsError(try ExtendedPrivateKey(from: "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFGTQQD3dC4H2D5GBj7vWvSQaaBv5cxi9gafk7NF3pnBju6dwKvH", networkType: .mainnet)) + + // (invalid pubkey prefix 04) + XCTAssertThrowsError(try ExtendedPublicKey(from: "xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6Txnt3siSujt9RCVYsx4qHZGc62TG4McvMGcAUjeuwZdduYEvFn", networkType: .mainnet)) + + // (invalid prvkey prefix 04) + XCTAssertThrowsError(try ExtendedPrivateKey(from: "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFGpWnsj83BHtEy5Zt8CcDr1UiRXuWCmTQLxEK9vbz5gPstX92JQ", networkType: .mainnet)) + + // (invalid pubkey prefix 01) + XCTAssertThrowsError(try ExtendedPublicKey(from: "xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6N8ZMMXctdiCjxTNq964yKkwrkBJJwpzZS4HS2fxvyYUA4q2Xe4", networkType: .mainnet)) + + // (invalid prvkey prefix 01) + XCTAssertThrowsError(try ExtendedPrivateKey(from: "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFAzHGBP2UuGCqWLTAPLcMtD9y5gkZ6Eq3Rjuahrv17fEQ3Qen6J", networkType: .mainnet)) + + // (zero depth with non-zero parent fingerprint) + XCTAssertThrowsError(try ExtendedPrivateKey(from: "xprv9s2SPatNQ9Vc6GTbVMFPFo7jsaZySyzk7L8n2uqKXJen3KUmvQNTuLh3fhZMBoG3G4ZW1N2kZuHEPY53qmbZzCHshoQnNf4GvELZfqTUrcv", networkType: .mainnet)) + + // (zero depth with non-zero index) + XCTAssertThrowsError(try ExtendedPrivateKey(from: "xprv9s21ZrQH4r4TsiLvyLXqM9P7k1K3EYhA1kkD6xuquB5i39AU8KF42acDyL3qsDbU9NmZn6MsGSUYZEsuoePmjzsB3eFKSUEh3Gu1N3cqVUN", networkType: .mainnet)) + + // (private key 0 not in 1..n-1) + XCTAssertThrowsError(try ExtendedPrivateKey(from: "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzF93Y5wvzdUayhgkkFoicQZcP3y52uPPxFnfoLZB21Teqt1VvEHx", networkType: .mainnet)) + + // (private key n not in 1..n-1) + XCTAssertThrowsError(try ExtendedPrivateKey(from: "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFAzHGBP2UuGCqWLTAPLcMtD5SDKr24z3aiUvKr9bJpdrcLg1y3G", networkType: .mainnet)) + + // (invalid checksum) + XCTAssertThrowsError(try ExtendedPrivateKey(from: "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHL", networkType: .mainnet)) + } +} diff --git a/TangemSdk/TangemSdkTests/Jsons/DeriveWalletPublicKey.json b/TangemSdk/TangemSdkTests/Jsons/DeriveWalletPublicKey.json index f809ef802..d8625da3e 100644 --- a/TangemSdk/TangemSdkTests/Jsons/DeriveWalletPublicKey.json +++ b/TangemSdk/TangemSdkTests/Jsons/DeriveWalletPublicKey.json @@ -12,7 +12,10 @@ "jsonrpc" : "2.0", "result" : { "publicKey": "0200300397571D99D41BB2A577E2CBE495C04AC5B9A97B7A4ECF999F23CE45E962", - "chainCode": "537F7361175B150732E17508066982B42D9FB1F8239C4D7BFC490088C83A8BBB" + "chainCode": "537F7361175B150732E17508066982B42D9FB1F8239C4D7BFC490088C83A8BBB", + "depth" : 0, + "parentFingerprint" : "00000000", + "childNumber" : 0 }, "id" : 1 } diff --git a/TangemSdk/TangemSdkTests/Jsons/DeriveWalletPublicKeys.json b/TangemSdk/TangemSdkTests/Jsons/DeriveWalletPublicKeys.json index 10ae82846..46d780d60 100644 --- a/TangemSdk/TangemSdkTests/Jsons/DeriveWalletPublicKeys.json +++ b/TangemSdk/TangemSdkTests/Jsons/DeriveWalletPublicKeys.json @@ -14,12 +14,18 @@ "m/44'/0'" : { "publicKey": "0200300397571D99D41BB2A577E2CBE495C04AC5B9A97B7A4ECF999F23CE45E962", - "chainCode": "537F7361175B150732E17508066982B42D9FB1F8239C4D7BFC490088C83A8BBB" + "chainCode": "537F7361175B150732E17508066982B42D9FB1F8239C4D7BFC490088C83A8BBB", + "depth" : 0, + "parentFingerprint" : "00000000", + "childNumber" : 0 }, "m/44'/1'" : { "publicKey": "0200300397571D99D41BB2A577E2CBE495C04AC5B9A97B7A4ECF999F23CE45E962", - "chainCode": "537F7361175B150732E17508066982B42D9FB1F8239C4D7BFC490088C83A8BBB" + "chainCode": "537F7361175B150732E17508066982B42D9FB1F8239C4D7BFC490088C83A8BBB", + "depth" : 0, + "parentFingerprint" : "00000000", + "childNumber" : 0 } }, "id" : 1 diff --git a/TangemSdk/TangemSdkTests/WIFTests.swift b/TangemSdk/TangemSdkTests/WIFTests.swift new file mode 100644 index 000000000..91e349082 --- /dev/null +++ b/TangemSdk/TangemSdkTests/WIFTests.swift @@ -0,0 +1,22 @@ +// +// WIFTests.swift +// TangemSdkTests +// +// Created by Alexander Osokin on 13.01.2023. +// Copyright © 2023 Tangem AG. All rights reserved. +// + +import XCTest +import Foundation +@testable import TangemSdk + +@available(iOS 13.0, *) +class WIFTests: XCTestCase { + func testRoundTrip() { + let key = Data(hexString: "589aeb596710f33d7ac31598ec10440a7df8808cf2c3d69ba670ff3fae66aafb") + let wif = "KzBwvPW6L5iwJSiE5vgS52Y69bUxfwizW3wF4C4Xa3ba3pdd7j63" + + XCTAssertEqual(WIF.decodeWIFCompressed(wif), key) + XCTAssertEqual(WIF.encodeToWIFCompressed(key, networkType: .mainnet), wif) + } +} From c3d7c4613e2cc4639a50d1c2b9ecd1f6e70afcd3 Mon Sep 17 00:00:00 2001 From: Alexander Osokin Date: Sat, 11 Mar 2023 00:05:58 +0500 Subject: [PATCH 10/24] IOS-3059 Remove comment --- .../TangemSdk/Crypto/HDWallet/BIP32/ExtendedPublicKey.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedPublicKey.swift b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedPublicKey.swift index 4790f1b92..a65719b16 100644 --- a/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedPublicKey.swift +++ b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedPublicKey.swift @@ -12,7 +12,6 @@ import CryptoKit @available(iOS 13.0, *) /// BIP32 extended public key for `secp256k1`. public struct ExtendedPublicKey: Equatable, Hashable, JSONStringConvertible, Codable { - /// Compressed `secp256k1` key public let publicKey: Data public let chainCode: Data From 9c753c99a16ec7eda6c92a1ecb421c33ed6fb0ef Mon Sep 17 00:00:00 2001 From: Alexander Osokin Date: Sat, 11 Mar 2023 00:57:05 +0500 Subject: [PATCH 11/24] IOS-3059 Remove comment --- .../TangemSdk/Crypto/HDWallet/BIP32/ExtendedPublicKey.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedPublicKey.swift b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedPublicKey.swift index a65719b16..6434aeb65 100644 --- a/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedPublicKey.swift +++ b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedPublicKey.swift @@ -10,7 +10,6 @@ import Foundation import CryptoKit @available(iOS 13.0, *) -/// BIP32 extended public key for `secp256k1`. public struct ExtendedPublicKey: Equatable, Hashable, JSONStringConvertible, Codable { public let publicKey: Data public let chainCode: Data From ed1877403d05c35f2225baef3f04bdeb647a41a0 Mon Sep 17 00:00:00 2001 From: Alexander Osokin Date: Mon, 13 Mar 2023 14:50:33 +0500 Subject: [PATCH 12/24] IOS-3059 Small polishing and commenting --- .../TangemSdk/Common/Extensions/Byte+.swift | 4 +- TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift | 107 ++++++++++-------- .../TangemSdk/Crypto/BIP39/Mnemonic.swift | 10 ++ .../Crypto/HDWallet/BIP32/BIP32.swift | 6 + 4 files changed, 77 insertions(+), 50 deletions(-) diff --git a/TangemSdk/TangemSdk/Common/Extensions/Byte+.swift b/TangemSdk/TangemSdk/Common/Extensions/Byte+.swift index 0cb0ed8c1..306f4b3a5 100644 --- a/TangemSdk/TangemSdk/Common/Extensions/Byte+.swift +++ b/TangemSdk/TangemSdk/Common/Extensions/Byte+.swift @@ -48,20 +48,18 @@ extension UInt16 { } } +@available(iOS 13.0, *) extension Array where Element == UInt8 { - @available(iOS 13.0, *) public func getSha256() -> Data { let digest = SHA256.hash(data: self) return Data(digest) } - @available(iOS 13.0, *) public func getSha512() -> Data { let digest = SHA512.hash(data: self) return Data(digest) } - @available(iOS 13.0, *) public func getDoubleSha256() -> Data { return getSha256().getSha256() } diff --git a/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift b/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift index a41aa1ae0..430c46646 100644 --- a/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift +++ b/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift @@ -8,6 +8,7 @@ import Foundation +// https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki @available(iOS 13.0, *) struct BIP39 { /// Generate a mnemonic. @@ -40,27 +41,45 @@ struct BIP39 { return seed } - func convertToMnemonicString(_ mnemonicComponents: [String]) -> String { - return mnemonicComponents.joined(separator: " ") - } + /// Generate a mnemonic from data. Useful for testing purposes. + /// - Parameters: + /// - data: The entropy data in hex format + /// - wordlist: The wordlist to use. + /// - Returns: The generated mnemonic + func generateMnemonic(from entropyData: Data, wordlist: Wordlist) throws -> [String] { + guard let entropyLength = EntropyLength(rawValue: entropyData.count * 8) else { + throw MnemonicError.invalidEntropyLength + } - func parse(mnemonicString: String) throws -> [String] { - let regex = try NSRegularExpression(pattern: "[a-zA-Z]+") - let range = NSRange(location: 0, length: mnemonicString.count) - let matches = regex.matches(in: mnemonicString, range: range) - let components = matches.compactMap { result -> String? in - guard result.numberOfRanges > 0, - let stringRange = Range(result.range(at: 0), in: mnemonicString) else { - return nil + let entropyHashBits = entropyData.getSha256().toBits() + let entropyChecksumBits = entropyHashBits.prefix(entropyLength.cheksumBitsCount) + + let entropyBits = entropyData.toBits() + let concatenatedBits = entropyBits + entropyChecksumBits + let bitIndexes = concatenatedBits.chunked(into: 11) + let indexes = bitIndexes.compactMap { Int($0.joined(), radix: 2) } + + guard indexes.count == entropyLength.wordsCount else { + throw MnemonicError.mnenmonicCreationFailed + } + + let allWords = wordlist.words + let maxWordIndex = allWords.count + + let words = try indexes.map { index in + guard index < maxWordIndex else { + throw MnemonicError.mnenmonicCreationFailed } - return String(mnemonicString[stringRange]).trim().lowercased() + return allWords[index] + } - try validate(mnemonicComponents: components) - return components + return words } + /// Validate a mnemonic. + /// - Parameter mnemonicComponents: Menemonic components to use func validate(mnemonicComponents: [String]) throws { // Validate words count guard !mnemonicComponents.isEmpty else { @@ -120,46 +139,40 @@ struct BIP39 { } } - // Validate wordlist by the first word - func parseWordlist(from mnemonicComponents: [String]) throws -> Wordlist { - return try getWordlist(by: mnemonicComponents[0]).0 - } - - /// Generate a mnemonic from data. Useful for testing purposes. - /// - Parameters: - /// - data: The entropy data in hex format - /// - wordlist: The wordlist to use. - /// - Returns: The generated mnemonic - func generateMnemonic(from entropyData: Data, wordlist: Wordlist) throws -> [String] { - guard let entropyLength = EntropyLength(rawValue: entropyData.count * 8) else { - throw MnemonicError.invalidEntropyLength - } - - let entropyHashBits = entropyData.getSha256().toBits() - let entropyChecksumBits = entropyHashBits.prefix(entropyLength.cheksumBitsCount) - - let entropyBits = entropyData.toBits() - let concatenatedBits = entropyBits + entropyChecksumBits - let bitIndexes = concatenatedBits.chunked(into: 11) - let indexes = bitIndexes.compactMap { Int($0.joined(), radix: 2) } + /// Parse a mnemonic. + /// - Parameter mnemonicString: The mnemonic to parse + /// - Returns: Menemonic components + func parse(mnemonicString: String) throws -> [String] { + let regex = try NSRegularExpression(pattern: "[a-zA-Z]+") + let range = NSRange(location: 0, length: mnemonicString.count) + let matches = regex.matches(in: mnemonicString, range: range) + let components = matches.compactMap { result -> String? in + guard result.numberOfRanges > 0, + let stringRange = Range(result.range(at: 0), in: mnemonicString) else { + return nil + } - guard indexes.count == entropyLength.wordsCount else { - throw MnemonicError.mnenmonicCreationFailed + return String(mnemonicString[stringRange]).trim().lowercased() } - let allWords = wordlist.words - let maxWordIndex = allWords.count + try validate(mnemonicComponents: components) + return components + } - let words = try indexes.map { index in - guard index < maxWordIndex else { - throw MnemonicError.mnenmonicCreationFailed - } - return allWords[index] + /// Validate wordlist by the first word + /// - Parameter mnemonicComponents: Menemonic components to use + /// - Returns: The Wordlist, selected by the first word + func parseWordlist(from mnemonicComponents: [String]) throws -> Wordlist { + return try getWordlist(by: mnemonicComponents[0]).0 + } - } - return words + /// Convert mnemonic componets to a sungle string, splitted by spaces + /// - Parameter mnemonicComponents: Menemonic components to use + /// - Returns: The mnemonic string + func convertToMnemonicString(_ mnemonicComponents: [String]) -> String { + return mnemonicComponents.joined(separator: " ") } private func normalizedData(from string: String) throws -> Data { diff --git a/TangemSdk/TangemSdk/Crypto/BIP39/Mnemonic.swift b/TangemSdk/TangemSdk/Crypto/BIP39/Mnemonic.swift index 2966eeadf..a5ebbc84c 100644 --- a/TangemSdk/TangemSdk/Crypto/BIP39/Mnemonic.swift +++ b/TangemSdk/TangemSdk/Crypto/BIP39/Mnemonic.swift @@ -9,6 +9,7 @@ import Foundation @available(iOS 13.0, *) +/// The `BIP-39` facade public struct Mnemonic { public let mnemonicComponents: [String] public let wordlist: Wordlist @@ -17,16 +18,25 @@ public struct Mnemonic { private let bip39 = BIP39() + /// Genarate a mnemonic + /// - Parameters: + /// - entropy: The entropy length to use. Default is 128 bit (12 words). + /// - wordList: The Wordlist length to use. Default is en. public init(with entropy: EntropyLength = .bits128, wordList: Wordlist = .en) throws { mnemonicComponents = try bip39.generateMnemonic(entropyLength: entropy, wordlist: wordList) self.wordlist = wordList } + /// Parse a mnemonic strind + /// - Parameter mnemonic: The mnemonic string to use public init(with mnemonic: String) throws { mnemonicComponents = try bip39.parse(mnemonicString: mnemonic) self.wordlist = try bip39.parseWordlist(from: mnemonicComponents) } + /// Generate a seed from the current mnemonic. + /// - Parameter passphrase: The optional passphrase to use. Empty by defaul. + /// - Returns: The generated deterministic seed according to BIP-39 public func generateSeed(with passphrase: String = "") throws -> Data { return try bip39.generateSeed(from: mnemonicComponents, passphrase: passphrase) } diff --git a/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/BIP32.swift b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/BIP32.swift index 31ed0ce05..bacc67834 100644 --- a/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/BIP32.swift +++ b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/BIP32.swift @@ -9,8 +9,14 @@ import Foundation import CryptoKit +// https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki struct BIP32 { @available(iOS 13.0, *) + /// Generate an extended private key from the seed. + /// - Parameters: + /// - seed: The seed to use + /// - curve: The curve to use + /// - Returns: The `ExtendedPrivateKey` func makeMasterKey(from seed: Data, curve: EllipticCurve) throws -> ExtendedPrivateKey { // The seed must be between 128 and 512 bits guard 16...64 ~= seed.count else { From 17241c4e31a4e33a658b14dcfb523bbb4a28225a Mon Sep 17 00:00:00 2001 From: Alexander Osokin Date: Mon, 13 Mar 2023 15:24:15 +0500 Subject: [PATCH 13/24] IOS-3059 Polishing and commenting --- .../TangemSdk/Common/Extensions/Byte+.swift | 4 +- TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift | 107 ++++++++++-------- .../TangemSdk/Crypto/BIP39/Mnemonic.swift | 10 ++ .../Crypto/HDWallet/BIP32/BIP32.swift | 6 + .../HDWallet/BIP32/ExtendedPublicKey.swift | 2 - 5 files changed, 77 insertions(+), 52 deletions(-) diff --git a/TangemSdk/TangemSdk/Common/Extensions/Byte+.swift b/TangemSdk/TangemSdk/Common/Extensions/Byte+.swift index 0cb0ed8c1..306f4b3a5 100644 --- a/TangemSdk/TangemSdk/Common/Extensions/Byte+.swift +++ b/TangemSdk/TangemSdk/Common/Extensions/Byte+.swift @@ -48,20 +48,18 @@ extension UInt16 { } } +@available(iOS 13.0, *) extension Array where Element == UInt8 { - @available(iOS 13.0, *) public func getSha256() -> Data { let digest = SHA256.hash(data: self) return Data(digest) } - @available(iOS 13.0, *) public func getSha512() -> Data { let digest = SHA512.hash(data: self) return Data(digest) } - @available(iOS 13.0, *) public func getDoubleSha256() -> Data { return getSha256().getSha256() } diff --git a/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift b/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift index a41aa1ae0..430c46646 100644 --- a/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift +++ b/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift @@ -8,6 +8,7 @@ import Foundation +// https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki @available(iOS 13.0, *) struct BIP39 { /// Generate a mnemonic. @@ -40,27 +41,45 @@ struct BIP39 { return seed } - func convertToMnemonicString(_ mnemonicComponents: [String]) -> String { - return mnemonicComponents.joined(separator: " ") - } + /// Generate a mnemonic from data. Useful for testing purposes. + /// - Parameters: + /// - data: The entropy data in hex format + /// - wordlist: The wordlist to use. + /// - Returns: The generated mnemonic + func generateMnemonic(from entropyData: Data, wordlist: Wordlist) throws -> [String] { + guard let entropyLength = EntropyLength(rawValue: entropyData.count * 8) else { + throw MnemonicError.invalidEntropyLength + } - func parse(mnemonicString: String) throws -> [String] { - let regex = try NSRegularExpression(pattern: "[a-zA-Z]+") - let range = NSRange(location: 0, length: mnemonicString.count) - let matches = regex.matches(in: mnemonicString, range: range) - let components = matches.compactMap { result -> String? in - guard result.numberOfRanges > 0, - let stringRange = Range(result.range(at: 0), in: mnemonicString) else { - return nil + let entropyHashBits = entropyData.getSha256().toBits() + let entropyChecksumBits = entropyHashBits.prefix(entropyLength.cheksumBitsCount) + + let entropyBits = entropyData.toBits() + let concatenatedBits = entropyBits + entropyChecksumBits + let bitIndexes = concatenatedBits.chunked(into: 11) + let indexes = bitIndexes.compactMap { Int($0.joined(), radix: 2) } + + guard indexes.count == entropyLength.wordsCount else { + throw MnemonicError.mnenmonicCreationFailed + } + + let allWords = wordlist.words + let maxWordIndex = allWords.count + + let words = try indexes.map { index in + guard index < maxWordIndex else { + throw MnemonicError.mnenmonicCreationFailed } - return String(mnemonicString[stringRange]).trim().lowercased() + return allWords[index] + } - try validate(mnemonicComponents: components) - return components + return words } + /// Validate a mnemonic. + /// - Parameter mnemonicComponents: Menemonic components to use func validate(mnemonicComponents: [String]) throws { // Validate words count guard !mnemonicComponents.isEmpty else { @@ -120,46 +139,40 @@ struct BIP39 { } } - // Validate wordlist by the first word - func parseWordlist(from mnemonicComponents: [String]) throws -> Wordlist { - return try getWordlist(by: mnemonicComponents[0]).0 - } - - /// Generate a mnemonic from data. Useful for testing purposes. - /// - Parameters: - /// - data: The entropy data in hex format - /// - wordlist: The wordlist to use. - /// - Returns: The generated mnemonic - func generateMnemonic(from entropyData: Data, wordlist: Wordlist) throws -> [String] { - guard let entropyLength = EntropyLength(rawValue: entropyData.count * 8) else { - throw MnemonicError.invalidEntropyLength - } - - let entropyHashBits = entropyData.getSha256().toBits() - let entropyChecksumBits = entropyHashBits.prefix(entropyLength.cheksumBitsCount) - - let entropyBits = entropyData.toBits() - let concatenatedBits = entropyBits + entropyChecksumBits - let bitIndexes = concatenatedBits.chunked(into: 11) - let indexes = bitIndexes.compactMap { Int($0.joined(), radix: 2) } + /// Parse a mnemonic. + /// - Parameter mnemonicString: The mnemonic to parse + /// - Returns: Menemonic components + func parse(mnemonicString: String) throws -> [String] { + let regex = try NSRegularExpression(pattern: "[a-zA-Z]+") + let range = NSRange(location: 0, length: mnemonicString.count) + let matches = regex.matches(in: mnemonicString, range: range) + let components = matches.compactMap { result -> String? in + guard result.numberOfRanges > 0, + let stringRange = Range(result.range(at: 0), in: mnemonicString) else { + return nil + } - guard indexes.count == entropyLength.wordsCount else { - throw MnemonicError.mnenmonicCreationFailed + return String(mnemonicString[stringRange]).trim().lowercased() } - let allWords = wordlist.words - let maxWordIndex = allWords.count + try validate(mnemonicComponents: components) + return components + } - let words = try indexes.map { index in - guard index < maxWordIndex else { - throw MnemonicError.mnenmonicCreationFailed - } - return allWords[index] + /// Validate wordlist by the first word + /// - Parameter mnemonicComponents: Menemonic components to use + /// - Returns: The Wordlist, selected by the first word + func parseWordlist(from mnemonicComponents: [String]) throws -> Wordlist { + return try getWordlist(by: mnemonicComponents[0]).0 + } - } - return words + /// Convert mnemonic componets to a sungle string, splitted by spaces + /// - Parameter mnemonicComponents: Menemonic components to use + /// - Returns: The mnemonic string + func convertToMnemonicString(_ mnemonicComponents: [String]) -> String { + return mnemonicComponents.joined(separator: " ") } private func normalizedData(from string: String) throws -> Data { diff --git a/TangemSdk/TangemSdk/Crypto/BIP39/Mnemonic.swift b/TangemSdk/TangemSdk/Crypto/BIP39/Mnemonic.swift index 2966eeadf..a5ebbc84c 100644 --- a/TangemSdk/TangemSdk/Crypto/BIP39/Mnemonic.swift +++ b/TangemSdk/TangemSdk/Crypto/BIP39/Mnemonic.swift @@ -9,6 +9,7 @@ import Foundation @available(iOS 13.0, *) +/// The `BIP-39` facade public struct Mnemonic { public let mnemonicComponents: [String] public let wordlist: Wordlist @@ -17,16 +18,25 @@ public struct Mnemonic { private let bip39 = BIP39() + /// Genarate a mnemonic + /// - Parameters: + /// - entropy: The entropy length to use. Default is 128 bit (12 words). + /// - wordList: The Wordlist length to use. Default is en. public init(with entropy: EntropyLength = .bits128, wordList: Wordlist = .en) throws { mnemonicComponents = try bip39.generateMnemonic(entropyLength: entropy, wordlist: wordList) self.wordlist = wordList } + /// Parse a mnemonic strind + /// - Parameter mnemonic: The mnemonic string to use public init(with mnemonic: String) throws { mnemonicComponents = try bip39.parse(mnemonicString: mnemonic) self.wordlist = try bip39.parseWordlist(from: mnemonicComponents) } + /// Generate a seed from the current mnemonic. + /// - Parameter passphrase: The optional passphrase to use. Empty by defaul. + /// - Returns: The generated deterministic seed according to BIP-39 public func generateSeed(with passphrase: String = "") throws -> Data { return try bip39.generateSeed(from: mnemonicComponents, passphrase: passphrase) } diff --git a/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/BIP32.swift b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/BIP32.swift index 31ed0ce05..bacc67834 100644 --- a/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/BIP32.swift +++ b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/BIP32.swift @@ -9,8 +9,14 @@ import Foundation import CryptoKit +// https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki struct BIP32 { @available(iOS 13.0, *) + /// Generate an extended private key from the seed. + /// - Parameters: + /// - seed: The seed to use + /// - curve: The curve to use + /// - Returns: The `ExtendedPrivateKey` func makeMasterKey(from seed: Data, curve: EllipticCurve) throws -> ExtendedPrivateKey { // The seed must be between 128 and 512 bits guard 16...64 ~= seed.count else { diff --git a/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedPublicKey.swift b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedPublicKey.swift index 4790f1b92..6434aeb65 100644 --- a/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedPublicKey.swift +++ b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedPublicKey.swift @@ -10,9 +10,7 @@ import Foundation import CryptoKit @available(iOS 13.0, *) -/// BIP32 extended public key for `secp256k1`. public struct ExtendedPublicKey: Equatable, Hashable, JSONStringConvertible, Codable { - /// Compressed `secp256k1` key public let publicKey: Data public let chainCode: Data From 47ea569324f43833bfea8554fd71c322b7bae184 Mon Sep 17 00:00:00 2001 From: Alexander Osokin Date: Mon, 13 Mar 2023 15:32:51 +0500 Subject: [PATCH 14/24] Update TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift Co-authored-by: Andrew Son --- TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift b/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift index 430c46646..5417236b0 100644 --- a/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift +++ b/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift @@ -82,7 +82,7 @@ struct BIP39 { /// - Parameter mnemonicComponents: Menemonic components to use func validate(mnemonicComponents: [String]) throws { // Validate words count - guard !mnemonicComponents.isEmpty else { + if mnemonicComponents.isEmpty { throw MnemonicError.wrongWordCount } From f7a2b25573cc3babddcad9bd38874009d0437322 Mon Sep 17 00:00:00 2001 From: Alexander Osokin Date: Mon, 13 Mar 2023 15:57:23 +0500 Subject: [PATCH 15/24] IOS-3059 Code polishing --- TangemSdk/TangemSdk/Common/Extensions/String+.swift | 4 ++-- TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift | 12 ++++++------ TangemSdk/TangemSdk/Crypto/CryptoUtils.swift | 5 ----- TangemSdk/TangemSdkTests/BIP39Tests.swift | 2 +- .../Jsons/BIP39/mnemonic_invalid_test_vectors.json | 3 ++- 5 files changed, 11 insertions(+), 15 deletions(-) diff --git a/TangemSdk/TangemSdk/Common/Extensions/String+.swift b/TangemSdk/TangemSdk/Common/Extensions/String+.swift index 35fdf999d..4fc5c3075 100644 --- a/TangemSdk/TangemSdk/Common/Extensions/String+.swift +++ b/TangemSdk/TangemSdk/Common/Extensions/String+.swift @@ -59,10 +59,10 @@ public extension String { return trimmingCharacters(in: .whitespacesAndNewlines) } - internal func zeroPadding(toLength newLength: Int) -> String { + internal func leadingZeroPadding(toLength newLength: Int) -> String { guard count < newLength else { return self } - let prefix = Array(repeating: "0", count: newLength - count).joined() + let prefix = String(repeating: "0", count: newLength - count) return prefix + self } } diff --git a/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift b/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift index 430c46646..98bb58a2d 100644 --- a/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift +++ b/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift @@ -43,7 +43,7 @@ struct BIP39 { /// Generate a mnemonic from data. Useful for testing purposes. /// - Parameters: - /// - data: The entropy data in hex format + /// - entropyData: The entropy data in hex format /// - wordlist: The wordlist to use. /// - Returns: The generated mnemonic func generateMnemonic(from entropyData: Data, wordlist: Wordlist) throws -> [String] { @@ -91,7 +91,7 @@ struct BIP39 { } // Validate wordlist by the first word - let wordlist = try getWordlist(by: mnemonicComponents[0]).1 + let wordlistDictionary = try getWordlist(by: mnemonicComponents[0]).dictionary // Validate all the words var invalidWords = Set() @@ -100,12 +100,12 @@ struct BIP39 { var concatenatedBits = "" for word in mnemonicComponents { - guard let wordIndex = wordlist.firstIndex(of: word) else { + guard let wordIndex = wordlistDictionary.firstIndex(of: word) else { invalidWords.insert(word) continue } - let indexBits = String(wordIndex, radix: 2).zeroPadding(toLength: 11) + let indexBits = String(wordIndex, radix: 2).leadingZeroPadding(toLength: 11) concatenatedBits.append(contentsOf: indexBits) } @@ -164,7 +164,7 @@ struct BIP39 { /// - Parameter mnemonicComponents: Menemonic components to use /// - Returns: The Wordlist, selected by the first word func parseWordlist(from mnemonicComponents: [String]) throws -> Wordlist { - return try getWordlist(by: mnemonicComponents[0]).0 + return try getWordlist(by: mnemonicComponents[0]).wordlist } @@ -185,7 +185,7 @@ struct BIP39 { return data } - private func getWordlist(by word: String) throws -> (Wordlist, [String]) { + private func getWordlist(by word: String) throws -> (wordlist: Wordlist, dictionary: [String]) { for list in Wordlist.allCases { let words = list.words diff --git a/TangemSdk/TangemSdk/Crypto/CryptoUtils.swift b/TangemSdk/TangemSdk/Crypto/CryptoUtils.swift index 486b847d3..ee0bf752d 100644 --- a/TangemSdk/TangemSdk/Crypto/CryptoUtils.swift +++ b/TangemSdk/TangemSdk/Crypto/CryptoUtils.swift @@ -94,11 +94,6 @@ public enum CryptoUtils { throw TangemSdkError.unsupportedCurve } } - - func createPublicKey(privateKey: Data) throws -> Data { - let key = try P256.Signing.PrivateKey(rawRepresentation: privateKey) - return key.publicKey.rawRepresentation - } /** * Helper function to verify that the data was signed with a private key that corresponds diff --git a/TangemSdk/TangemSdkTests/BIP39Tests.swift b/TangemSdk/TangemSdkTests/BIP39Tests.swift index 40caf0845..3a4c1a1e2 100644 --- a/TangemSdk/TangemSdkTests/BIP39Tests.swift +++ b/TangemSdk/TangemSdkTests/BIP39Tests.swift @@ -16,7 +16,7 @@ class BIP39Tests: XCTestCase { let langs = Wordlist.allCases for lang in langs { - XCTAssertTrue(lang.words.count > 0) + XCTAssertTrue(lang.words.count == 2048) } } diff --git a/TangemSdk/TangemSdkTests/Jsons/BIP39/mnemonic_invalid_test_vectors.json b/TangemSdk/TangemSdkTests/Jsons/BIP39/mnemonic_invalid_test_vectors.json index dc35f3565..68a2eeef2 100644 --- a/TangemSdk/TangemSdkTests/Jsons/BIP39/mnemonic_invalid_test_vectors.json +++ b/TangemSdk/TangemSdkTests/Jsons/BIP39/mnemonic_invalid_test_vectors.json @@ -7,7 +7,8 @@ "legal winner thank year wave sausage worth useful legal winner thank yellow yellow", "legal winner thank year 亂 sausage worth useful legal winner thank yellow", "", - "pear peasant pelican pen pear peasant pelican pen pear peasant pelican pen pear peasant pelican pen" + "pear peasant pelican pen pear peasant pelican pen pear peasant pelican pen pear peasant pelican pen", + "Legal winner thank year wave sausige worth useful legal winner thank yellow" ] ] } From 64fa93f722a680254228765673348a6c33f7baa2 Mon Sep 17 00:00:00 2001 From: Alexander Osokin Date: Mon, 13 Mar 2023 16:01:30 +0500 Subject: [PATCH 16/24] IOS-3059 Rename wordscount --- TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift | 4 ++-- TangemSdk/TangemSdk/Crypto/BIP39/EntropyLength.swift | 2 +- TangemSdk/TangemSdkTests/BIP39Tests.swift | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift b/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift index 6b8bc8b8e..0dfb455df 100644 --- a/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift +++ b/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift @@ -59,7 +59,7 @@ struct BIP39 { let bitIndexes = concatenatedBits.chunked(into: 11) let indexes = bitIndexes.compactMap { Int($0.joined(), radix: 2) } - guard indexes.count == entropyLength.wordsCount else { + guard indexes.count == entropyLength.wordCount else { throw MnemonicError.mnenmonicCreationFailed } @@ -86,7 +86,7 @@ struct BIP39 { throw MnemonicError.wrongWordCount } - guard let entropyLength = EntropyLength.allCases.first(where: { $0.wordsCount == mnemonicComponents.count }) else { + guard let entropyLength = EntropyLength.allCases.first(where: { $0.wordCount == mnemonicComponents.count }) else { throw MnemonicError.wrongWordCount } diff --git a/TangemSdk/TangemSdk/Crypto/BIP39/EntropyLength.swift b/TangemSdk/TangemSdk/Crypto/BIP39/EntropyLength.swift index d69319f80..734d49c0f 100644 --- a/TangemSdk/TangemSdk/Crypto/BIP39/EntropyLength.swift +++ b/TangemSdk/TangemSdk/Crypto/BIP39/EntropyLength.swift @@ -15,7 +15,7 @@ public enum EntropyLength: Int, CaseIterable { case bits224 = 224 case bits256 = 256 - var wordsCount: Int { + var wordCount: Int { switch self { case .bits128: return 12 case .bits160: return 15 diff --git a/TangemSdk/TangemSdkTests/BIP39Tests.swift b/TangemSdk/TangemSdkTests/BIP39Tests.swift index 3a4c1a1e2..add7c8e2e 100644 --- a/TangemSdk/TangemSdkTests/BIP39Tests.swift +++ b/TangemSdk/TangemSdkTests/BIP39Tests.swift @@ -29,7 +29,7 @@ class BIP39Tests: XCTestCase { for entropyLength in entropyLengthArray { for wordlist in wordLists { let mnemonic = try bip39.generateMnemonic(entropyLength: entropyLength, wordlist: wordlist) - XCTAssertEqual(mnemonic.count, entropyLength.wordsCount) + XCTAssertEqual(mnemonic.count, entropyLength.wordCount) } } } From b0c9dec42dc725fec9244605cee906df78830194 Mon Sep 17 00:00:00 2001 From: Alexander Osokin Date: Mon, 13 Mar 2023 16:34:15 +0500 Subject: [PATCH 17/24] IOS-3059 Make serialization public --- .../ExtendedKeySerialization/ExtendedKeySerializable.swift | 2 +- .../TangemSdk/Crypto/HDWallet/BIP32/ExtendedPrivateKey.swift | 4 ++-- .../TangemSdk/Crypto/HDWallet/BIP32/ExtendedPublicKey.swift | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedKeySerialization/ExtendedKeySerializable.swift b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedKeySerialization/ExtendedKeySerializable.swift index cbef23166..e57bfe69b 100644 --- a/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedKeySerialization/ExtendedKeySerializable.swift +++ b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedKeySerialization/ExtendedKeySerializable.swift @@ -10,7 +10,7 @@ import Foundation /// https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#serialization-format @available(iOS 13.0, *) -protocol ExtendedKeySerializable { +public protocol ExtendedKeySerializable { init(from extendedKeyString: String, networkType: NetworkType) throws func serialize(for networkType: NetworkType) throws -> String } diff --git a/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedPrivateKey.swift b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedPrivateKey.swift index dd85494cc..2aeea7072 100644 --- a/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedPrivateKey.swift +++ b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedPrivateKey.swift @@ -63,7 +63,7 @@ public struct ExtendedPrivateKey: Equatable, Hashable, JSONStringConvertible, Co @available(iOS 13.0, *) extension ExtendedPrivateKey: ExtendedKeySerializable { - init(from extendedKeyString: String, networkType: NetworkType) throws { + public init(from extendedKeyString: String, networkType: NetworkType) throws { guard let data = extendedKeyString.base58CheckDecodedData else { throw ExtendedKeySerializationError.decodingFailed } @@ -104,7 +104,7 @@ extension ExtendedPrivateKey: ExtendedKeySerializable { ) } - func serialize(for networkType: NetworkType) throws -> String { + public func serialize(for networkType: NetworkType) throws -> String { var data = Data(capacity: ExtendedKeySerializer.Constants.dataLength) let version = ExtendedKeySerializer.Version.private diff --git a/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedPublicKey.swift b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedPublicKey.swift index 6434aeb65..416c57155 100644 --- a/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedPublicKey.swift +++ b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/ExtendedPublicKey.swift @@ -89,7 +89,7 @@ public struct ExtendedPublicKey: Equatable, Hashable, JSONStringConvertible, Cod @available(iOS 13.0, *) extension ExtendedPublicKey: ExtendedKeySerializable { - init(from extendedKeyString: String, networkType: NetworkType) throws { + public init(from extendedKeyString: String, networkType: NetworkType) throws { guard let data = extendedKeyString.base58CheckDecodedData else { throw ExtendedKeySerializationError.decodingFailed } @@ -125,7 +125,7 @@ extension ExtendedPublicKey: ExtendedKeySerializable { ) } - func serialize(for networkType: NetworkType) throws -> String { + public func serialize(for networkType: NetworkType) throws -> String { guard let secpKey = try? Secp256k1Key(with: publicKey) else { throw TangemSdkError.unsupportedCurve } From 0056f00e4bac2ec38bf339a8022bb10bc0a567ec Mon Sep 17 00:00:00 2001 From: Alexander Osokin Date: Mon, 13 Mar 2023 16:53:09 +0500 Subject: [PATCH 18/24] IOS-3059 Add tests --- TangemSdk/TangemSdk.xcodeproj/project.pbxproj | 4 ++++ TangemSdk/TangemSdkTests/Base58Tests.swift | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 TangemSdk/TangemSdkTests/Base58Tests.swift diff --git a/TangemSdk/TangemSdk.xcodeproj/project.pbxproj b/TangemSdk/TangemSdk.xcodeproj/project.pbxproj index f375137fa..809d6e1e8 100644 --- a/TangemSdk/TangemSdk.xcodeproj/project.pbxproj +++ b/TangemSdk/TangemSdk.xcodeproj/project.pbxproj @@ -311,6 +311,7 @@ DC1244E429BB806E0037BC05 /* WIFTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244E329BB806E0037BC05 /* WIFTests.swift */; }; DC1244E629BB8E580037BC05 /* NetworkType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244E529BB8E580037BC05 /* NetworkType.swift */; }; DC1244E829BB9E0C0037BC05 /* ExtendedKeySerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244E729BB9E0C0037BC05 /* ExtendedKeySerializer.swift */; }; + DC4E442929BF42630088617C /* Base58Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4E442829BF42630088617C /* Base58Tests.swift */; }; DC59CB0429AF597900EC14E1 /* Wordlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC59CB0329AF597900EC14E1 /* Wordlist.swift */; }; DC59CB0A29AF6F9C00EC14E1 /* EntropyLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC59CB0929AF6F9C00EC14E1 /* EntropyLength.swift */; }; DC59CB0C29AF706100EC14E1 /* MnemonicError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC59CB0B29AF706100EC14E1 /* MnemonicError.swift */; }; @@ -645,6 +646,7 @@ DC1244E329BB806E0037BC05 /* WIFTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WIFTests.swift; sourceTree = ""; }; DC1244E529BB8E580037BC05 /* NetworkType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkType.swift; sourceTree = ""; }; DC1244E729BB9E0C0037BC05 /* ExtendedKeySerializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtendedKeySerializer.swift; sourceTree = ""; }; + DC4E442829BF42630088617C /* Base58Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Base58Tests.swift; sourceTree = ""; }; DC59CB0329AF597900EC14E1 /* Wordlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wordlist.swift; sourceTree = ""; }; DC59CB0929AF6F9C00EC14E1 /* EntropyLength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntropyLength.swift; sourceTree = ""; }; DC59CB0B29AF706100EC14E1 /* MnemonicError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MnemonicError.swift; sourceTree = ""; }; @@ -1247,6 +1249,7 @@ 5DD127A124F3D1A0009ACA29 /* JsonTests.swift */, 5D6F51FD265845D5007CD7E2 /* JSONRPCTests.swift */, 5D170AF226B46DAC000D4F36 /* HDWalletTests.swift */, + DC4E442829BF42630088617C /* Base58Tests.swift */, ); path = TangemSdkTests; sourceTree = ""; @@ -1936,6 +1939,7 @@ 5D437EEB237BE980009C82A8 /* TlvTests.swift in Sources */, 5D170AF326B46DAC000D4F36 /* HDWalletTests.swift in Sources */, 5D14947A268625A400C0D923 /* CommonTests.swift in Sources */, + DC4E442929BF42630088617C /* Base58Tests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/TangemSdk/TangemSdkTests/Base58Tests.swift b/TangemSdk/TangemSdkTests/Base58Tests.swift new file mode 100644 index 000000000..af49cf718 --- /dev/null +++ b/TangemSdk/TangemSdkTests/Base58Tests.swift @@ -0,0 +1,24 @@ +// +// Base58Tests.swift +// TangemSdkTests +// +// Created by Alexander Osokin on 13.03.2023. +// Copyright © 2023 Tangem AG. All rights reserved. +// + +import Foundation +import XCTest +@testable import TangemSdk + +@available(iOS 13.0, *) +class Base58Tests: XCTestCase { + func testRoundTrip() { + let data = Data(repeating: UInt8(1), count: 32) + XCTAssertEqual(data.base58CheckEncodedString.base58CheckDecodedData, data) + XCTAssertEqual(data.base58EncodedString.base58DecodedData, data) + + let array = [UInt8](repeating: UInt8(1), count: 32) + XCTAssertEqual(array.base58CheckEncodedString.base58CheckDecodedBytes, array) + XCTAssertEqual(array.base58EncodedString.base58DecodedBytes, array) + } +} From b4a4a6d3388b343c428930a42e93fe4a3ba67004 Mon Sep 17 00:00:00 2001 From: Alexander Osokin Date: Mon, 13 Mar 2023 16:53:22 +0500 Subject: [PATCH 19/24] IOS-3059 Make Base58 extensions public --- TangemSdk/TangemSdk/Crypto/Base58.swift | 26 +++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/TangemSdk/TangemSdk/Crypto/Base58.swift b/TangemSdk/TangemSdk/Crypto/Base58.swift index 04bea6208..06c45b25e 100644 --- a/TangemSdk/TangemSdk/Crypto/Base58.swift +++ b/TangemSdk/TangemSdk/Crypto/Base58.swift @@ -116,14 +116,14 @@ fileprivate enum Base58 { // MARK: - Data+ -extension Data { - public var base58EncodedString: String { +public extension Data { + var base58EncodedString: String { let bytes = Array(self) return bytes.base58EncodedString } @available(iOS 13.0, *) - public var base58CheckEncodedString: String { + var base58CheckEncodedString: String { let bytes = Array(self) return bytes.base58CheckEncodedString } @@ -131,15 +131,15 @@ extension Data { // MARK: - Array+ -extension Array where Element == UInt8 { - public var base58EncodedString: String { +public extension Array where Element == UInt8 { + var base58EncodedString: String { guard !self.isEmpty else { return "" } return Base58.base58FromBytes(self) } @available(iOS 13.0, *) - public var base58CheckEncodedString: String { + var base58CheckEncodedString: String { guard !self.isEmpty else { return "" } let checksum = self.getDoubleSha256().prefix(4) @@ -150,22 +150,24 @@ extension Array where Element == UInt8 { // MARK: - String+ -extension String { - public var base58DecodedData: Data { - let bytes = Base58.bytesFromBase58(self) +public extension String { + var base58DecodedData: Data { + Data(base58DecodedBytes) + } - return Data(bytes) + var base58DecodedBytes: [UInt8] { + return Base58.bytesFromBase58(self) } @available(iOS 13.0, *) - public var base58CheckDecodedData: Data? { + var base58CheckDecodedData: Data? { guard let bytes = base58CheckDecodedBytes else { return nil } return Data(bytes) } @available(iOS 13.0, *) - public var base58CheckDecodedBytes: [UInt8]? { + var base58CheckDecodedBytes: [UInt8]? { let bytes = Base58.bytesFromBase58(self) guard bytes.count >= 4 else { return nil } From de875a144fceb1dd18e5e7c3a044654cf751a6d1 Mon Sep 17 00:00:00 2001 From: Alexander Osokin Date: Mon, 13 Mar 2023 18:41:54 +0500 Subject: [PATCH 20/24] IOS-3059 Refactor from HEX conversion --- TangemSdk/TangemSdk.xcodeproj/project.pbxproj | 8 ++--- .../TangemSdk/Common/Extensions/Data+.swift | 2 +- .../Common/Extensions/HexConvertible.swift | 27 +++++++++++++++++ .../TangemSdk/Common/Extensions/Int+.swift | 9 ------ .../TangemSdk/Common/Extensions/String+.swift | 2 +- .../TangemSdk/Common/Extensions/UInt64+.swift | 20 ------------- .../TangemSdk/Common/TLV/TlvDecoder.swift | 29 ++++++++++++------- TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift | 4 +-- .../TangemSdk/Crypto/BIP39/Mnemonic.swift | 4 +-- .../TangemSdk/Crypto/BIP39/Wordlist.swift | 11 +++++-- .../HDWallet/BIP32/DerivationNode.swift | 7 +++-- .../HDWallet/BIP32/DerivationPath.swift | 4 +-- .../HDWallet/BIP32/ExtendedPrivateKey.swift | 6 ++-- .../HDWallet/BIP32/ExtendedPublicKey.swift | 6 ++-- TangemSdk/TangemSdkTests/BIP39Tests.swift | 6 ++-- TangemSdk/TangemSdkTests/Base58Tests.swift | 6 ++++ TangemSdk/TangemSdkTests/IntUtilsTests.swift | 15 ++++++++++ 17 files changed, 102 insertions(+), 64 deletions(-) create mode 100644 TangemSdk/TangemSdk/Common/Extensions/HexConvertible.swift delete mode 100644 TangemSdk/TangemSdk/Common/Extensions/UInt64+.swift diff --git a/TangemSdk/TangemSdk.xcodeproj/project.pbxproj b/TangemSdk/TangemSdk.xcodeproj/project.pbxproj index 809d6e1e8..ac2e67d6f 100644 --- a/TangemSdk/TangemSdk.xcodeproj/project.pbxproj +++ b/TangemSdk/TangemSdk.xcodeproj/project.pbxproj @@ -301,7 +301,7 @@ DC1244C529B769400037BC05 /* mnemonic_invalid_test_vectors.json in Resources */ = {isa = PBXBuildFile; fileRef = DC1244C429B769400037BC05 /* mnemonic_invalid_test_vectors.json */; }; DC1244C729B776D40037BC05 /* ExtendedPrivateKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244C629B776D40037BC05 /* ExtendedPrivateKey.swift */; }; DC1244C929B778750037BC05 /* BIP32Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244C829B778750037BC05 /* BIP32Tests.swift */; }; - DC1244CB29B9ECAA0037BC05 /* UInt64+.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244CA29B9ECAA0037BC05 /* UInt64+.swift */; }; + DC1244CB29B9ECAA0037BC05 /* HexConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244CA29B9ECAA0037BC05 /* HexConvertible.swift */; }; DC1244D829BB65970037BC05 /* ExtendedKeySerializationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244D429BB65970037BC05 /* ExtendedKeySerializationError.swift */; }; DC1244DA29BB65970037BC05 /* ExtendedKeySerializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244D629BB65970037BC05 /* ExtendedKeySerializable.swift */; }; DC1244DC29BB66840037BC05 /* ExtendedKeyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1244DB29BB66840037BC05 /* ExtendedKeyTests.swift */; }; @@ -636,7 +636,7 @@ DC1244C429B769400037BC05 /* mnemonic_invalid_test_vectors.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = mnemonic_invalid_test_vectors.json; sourceTree = ""; }; DC1244C629B776D40037BC05 /* ExtendedPrivateKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtendedPrivateKey.swift; sourceTree = ""; }; DC1244C829B778750037BC05 /* BIP32Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BIP32Tests.swift; sourceTree = ""; }; - DC1244CA29B9ECAA0037BC05 /* UInt64+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UInt64+.swift"; sourceTree = ""; }; + DC1244CA29B9ECAA0037BC05 /* HexConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HexConvertible.swift; sourceTree = ""; }; DC1244D429BB65970037BC05 /* ExtendedKeySerializationError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtendedKeySerializationError.swift; sourceTree = ""; }; DC1244D629BB65970037BC05 /* ExtendedKeySerializable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtendedKeySerializable.swift; sourceTree = ""; }; DC1244DB29BB66840037BC05 /* ExtendedKeyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtendedKeyTests.swift; sourceTree = ""; }; @@ -1365,7 +1365,7 @@ 5DDA5DA226E95F2F00199274 /* Result+RAPDU.swift */, 5D04F9E126EF731B00D15F75 /* Publisher+.swift */, DA6C7529292682650070EEFD /* LAContext+.swift */, - DC1244CA29B9ECAA0037BC05 /* UInt64+.swift */, + DC1244CA29B9ECAA0037BC05 /* HexConvertible.swift */, ); path = Extensions; sourceTree = ""; @@ -1815,7 +1815,7 @@ 5DEFA99526D7F40300E5CDE4 /* FinalizeBackupCardTask.swift in Sources */, 5D445B7226E295A700F6F0FE /* AuthorizeResetPinToken.swift in Sources */, B0EC6500260110530088F03D /* WalletDeserializer.swift in Sources */, - DC1244CB29B9ECAA0037BC05 /* UInt64+.swift in Sources */, + DC1244CB29B9ECAA0037BC05 /* HexConvertible.swift in Sources */, 5D379C2B268FA4D200C7F473 /* CompletionResult.swift in Sources */, 5D86CBDD24A1105A00FB5BA7 /* Manufacturer.swift in Sources */, B0D3C48B26088ECD0038A5C9 /* Array+Tlv.swift in Sources */, diff --git a/TangemSdk/TangemSdk/Common/Extensions/Data+.swift b/TangemSdk/TangemSdk/Common/Extensions/Data+.swift index 5612c9541..bceed0a4d 100644 --- a/TangemSdk/TangemSdk/Common/Extensions/Data+.swift +++ b/TangemSdk/TangemSdk/Common/Extensions/Data+.swift @@ -23,7 +23,7 @@ extension Data { return hexString } - public func toInt() -> Int { + public func toInt() -> Int? { return Int(hexData: self) } diff --git a/TangemSdk/TangemSdk/Common/Extensions/HexConvertible.swift b/TangemSdk/TangemSdk/Common/Extensions/HexConvertible.swift new file mode 100644 index 000000000..56619ed78 --- /dev/null +++ b/TangemSdk/TangemSdk/Common/Extensions/HexConvertible.swift @@ -0,0 +1,27 @@ +// +// HexConvertible.swift +// TangemSdk +// +// Created by Alexander Osokin on 09.03.2023. +// Copyright © 2023 Tangem AG. All rights reserved. +// + +import Foundation + +// Convert hex data to Integer +public protocol HexConvertible { + init?(hexData: Data) +} + +public extension HexConvertible where Self: FixedWidthInteger { + init?(hexData: Data) { + guard let intValue = Self(hexData.hexString, radix: 16) else { + return nil + } + + self = intValue + } +} + +extension Int: HexConvertible {} +extension UInt64: HexConvertible {} diff --git a/TangemSdk/TangemSdk/Common/Extensions/Int+.swift b/TangemSdk/TangemSdk/Common/Extensions/Int+.swift index 9c5088c09..920a6a80c 100644 --- a/TangemSdk/TangemSdk/Common/Extensions/Int+.swift +++ b/TangemSdk/TangemSdk/Common/Extensions/Int+.swift @@ -9,15 +9,6 @@ import Foundation extension Int { - /// Convert hex data to Integer - /// - Parameter hexData: length bytes - public init(hexData: Data) { - let value = hexData.reduce(0) { v, byte in - return v << 8 | Int(byte) - } - self = value - } - /// Convert int to byte, truncatingIfNeeded public var byte: Data { return Data([Byte(truncatingIfNeeded: self)]) diff --git a/TangemSdk/TangemSdk/Common/Extensions/String+.swift b/TangemSdk/TangemSdk/Common/Extensions/String+.swift index 4fc5c3075..56dbe7799 100644 --- a/TangemSdk/TangemSdk/Common/Extensions/String+.swift +++ b/TangemSdk/TangemSdk/Common/Extensions/String+.swift @@ -61,7 +61,7 @@ public extension String { internal func leadingZeroPadding(toLength newLength: Int) -> String { guard count < newLength else { return self } - + let prefix = String(repeating: "0", count: newLength - count) return prefix + self } diff --git a/TangemSdk/TangemSdk/Common/Extensions/UInt64+.swift b/TangemSdk/TangemSdk/Common/Extensions/UInt64+.swift deleted file mode 100644 index 21b7a4a59..000000000 --- a/TangemSdk/TangemSdk/Common/Extensions/UInt64+.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// UInt64+.swift -// TangemSdk -// -// Created by Alexander Osokin on 09.03.2023. -// Copyright © 2023 Tangem AG. All rights reserved. -// - -import Foundation - -extension UInt64 { - /// Convert hex data to Integer - /// - Parameter hexData: length bytes - public init(hexData: Data) { - let value = hexData.reduce(0) { v, byte in - return v << 8 | UInt64(byte) - } - self = value - } -} diff --git a/TangemSdk/TangemSdk/Common/TLV/TlvDecoder.swift b/TangemSdk/TangemSdk/Common/TLV/TlvDecoder.swift index 5cdc24441..2e654560c 100644 --- a/TangemSdk/TangemSdk/Common/TLV/TlvDecoder.swift +++ b/TangemSdk/TangemSdk/Common/TLV/TlvDecoder.swift @@ -136,30 +136,38 @@ public final class TlvDecoder { case .settingsMask: do { try typeCheck(CardSettingsMask.self, T.self, for: tag) - let intValue = tagValue.toInt() + guard let intValue = tagValue.toInt() else { + throw TangemSdkError.decodingFailed("Decoding error. Failed convert \(tag) to SettingsMask") + } + let settingsMask = CardSettingsMask(rawValue: intValue) return settingsMask as! T } catch TangemSdkError.decodingFailedTypeMismatch { try typeCheck(WalletSettingsMask.self, T.self, for: tag) - let intValue = tagValue.toInt() + guard let intValue = tagValue.toInt() else { + throw TangemSdkError.decodingFailed("Decoding error. Failed convert \(tag) to WalletSettingsMask") + } + let settingsMask = WalletSettingsMask(rawValue: intValue) return settingsMask as! T } case .status: do { try typeCheck(Card.Status.self, T.self, for: tag) - let intValue = tagValue.toInt() - guard let cardStatus = Card.Status(rawValue: intValue) else { + guard let intValue = tagValue.toInt(), + let cardStatus = Card.Status(rawValue: intValue) else { throw TangemSdkError.decodingFailed("Decoding error. Failed convert \(tag) to int and CardStatus") } - + return cardStatus as! T } catch TangemSdkError.decodingFailedTypeMismatch { try typeCheck(Card.Wallet.Status.self, T.self, for: tag) - let intValue = tagValue.toInt() - guard let walletStatus = Card.Wallet.Status(rawValue: intValue) else { + + guard let intValue = tagValue.toInt(), + let walletStatus = Card.Wallet.Status(rawValue: intValue) else { throw TangemSdkError.decodingFailed("Decoding error. Failed convert \(tag) to int and WalletStatus") } + return walletStatus as! T } case .signingMethod: @@ -195,10 +203,11 @@ public final class TlvDecoder { try typeCheck(DerivationPath.self, T.self, for: tag) return try DerivationPath(from: tagValue) as! T case .backupStatus: - let intValue = tagValue.toInt() + try typeCheck(Card.BackupRawStatus.self, T.self, for: tag) - guard let status = Card.BackupRawStatus.make(from: intValue) else { - throw TangemSdkError.decodingFailed("Decoding error. Unknown iBackupStatus") + guard let intValue = tagValue.toInt(), + let status = Card.BackupRawStatus.make(from: intValue) else { + throw TangemSdkError.decodingFailed("Decoding error. Unknown BackupRawStatus") } return status as! T } diff --git a/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift b/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift index 0dfb455df..dd2f51f1c 100644 --- a/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift +++ b/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift @@ -10,7 +10,7 @@ import Foundation // https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki @available(iOS 13.0, *) -struct BIP39 { +public struct BIP39 { /// Generate a mnemonic. /// - Parameters: /// - entropyLength: The entropy length to use. Default is 128 bit. @@ -21,7 +21,7 @@ struct BIP39 { throw MnemonicError.mnenmonicCreationFailed } - let entropyBytesCount = entropyLength.rawValue / 8 + let entropyBytesCount = entropyLength.rawValue / 8 // convert bits to bytes let entropyData = try CryptoUtils.generateRandomBytes(count: entropyBytesCount) return try generateMnemonic(from: entropyData, wordlist: wordlist) } diff --git a/TangemSdk/TangemSdk/Crypto/BIP39/Mnemonic.swift b/TangemSdk/TangemSdk/Crypto/BIP39/Mnemonic.swift index a5ebbc84c..70140c9ab 100644 --- a/TangemSdk/TangemSdk/Crypto/BIP39/Mnemonic.swift +++ b/TangemSdk/TangemSdk/Crypto/BIP39/Mnemonic.swift @@ -12,7 +12,7 @@ import Foundation /// The `BIP-39` facade public struct Mnemonic { public let mnemonicComponents: [String] - public let wordlist: Wordlist + public let wordlist: BIP39.Wordlist public var mnemonic: String { bip39.convertToMnemonicString(mnemonicComponents) } @@ -22,7 +22,7 @@ public struct Mnemonic { /// - Parameters: /// - entropy: The entropy length to use. Default is 128 bit (12 words). /// - wordList: The Wordlist length to use. Default is en. - public init(with entropy: EntropyLength = .bits128, wordList: Wordlist = .en) throws { + public init(with entropy: EntropyLength = .bits128, wordList: BIP39.Wordlist = .en) throws { mnemonicComponents = try bip39.generateMnemonic(entropyLength: entropy, wordlist: wordList) self.wordlist = wordList } diff --git a/TangemSdk/TangemSdk/Crypto/BIP39/Wordlist.swift b/TangemSdk/TangemSdk/Crypto/BIP39/Wordlist.swift index dc75cf63b..ee81111a8 100644 --- a/TangemSdk/TangemSdk/Crypto/BIP39/Wordlist.swift +++ b/TangemSdk/TangemSdk/Crypto/BIP39/Wordlist.swift @@ -9,9 +9,14 @@ import Foundation @available(iOS 13.0, *) -public enum Wordlist: CaseIterable { - case en +extension BIP39 { + public enum Wordlist: CaseIterable { + case en + } +} +@available(iOS 13.0, *) +extension BIP39.Wordlist { /// This var reads a big array from a file public var words: [String] { (try? readWords(from: fileName)) ?? [] @@ -31,7 +36,7 @@ public enum Wordlist: CaseIterable { let content = try String(contentsOfFile: path, encoding: .utf8) let words = content.trim().components(separatedBy: "\n") - + guard words.count == 2048 else { throw MnemonicError.invalidWordCount } diff --git a/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/DerivationNode.swift b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/DerivationNode.swift index bb857cdb7..4e8eee8a0 100644 --- a/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/DerivationNode.swift +++ b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/DerivationNode.swift @@ -54,8 +54,11 @@ extension DerivationNode { index.bytes4 } - static func deserialize(from data: Data) -> DerivationNode { - let index = UInt32(data.toInt()) + static func deserialize(from data: Data) -> DerivationNode? { + guard let intValue = data.toInt() else { return nil } + + let index = UInt32(intValue) + if index >= BIP32.Constants.hardenedOffset { return .hardened(index - BIP32.Constants.hardenedOffset) } diff --git a/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/DerivationPath.swift b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/DerivationPath.swift index f4b74ee52..766c693f8 100644 --- a/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/DerivationPath.swift +++ b/TangemSdk/TangemSdk/Crypto/HDWallet/BIP32/DerivationPath.swift @@ -77,8 +77,8 @@ extension DerivationPath { } let chunks = 0.. Date: Fri, 17 Mar 2023 18:07:14 +0500 Subject: [PATCH 21/24] IOS-3059 Open validation, change regex --- TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift | 124 +++++++++---------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift b/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift index dd2f51f1c..225c4aea5 100644 --- a/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift +++ b/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift @@ -11,6 +11,67 @@ import Foundation // https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki @available(iOS 13.0, *) public struct BIP39 { + /// Validate a mnemonic. + /// - Parameter mnemonicComponents: Menemonic components to use + public func validate(mnemonicComponents: [String]) throws { + // Validate words count + if mnemonicComponents.isEmpty { + throw MnemonicError.wrongWordCount + } + + guard let entropyLength = EntropyLength.allCases.first(where: { $0.wordCount == mnemonicComponents.count }) else { + throw MnemonicError.wrongWordCount + } + + // Validate wordlist by the first word + let wordlistDictionary = try getWordlist(by: mnemonicComponents[0]).dictionary + + // Validate all the words + var invalidWords = Set() + + // Generate an indices array inplace + var concatenatedBits = "" + + for word in mnemonicComponents { + guard let wordIndex = wordlistDictionary.firstIndex(of: word) else { + invalidWords.insert(word) + continue + } + + let indexBits = String(wordIndex, radix: 2).leadingZeroPadding(toLength: 11) + concatenatedBits.append(contentsOf: indexBits) + } + + guard invalidWords.isEmpty else { + throw MnemonicError.invalidWords(words: Array(invalidWords)) + } + + // Validate checksum + + let checksumBitsCount = mnemonicComponents.count / 3 + guard checksumBitsCount == entropyLength.cheksumBitsCount else { + throw MnemonicError.invalidCheksum + } + + let entropyBitsCount = concatenatedBits.count - checksumBitsCount + let entropyBits = String(concatenatedBits.prefix(entropyBitsCount)) + let checksumBits = String(concatenatedBits.suffix(checksumBitsCount)) + + guard let entropyData = Data(bitsString: entropyBits) else { + throw MnemonicError.invalidCheksum + } + + let calculatedChecksumBits = entropyData + .getSha256() + .toBits() + .prefix(entropyLength.cheksumBitsCount) + .joined() + + guard calculatedChecksumBits == checksumBits else { + throw MnemonicError.invalidCheksum + } + } + /// Generate a mnemonic. /// - Parameters: /// - entropyLength: The entropy length to use. Default is 128 bit. @@ -78,72 +139,13 @@ public struct BIP39 { return words } - /// Validate a mnemonic. - /// - Parameter mnemonicComponents: Menemonic components to use - func validate(mnemonicComponents: [String]) throws { - // Validate words count - if mnemonicComponents.isEmpty { - throw MnemonicError.wrongWordCount - } - - guard let entropyLength = EntropyLength.allCases.first(where: { $0.wordCount == mnemonicComponents.count }) else { - throw MnemonicError.wrongWordCount - } - - // Validate wordlist by the first word - let wordlistDictionary = try getWordlist(by: mnemonicComponents[0]).dictionary - - // Validate all the words - var invalidWords = Set() - - // Generate an indices array inplace - var concatenatedBits = "" - - for word in mnemonicComponents { - guard let wordIndex = wordlistDictionary.firstIndex(of: word) else { - invalidWords.insert(word) - continue - } - - let indexBits = String(wordIndex, radix: 2).leadingZeroPadding(toLength: 11) - concatenatedBits.append(contentsOf: indexBits) - } - - guard invalidWords.isEmpty else { - throw MnemonicError.invalidWords(words: Array(invalidWords)) - } - - // Validate checksum - - let checksumBitsCount = mnemonicComponents.count / 3 - guard checksumBitsCount == entropyLength.cheksumBitsCount else { - throw MnemonicError.invalidCheksum - } - - let entropyBitsCount = concatenatedBits.count - checksumBitsCount - let entropyBits = String(concatenatedBits.prefix(entropyBitsCount)) - let checksumBits = String(concatenatedBits.suffix(checksumBitsCount)) - - guard let entropyData = Data(bitsString: entropyBits) else { - throw MnemonicError.invalidCheksum - } - let calculatedChecksumBits = entropyData - .getSha256() - .toBits() - .prefix(entropyLength.cheksumBitsCount) - .joined() - - guard calculatedChecksumBits == checksumBits else { - throw MnemonicError.invalidCheksum - } - } /// Parse a mnemonic. /// - Parameter mnemonicString: The mnemonic to parse /// - Returns: Menemonic components func parse(mnemonicString: String) throws -> [String] { - let regex = try NSRegularExpression(pattern: "[a-zA-Z]+") + let regex = try NSRegularExpression(pattern: "\\p{L}+") let range = NSRange(location: 0, length: mnemonicString.count) let matches = regex.matches(in: mnemonicString, range: range) let components = matches.compactMap { result -> String? in @@ -159,7 +161,6 @@ public struct BIP39 { return components } - /// Validate wordlist by the first word /// - Parameter mnemonicComponents: Menemonic components to use /// - Returns: The Wordlist, selected by the first word @@ -167,7 +168,6 @@ public struct BIP39 { return try getWordlist(by: mnemonicComponents[0]).wordlist } - /// Convert mnemonic componets to a sungle string, splitted by spaces /// - Parameter mnemonicComponents: Menemonic components to use /// - Returns: The mnemonic string From e18e99cec179ff4e6a5dde72683cbe02389e1f39 Mon Sep 17 00:00:00 2001 From: Alexander Osokin Date: Fri, 17 Mar 2023 18:14:58 +0500 Subject: [PATCH 22/24] IOS-3059 Update podspec --- TangemSdk.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TangemSdk.podspec b/TangemSdk.podspec index b8858308f..b542623d7 100644 --- a/TangemSdk.podspec +++ b/TangemSdk.podspec @@ -54,6 +54,6 @@ Tangem is a Swiss-based secure hardware wallet manufacturer that enables blockch s.resource_bundles = { 'TangemSdk' => ['TangemSdk/TangemSdk/**/*.lproj/*.strings', 'TangemSdk/TangemSdk/Haptics/*.ahap', - 'TangemSdk/TangemSdk/Common/BIP39/Wordlists/*.txt']} + 'TangemSdk/TangemSdk/**/Wordlists/*.txt']} end From 444d689e22dc48807ef353f5753edaae30395d0e Mon Sep 17 00:00:00 2001 From: Alexander Osokin Date: Fri, 17 Mar 2023 18:18:19 +0500 Subject: [PATCH 23/24] IOS-3059 Open initializer --- TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift b/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift index 225c4aea5..d1abeec14 100644 --- a/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift +++ b/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift @@ -11,6 +11,9 @@ import Foundation // https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki @available(iOS 13.0, *) public struct BIP39 { + + public init() {} + /// Validate a mnemonic. /// - Parameter mnemonicComponents: Menemonic components to use public func validate(mnemonicComponents: [String]) throws { From ccf272ae5a9e9f7fa26f0491688bd9d74e69ee81 Mon Sep 17 00:00:00 2001 From: Alexander Osokin Date: Fri, 17 Mar 2023 18:18:59 +0500 Subject: [PATCH 24/24] IOS-3059 Remowe extra whitespases --- TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift b/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift index d1abeec14..eb966f205 100644 --- a/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift +++ b/TangemSdk/TangemSdk/Crypto/BIP39/BIP39.swift @@ -13,7 +13,7 @@ import Foundation public struct BIP39 { public init() {} - + /// Validate a mnemonic. /// - Parameter mnemonicComponents: Menemonic components to use public func validate(mnemonicComponents: [String]) throws { @@ -142,8 +142,6 @@ public struct BIP39 { return words } - - /// Parse a mnemonic. /// - Parameter mnemonicString: The mnemonic to parse /// - Returns: Menemonic components