Fix: validate checksum bug + tests (#7)
* Fix: validate checksum + tests * Changes per Review: make all cryptographic derivation functions throwing. Add tests * Fix PR Comments * PR fixes * bump version to 2.0.0
This commit is contained in:
parent
939b557c5d
commit
f1377b9321
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = "MnemonicSwift"
|
||||
s.version = "1.0.0"
|
||||
s.version = "2.0.0"
|
||||
s.summary = "A Swift implementation of BIP39 Mnemonics"
|
||||
s.description = <<-DESC
|
||||
MnemonicSwift provides a Swift implementation of BIP39 using CriptoKit
|
||||
|
|
|
@ -26,7 +26,18 @@
|
|||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
codeCoverageEnabled = "YES"
|
||||
onlyGenerateCoverageForSpecifiedTargets = "YES">
|
||||
<CodeCoverageTargets>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3965BB285FF94319B8863435"
|
||||
BuildableName = "MnemonicSwift.framework"
|
||||
BlueprintName = "MnemonicSwift_iOS"
|
||||
ReferencedContainer = "container:MnemonicSwift.xcodeproj">
|
||||
</BuildableReference>
|
||||
</CodeCoverageTargets>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1160"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3A14B55BA3F3716B46D8CA3F"
|
||||
BuildableName = "MnemonicSwift.framework"
|
||||
BlueprintName = "MnemonicSwift_macOS"
|
||||
ReferencedContainer = "container:MnemonicSwift.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
codeCoverageEnabled = "YES"
|
||||
onlyGenerateCoverageForSpecifiedTargets = "YES">
|
||||
<CodeCoverageTargets>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3A14B55BA3F3716B46D8CA3F"
|
||||
BuildableName = "MnemonicSwift.framework"
|
||||
BlueprintName = "MnemonicSwift_macOS"
|
||||
ReferencedContainer = "container:MnemonicSwift.xcodeproj">
|
||||
</BuildableReference>
|
||||
</CodeCoverageTargets>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "6C3E5C33EDE0FF1268354BCC"
|
||||
BuildableName = "MnemonicSwiftTests.xctest"
|
||||
BlueprintName = "MnemonicSwiftTests_macOS"
|
||||
ReferencedContainer = "container:MnemonicSwift.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3A14B55BA3F3716B46D8CA3F"
|
||||
BuildableName = "MnemonicSwift.framework"
|
||||
BlueprintName = "MnemonicSwift_macOS"
|
||||
ReferencedContainer = "container:MnemonicSwift.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
|
@ -17,14 +17,45 @@ public enum MnemonicLanguageType {
|
|||
}
|
||||
}
|
||||
|
||||
public enum MnemonicError: Error {
|
||||
case wrongWordCount
|
||||
case checksumError
|
||||
case invalidWord(word: String)
|
||||
case unsupportedLanguage
|
||||
case invalidHexstring
|
||||
case invalidBitString
|
||||
case invalidInput
|
||||
case entropyCreationFailed
|
||||
}
|
||||
|
||||
public enum WordCount: Int {
|
||||
case twelve = 12
|
||||
case fifteen = 15
|
||||
case eighteen = 18
|
||||
case twentyOne = 21
|
||||
case twentFour = 24
|
||||
|
||||
var bitLength: Int {
|
||||
self.rawValue / 3 * 32
|
||||
}
|
||||
|
||||
var checksumLength: Int {
|
||||
self.rawValue / 3
|
||||
}
|
||||
}
|
||||
|
||||
public enum Mnemonic {
|
||||
/// Generate a mnemonic from the given hex string in the given language.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - hexString: The hex string to generate a mnemonic from.
|
||||
/// - language: The language to use. Default is english.
|
||||
public static func mnemonicString(from hexString: String, language: MnemonicLanguageType = .english) -> String? {
|
||||
let seedData = hexString.mnemonicData()
|
||||
/// - Returns: the mnemonic string or nil if input is invalid
|
||||
/// - Throws:
|
||||
/// - `MnemonicError.InvalidHexString`: when an invalid string is given
|
||||
/// - `MnemonicError.invalidBitString` when the resulting bitstring generates an invalid word index
|
||||
static func mnemonicString(from hexString: String, language: MnemonicLanguageType = .english) throws -> String {
|
||||
guard let seedData = hexString.mnemonicData() else { throw MnemonicError.invalidHexstring }
|
||||
let hashData = SHA256.hash(data: seedData)
|
||||
let checkSum = hashData.bytes.toBitArray()
|
||||
var seedBits = seedData.toBitArray()
|
||||
|
@ -43,40 +74,53 @@ public enum Mnemonic {
|
|||
let subArray = seedBits[startIndex ..< startIndex + length]
|
||||
let subString = subArray.joined(separator: "")
|
||||
|
||||
let index = Int(strtoul(subString, nil, 2))
|
||||
guard let index = Int(subString, radix: 2) else {
|
||||
throw MnemonicError.invalidBitString
|
||||
}
|
||||
mnemonic.append(words[index])
|
||||
}
|
||||
return mnemonic.joined(separator: " ")
|
||||
}
|
||||
|
||||
/// Generate a deterministic seed string from the given inputs.
|
||||
/// Generate a deterministic seed string from a Mnemonic String.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - mnemonic: The mnemonic to use.
|
||||
/// - iterations: The iterations to perform in the PBKDF2 algorithm. Default is 2048.
|
||||
/// - passphrase: An optional passphrase. Default is the empty string.
|
||||
/// - language: The language to use. Default is english.
|
||||
/// - Returns: hexString representing the deterministic seed bytes
|
||||
/// - Throws: `MnemonicError.checksumError` if checksum fails, `MnemonicError.invalidInput` if received input is invalid
|
||||
public static func deterministicSeedString(
|
||||
from mnemonic: String,
|
||||
iterations: Int = 2_048,
|
||||
passphrase: String = "",
|
||||
language: MnemonicLanguageType = .english
|
||||
) -> String? {
|
||||
deterministicSeedBytes(from: mnemonic, iterations: iterations, passphrase: passphrase, language: language)?.hexString
|
||||
) throws -> String {
|
||||
try deterministicSeedBytes(from: mnemonic, iterations: iterations, passphrase: passphrase, language: language).hexString
|
||||
}
|
||||
|
||||
/// Generate a deterministic seed bytes from a Mnemonic String.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - mnemonic: The mnemonic to use.
|
||||
/// - iterations: The iterations to perform in the PBKDF2 algorithm. Default is 2048.
|
||||
/// - passphrase: An optional passphrase. Default is the empty string.
|
||||
/// - language: The language to use. Default is english.
|
||||
/// - Returns: a byte array representing the deterministic seed bytes
|
||||
/// - Throws: `MnemonicError.checksumError` if checksum fails, `MnemonicError.invalidInput` if received input is invalid
|
||||
public static func deterministicSeedBytes(
|
||||
from mnemonic: String,
|
||||
iterations: Int = 2_048,
|
||||
passphrase: String = "",
|
||||
language: MnemonicLanguageType = .english
|
||||
) -> [UInt8]? {
|
||||
guard self.validate(mnemonic: mnemonic),
|
||||
let normalizedData = self.normalized(string: mnemonic),
|
||||
let saltData = normalized(string: "mnemonic" + passphrase) else {
|
||||
return nil
|
||||
}
|
||||
) throws -> [UInt8] {
|
||||
let normalizedMnemonic = mnemonic.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
try self.validate(mnemonic: normalizedMnemonic)
|
||||
|
||||
let normalizedData = try self.normalizedString(normalizedMnemonic)
|
||||
let saltData = try self.normalizedString("mnemonic" + passphrase)
|
||||
let passwordBytes = normalizedData.map { Int8(bitPattern: $0) }
|
||||
|
||||
do {
|
||||
|
@ -84,7 +128,7 @@ public enum Mnemonic {
|
|||
try PKCS5.PBKDF2SHA512(password: passwordBytes, salt: [UInt8](saltData), iterations: iterations)
|
||||
return bytes
|
||||
} catch {
|
||||
return nil
|
||||
throw MnemonicError.invalidInput
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -93,55 +137,109 @@ public enum Mnemonic {
|
|||
/// - Parameters:
|
||||
/// - strength: The strength to use. This must be a multiple of 32.
|
||||
/// - language: The language to use. Default is english.
|
||||
public static func generateMnemonic(strength: Int, language: MnemonicLanguageType = .english)
|
||||
-> String? {
|
||||
/// - Returns: the random mnemonic phrase of the given strenght and language or `nil` if the strength is invalid or an error occurs
|
||||
/// - Throws:
|
||||
/// - `MnemonicError.InvalidInput` if stregth is invalid in the terms of BIP-39
|
||||
/// - `MnemonicError.entropyCreationFailed` if random bytes created for entropy fails
|
||||
/// - `MnemonicError.InvalidHexString` when an invalid string is given
|
||||
/// - `MnemonicError.invalidBitString` when the resulting bitstring generates an invalid word index
|
||||
public static func generateMnemonic(strength: Int, language: MnemonicLanguageType = .english) throws
|
||||
-> String {
|
||||
guard strength % 32 == 0 else {
|
||||
return nil
|
||||
throw MnemonicError.invalidInput
|
||||
}
|
||||
|
||||
let count = strength / 8
|
||||
var bytes = [UInt8](repeating: 0, count: count)
|
||||
|
||||
guard SecRandomCopyBytes(kSecRandomDefault, count, &bytes) == errSecSuccess else { return nil }
|
||||
guard SecRandomCopyBytes(kSecRandomDefault, count, &bytes) == errSecSuccess else {
|
||||
throw MnemonicError.entropyCreationFailed
|
||||
}
|
||||
|
||||
return mnemonicString(from: bytes.hexString, language: language)
|
||||
return try mnemonicString(from: bytes.hexString, language: language)
|
||||
}
|
||||
|
||||
/// Validate that the given string is a valid mnemonic.
|
||||
public static func validate(mnemonic: String) -> Bool {
|
||||
let normalizedMnemonic = mnemonic.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let mnemonicComponents = normalizedMnemonic.components(separatedBy: " ")
|
||||
/// Validate that the given string is a valid mnemonic phrase according to BIP-39
|
||||
/// - Parameters:
|
||||
/// - mnemonic: a mnemonic phrase string
|
||||
/// - Throws:
|
||||
/// - `MnemonicError.wrongWordCount` if the word count is invalid
|
||||
/// - `MnemonicError.invalidWord(word: word)` this phase as a word that's not represented in this library's vocabulary for the detected language.
|
||||
/// - `MnemonicError.unsupportedLanguage` if the given phrase language isn't supported or couldn't be infered
|
||||
/// - `throw MnemonicError.checksumError` if the given phrase has an invalid checksum
|
||||
public static func validate(mnemonic: String) throws {
|
||||
let mnemonicComponents = mnemonic.components(separatedBy: " ")
|
||||
guard !mnemonicComponents.isEmpty else {
|
||||
return false
|
||||
throw MnemonicError.wrongWordCount
|
||||
}
|
||||
|
||||
// Use the first component of the mnemonic to determine the language, then make sure all
|
||||
// subsequent components are in that language.
|
||||
if String.englishMnemonics.contains(mnemonicComponents[0]) {
|
||||
for mnemonicComponent in mnemonicComponents {
|
||||
guard String.englishMnemonics.contains(mnemonicComponent) else {
|
||||
return false
|
||||
}
|
||||
guard let wordCount = WordCount(rawValue: mnemonicComponents.count) else {
|
||||
throw MnemonicError.wrongWordCount
|
||||
}
|
||||
|
||||
// determine the language of the seed or fail
|
||||
let language = try determineLanguage(from: mnemonicComponents)
|
||||
let vocabulary = language.words()
|
||||
|
||||
// generate indices array
|
||||
var seedBits = ""
|
||||
for word in mnemonicComponents {
|
||||
guard let indexInVocabulary = vocabulary.firstIndex(of: word) else {
|
||||
throw MnemonicError.invalidWord(word: word)
|
||||
}
|
||||
return true
|
||||
} else if String.chineseMnemonics.contains(mnemonicComponents[0]) {
|
||||
for mnemonicComponent in mnemonicComponents {
|
||||
guard String.chineseMnemonics.contains(mnemonicComponent) else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
let binaryString = String(indexInVocabulary, radix: 2).pad(toSize: 11)
|
||||
|
||||
seedBits.append(contentsOf: binaryString)
|
||||
}
|
||||
|
||||
let checksumLength = mnemonicComponents.count / 3
|
||||
|
||||
guard checksumLength == wordCount.checksumLength else {
|
||||
throw MnemonicError.checksumError
|
||||
}
|
||||
|
||||
let dataBitsLength = seedBits.count - checksumLength
|
||||
|
||||
let dataBits = String(seedBits.prefix(dataBitsLength))
|
||||
let checksumBits = String(seedBits.suffix(checksumLength))
|
||||
|
||||
guard let dataBytes = dataBits.bitStringToBytes() else {
|
||||
throw MnemonicError.checksumError
|
||||
}
|
||||
|
||||
let hash = SHA256.hash(data: dataBytes)
|
||||
let hashBits = hash.bytes.toBitArray().joined(separator: "").prefix(checksumLength)
|
||||
|
||||
guard hashBits == checksumBits else {
|
||||
throw MnemonicError.checksumError
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static func determineLanguage(from mnemonicWords: [String]) throws -> MnemonicLanguageType {
|
||||
guard mnemonicWords.count > 0 else {
|
||||
throw MnemonicError.wrongWordCount
|
||||
}
|
||||
|
||||
if String.englishMnemonics.contains(mnemonicWords[0]) {
|
||||
return .english
|
||||
} else if String.chineseMnemonics.contains(mnemonicWords[0]) {
|
||||
return .chinese
|
||||
} else {
|
||||
return false
|
||||
throw MnemonicError.unsupportedLanguage
|
||||
}
|
||||
}
|
||||
|
||||
/// Change a string into data.
|
||||
fileprivate static func normalized(string: String) -> Data? {
|
||||
/// - Parameter string: the string to convert
|
||||
/// - Returns: the utf8 encoded data
|
||||
/// - Throws: `MnemonicError.invalidInput` if the given String cannot be converted to Data
|
||||
static func normalizedString(_ string: String) throws -> Data {
|
||||
guard let data = string.data(using: .utf8, allowLossyConversion: true),
|
||||
let dataString = String(data: data, encoding: .utf8),
|
||||
let normalizedData = dataString.data(using: .utf8, allowLossyConversion: false) else {
|
||||
return nil
|
||||
throw MnemonicError.invalidInput
|
||||
}
|
||||
return normalizedData
|
||||
}
|
||||
|
@ -150,9 +248,7 @@ public enum Mnemonic {
|
|||
extension PKCS5 {
|
||||
public static func PBKDF2SHA512(password: String, salt: String, iterations: Int = 2_048, keyLength: Int = 64) throws -> Array<UInt8> {
|
||||
|
||||
guard let saltData = Mnemonic.normalized(string: salt) else {
|
||||
throw PKCS5.Error.invalidInput
|
||||
}
|
||||
let saltData = try Mnemonic.normalizedString(salt)
|
||||
|
||||
return try PBKDF2SHA512(password: password.utf8.map({ Int8(bitPattern: $0) }), salt: [UInt8](saltData), iterations: iterations, keyLength: keyLength)
|
||||
}
|
||||
|
|
|
@ -2,24 +2,54 @@
|
|||
// Copyright Electric Coin Company, 2020
|
||||
import Foundation
|
||||
|
||||
public extension String {
|
||||
func mnemonicData() -> Data {
|
||||
let length = self.count
|
||||
let dataLength = length / 2
|
||||
var dataToReturn = Data(capacity: dataLength)
|
||||
extension String {
|
||||
func mnemonicData() -> Data? {
|
||||
guard self.count % 2 == 0 else { return nil }
|
||||
let length = self.count
|
||||
let dataLength = length / 2
|
||||
var dataToReturn = Data(capacity: dataLength)
|
||||
|
||||
var outIndex = 0
|
||||
var outChars = ""
|
||||
for (_, char) in enumerated() {
|
||||
outChars += String(char)
|
||||
if outIndex % 2 == 1 {
|
||||
let i: UInt8 = UInt8(strtoul(outChars, nil, 16))
|
||||
dataToReturn.append(i)
|
||||
outChars = ""
|
||||
}
|
||||
outIndex += 1
|
||||
var outIndex = 0
|
||||
var outChars = ""
|
||||
for (_, char) in enumerated() {
|
||||
outChars += String(char)
|
||||
if outIndex % 2 == 1 {
|
||||
guard let i = UInt8(outChars, radix: 16) else { return nil }
|
||||
dataToReturn.append(i)
|
||||
outChars = ""
|
||||
}
|
||||
outIndex += 1
|
||||
}
|
||||
|
||||
return dataToReturn
|
||||
}
|
||||
|
||||
return dataToReturn
|
||||
}
|
||||
func pad(toSize: Int) -> String {
|
||||
guard self.count < toSize else { return self }
|
||||
var padded = self
|
||||
for _ in 0..<(toSize - self.count) {
|
||||
padded = "0" + padded
|
||||
}
|
||||
return padded
|
||||
}
|
||||
|
||||
/// turns an array of "0"s and "1"s into bytes. fails if count is not modulus of 8
|
||||
func bitStringToBytes() -> Data? {
|
||||
let length = 8
|
||||
guard self.count % length == 0 else {
|
||||
return nil
|
||||
}
|
||||
var data = Data(capacity: self.count)
|
||||
|
||||
for i in 0 ..< self.count / length {
|
||||
let startIdx = self.index(self.startIndex, offsetBy: i * length)
|
||||
let subArray = self[startIdx ..< self.index(startIdx, offsetBy: length)]
|
||||
let subString = String(subArray)
|
||||
guard let byte = UInt8(subString, radix: 2) else {
|
||||
return nil
|
||||
}
|
||||
data.append(byte)
|
||||
}
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,156 +1,310 @@
|
|||
// Copyright Keefer Taylor, 2018
|
||||
// Copyright Electric Coin Company, 2020
|
||||
import MnemonicSwift
|
||||
@testable import MnemonicSwift
|
||||
import XCTest
|
||||
|
||||
class MnemonicSwiftTests: XCTestCase {
|
||||
/// Indices in the input file.
|
||||
private let hexRepresentationIndex = 0
|
||||
private let mnenomicStringIndex = 1
|
||||
private let deterministicSeedStringIndex = 2
|
||||
/// Indices in the input file.
|
||||
private let hexRepresentationIndex = 0
|
||||
private let mnenomicStringIndex = 1
|
||||
private let deterministicSeedStringIndex = 2
|
||||
|
||||
/// Named arrays in the test file
|
||||
private let englishTestCases = "english"
|
||||
/// Named arrays in the test file
|
||||
private let englishTestCases = "english"
|
||||
|
||||
/// Passphrase
|
||||
private let passphrase = "TREZOR"
|
||||
/// Passphrase
|
||||
private let passphrase = "TREZOR"
|
||||
|
||||
/// Test that MnemonicSwift can generate mnemonic strings from hex representations.
|
||||
func testGenerateMnemonicFromHex() {
|
||||
guard let vectors = MnemonicSwiftTests.dictionaryFromTestInputFile(),
|
||||
let testCases = vectors[englishTestCases] as? [[String]] else {
|
||||
XCTFail("Failed to parse input file.")
|
||||
return
|
||||
/// Test that MnemonicSwift can generate mnemonic strings from hex representations.
|
||||
func testGenerateMnemonicFromHex() throws {
|
||||
guard let vectors = MnemonicSwiftTests.dictionaryFromTestInputFile(),
|
||||
let testCases = vectors[englishTestCases] as? [[String]] else {
|
||||
XCTFail("Failed to parse input file.")
|
||||
return
|
||||
}
|
||||
|
||||
for testCase in testCases {
|
||||
let expectedMnemonicString = testCase[mnenomicStringIndex]
|
||||
let hexRepresentation = testCase[hexRepresentationIndex]
|
||||
let mnemonicString = try Mnemonic.mnemonicString(from: hexRepresentation)
|
||||
|
||||
XCTAssertEqual(mnemonicString, expectedMnemonicString)
|
||||
}
|
||||
}
|
||||
|
||||
for testCase in testCases {
|
||||
let expectedMnemonicString = testCase[mnenomicStringIndex]
|
||||
let hexRepresentation = testCase[hexRepresentationIndex]
|
||||
let mnemonicString = Mnemonic.mnemonicString(from: hexRepresentation)
|
||||
/// Test that MnemonicSwift can generate deterministic seed strings strings without a passphrase.
|
||||
func testGenerateDeterministicSeedStringWithPassphrase() throws {
|
||||
guard let vectors = MnemonicSwiftTests.dictionaryFromTestInputFile(),
|
||||
let testCases = vectors[englishTestCases] as? [[String]] else {
|
||||
XCTFail("Failed to parse input file.")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(mnemonicString, expectedMnemonicString)
|
||||
}
|
||||
}
|
||||
for testCase in testCases {
|
||||
let mnemonicString = testCase[mnenomicStringIndex]
|
||||
let expectedDeterministicSeedString = testCase[deterministicSeedStringIndex]
|
||||
|
||||
/// Test that MnemonicSwift can generate deterministic seed strings strings without a passphrase.
|
||||
func testGenerateDeterministicSeedStringWithPassphrase() {
|
||||
guard let vectors = MnemonicSwiftTests.dictionaryFromTestInputFile(),
|
||||
let testCases = vectors[englishTestCases] as? [[String]] else {
|
||||
XCTFail("Failed to parse input file.")
|
||||
return
|
||||
XCTAssertNoThrow({
|
||||
let deterministicSeedString: String = try Mnemonic.deterministicSeedString(from: mnemonicString, passphrase: self.passphrase)
|
||||
XCTAssertEqual(deterministicSeedString, expectedDeterministicSeedString)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for testCase in testCases {
|
||||
let mnemonicString = testCase[mnenomicStringIndex]
|
||||
let expectedDeterministicSeedString = testCase[deterministicSeedStringIndex]
|
||||
static func dictionaryFromTestInputFile() -> [String: Any]? {
|
||||
let testBundle = Bundle(for: self)
|
||||
guard let url = testBundle.url(forResource: "vectors", withExtension: "json") else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let deterministicSeedString = Mnemonic.deterministicSeedString(from: mnemonicString, passphrase: passphrase)
|
||||
XCTAssertEqual(deterministicSeedString, expectedDeterministicSeedString)
|
||||
}
|
||||
}
|
||||
|
||||
static func dictionaryFromTestInputFile() -> [String: Any]? {
|
||||
let testBundle = Bundle(for: self)
|
||||
guard let url = testBundle.url(forResource: "vectors", withExtension: "json") else {
|
||||
return nil
|
||||
do {
|
||||
let data = try Data(contentsOf: url)
|
||||
let options: JSONSerialization.ReadingOptions = [.allowFragments, .mutableContainers, .mutableLeaves]
|
||||
guard let parsedDictionary =
|
||||
try JSONSerialization.jsonObject(with: data, options: options) as? [String: Any] else {
|
||||
return nil
|
||||
}
|
||||
return parsedDictionary
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: url)
|
||||
let options: JSONSerialization.ReadingOptions = [.allowFragments, .mutableContainers, .mutableLeaves]
|
||||
guard let parsedDictionary =
|
||||
try JSONSerialization.jsonObject(with: data, options: options) as? [String: Any] else {
|
||||
return nil
|
||||
}
|
||||
return parsedDictionary
|
||||
} catch {
|
||||
return nil
|
||||
/// Test mnemonic generation in english.
|
||||
func testGenerateMnemonic() {
|
||||
|
||||
XCTAssertNoThrow(try Mnemonic.generateMnemonic(strength: 32))
|
||||
}
|
||||
}
|
||||
|
||||
/// Test mnemonic generation in english.
|
||||
func testGenerateMnemonic() {
|
||||
let mnemonic = Mnemonic.generateMnemonic(strength: 32)
|
||||
XCTAssertNotNil(mnemonic)
|
||||
}
|
||||
/// Prove that functions work in chinese as well.
|
||||
func testGenerateMnemonicChinese() {
|
||||
XCTAssertNoThrow(try Mnemonic.generateMnemonic(strength: 32, language: .chinese))
|
||||
}
|
||||
|
||||
/// Prove that functions work in chinese as well.
|
||||
func testGenerateMnemonicChinese() {
|
||||
let chineseMnemonic = Mnemonic.generateMnemonic(strength: 32, language: .chinese)
|
||||
XCTAssertNotNil(chineseMnemonic)
|
||||
}
|
||||
/// Test input strengths for mnemonic generation.
|
||||
func testMnemonicGenerationStrength() {
|
||||
|
||||
/// Test input strengths for mnemonic generation.
|
||||
func testMnemonicGenerationStrength() {
|
||||
let mnemonic32 = Mnemonic.generateMnemonic(strength: 32)
|
||||
let mnemonic64 = Mnemonic.generateMnemonic(strength: 32)
|
||||
XCTAssertNotNil(mnemonic32)
|
||||
XCTAssertNotNil(mnemonic64)
|
||||
XCTAssertNoThrow(try {
|
||||
XCTAssertEqual(try Mnemonic.generateMnemonic(strength: 32).components(separatedBy: " ").count, 3)
|
||||
}())
|
||||
|
||||
let mnemonic16 = Mnemonic.generateMnemonic(strength: 32)
|
||||
XCTAssertNotNil(mnemonic16)
|
||||
}
|
||||
XCTAssertNoThrow(try {
|
||||
XCTAssertEqual(try Mnemonic.generateMnemonic(strength: 64).components(separatedBy: " ").count, 6)
|
||||
}())
|
||||
|
||||
/// Test valid chinese and english mnemonics are determined to be valid.
|
||||
func testValidEnglishAndChineseMnemonics() {
|
||||
let englishMnemonic =
|
||||
XCTAssertNoThrow(try {
|
||||
XCTAssertEqual(try Mnemonic.generateMnemonic(strength: 128).components(separatedBy: " ").count, 12)
|
||||
}())
|
||||
|
||||
XCTAssertNoThrow(try {
|
||||
XCTAssertEqual(try Mnemonic.generateMnemonic(strength: 160).components(separatedBy: " ").count, 15)
|
||||
}())
|
||||
|
||||
XCTAssertNoThrow(try {
|
||||
XCTAssertEqual(try Mnemonic.generateMnemonic(strength: 192).components(separatedBy: " ").count, 18)
|
||||
}())
|
||||
|
||||
XCTAssertNoThrow(try {
|
||||
XCTAssertEqual(try Mnemonic.generateMnemonic(strength: 224).components(separatedBy: " ").count, 21)
|
||||
}())
|
||||
|
||||
XCTAssertNoThrow(try {
|
||||
XCTAssertEqual(try Mnemonic.generateMnemonic(strength: 256).components(separatedBy: " ").count, 24)
|
||||
}())
|
||||
|
||||
}
|
||||
|
||||
/// Test valid chinese and english mnemonics are determined to be Invalid.
|
||||
func testInValidEnglishAndChineseMnemonics() {
|
||||
let englishMnemonic =
|
||||
"pear peasant pelican pen pear peasant pelican pen pear peasant pelican pen pear peasant pelican pen"
|
||||
let chineseMnemonic = "路 级 少 图 路 级 少 图 路 级 少 图 路 级 少 图"
|
||||
let chineseMnemonic = "式 扬 它 锦 亦 桥 晋 尼 登 串 焦 五 溶 寿 沿 能 妹 少 旅 冬 乳 承"
|
||||
|
||||
XCTAssertTrue(Mnemonic.validate(mnemonic: englishMnemonic))
|
||||
XCTAssertTrue(Mnemonic.validate(mnemonic: chineseMnemonic))
|
||||
}
|
||||
XCTAssertThrowsError(try Mnemonic.validate(mnemonic: englishMnemonic))
|
||||
XCTAssertThrowsError(try Mnemonic.validate(mnemonic: chineseMnemonic))
|
||||
}
|
||||
|
||||
/// Test invalid chinese and english mnemonics are determined to be invalid.
|
||||
func testInvalidEnglishAndChineseMnemonics() {
|
||||
let englishMnemonic = "slacktivist snacktivity snuggie"
|
||||
let chineseMnemonic = "亂 語"
|
||||
/// Test invalid chinese and english mnemonics are determined to be invalid.
|
||||
func testInvalidEnglishAndChineseMnemonics() {
|
||||
let englishMnemonic = "slacktivist snacktivity snuggie"
|
||||
let chineseMnemonic = "亂 語"
|
||||
|
||||
XCTAssertFalse(Mnemonic.validate(mnemonic: englishMnemonic))
|
||||
XCTAssertFalse(Mnemonic.validate(mnemonic: chineseMnemonic))
|
||||
}
|
||||
XCTAssertThrowsError(try Mnemonic.validate(mnemonic: englishMnemonic))
|
||||
XCTAssertThrowsError(try Mnemonic.validate(mnemonic: chineseMnemonic))
|
||||
}
|
||||
|
||||
/// Test the empty string is determined to be an invalid mnemonic.
|
||||
func testEmptyStringValidation() {
|
||||
XCTAssertFalse(Mnemonic.validate(mnemonic: ""))
|
||||
}
|
||||
/// Test the empty string is determined to be an invalid mnemonic.
|
||||
func testEmptyStringValidation() {
|
||||
XCTAssertThrowsError(try Mnemonic.validate(mnemonic: ""))
|
||||
}
|
||||
|
||||
/// Test that strings in an unknown language are determined to be invalid.
|
||||
func testUnknownLanguageValidation() {
|
||||
let spanishMnemonic =
|
||||
/// Test that strings in an unknown language are determined to be invalid.
|
||||
func testUnknownLanguageValidation() {
|
||||
let spanishMnemonic =
|
||||
"pera campesina pelican pen pera campesina pelican pen pera campesina pelican pen pera campesina pelican pen"
|
||||
XCTAssertFalse(Mnemonic.validate(mnemonic: spanishMnemonic))
|
||||
}
|
||||
XCTAssertThrowsError(try Mnemonic.validate(mnemonic: spanishMnemonic))
|
||||
}
|
||||
|
||||
/// Test that strings of mixed case are determined to be valid.
|
||||
func testMixedCaseValidation() {
|
||||
let mixedCaseMnemonic = "pear PEASANT PeLiCaN pen"
|
||||
XCTAssertTrue(Mnemonic.validate(mnemonic: mixedCaseMnemonic))
|
||||
}
|
||||
/// Test that strings of mixed case are determined to be invalid.
|
||||
func testMixedCaseValidation() {
|
||||
let mixedCaseMnemonic = "pear PEASANT PeLiCaN pen"
|
||||
XCTAssertThrowsError(try Mnemonic.validate(mnemonic: mixedCaseMnemonic))
|
||||
}
|
||||
|
||||
/// Test mixed language mnemonics.
|
||||
func testMixedLanguageMnemonicValidation() {
|
||||
let mixedLanguageMnemonic = "pear peasant pelican pen 路 级 少 图"
|
||||
XCTAssertFalse(Mnemonic.validate(mnemonic: mixedLanguageMnemonic))
|
||||
}
|
||||
/// Test mixed language mnemonics.
|
||||
func testMixedLanguageMnemonicValidation() {
|
||||
let mixedLanguageMnemonic = "pear peasant pelican pen 路 级 少 图"
|
||||
XCTAssertThrowsError(try Mnemonic.validate(mnemonic: mixedLanguageMnemonic))
|
||||
}
|
||||
|
||||
/// Test that strings padded with whitespace are determined to be valid.
|
||||
func testWhitespacePaddedValidation() {
|
||||
let whitespacePaddedMnemonic = " pear peasant pelican pen\t\t\n"
|
||||
XCTAssertTrue(Mnemonic.validate(mnemonic: whitespacePaddedMnemonic))
|
||||
}
|
||||
/// Test that strings padded with whitespace are determined to be valid.
|
||||
func testWhitespacePaddedValidation() {
|
||||
let whitespacePaddedMnemonic = " flash tobacco obey genius army stove desk anchor quarter reflect chalk caution\t\t\n"
|
||||
XCTAssertThrowsError(try Mnemonic.validate(mnemonic: whitespacePaddedMnemonic))
|
||||
|
||||
/// Test an valid mnemonic generates a seed string.
|
||||
func testDeterministicSeedStringValidMnemonic() {
|
||||
let invalidMnemonic =
|
||||
"pear peasant pelican pen pear peasant pelican pen pear peasant pelican pen pear peasant pelican pen"
|
||||
XCTAssertNotNil(Mnemonic.deterministicSeedString(from: invalidMnemonic))
|
||||
}
|
||||
XCTAssertNoThrow(try Mnemonic.deterministicSeedString(from: whitespacePaddedMnemonic))
|
||||
}
|
||||
|
||||
/// Test an valid mnemonic generates a seed string.
|
||||
func testDeterministicSeedStringVisuallyValidButYetInvalidMnemonic() {
|
||||
let invalidMnemonic =
|
||||
"pear peasant pelican pen pear peasant pelican pen pear peasant pelican pen pear peasant pelican pen"
|
||||
XCTAssertThrowsError(try Mnemonic.deterministicSeedString(from: invalidMnemonic))
|
||||
}
|
||||
|
||||
/// Test an invalid mnemonic does not generate a seed string.
|
||||
func testDeterministicSeedStringInvalidMnemonic() {
|
||||
let invalidMnemonic =
|
||||
"MnemonicSwift MnemonicSwift MnemonicSwift MnemonicSwift MnemonicSwift MnemonicSwift MnemonicSwift MnemonicSwift MnemonicSwift"
|
||||
XCTAssertThrowsError(try Mnemonic.deterministicSeedString(from: invalidMnemonic))
|
||||
}
|
||||
|
||||
func testValidWordCountChinese() {
|
||||
let mnemonic12 = "针 环 焦 译 脏 密 嘴 土 殿 钠 燕 仰"
|
||||
XCTAssertNoThrow(try Mnemonic.validate(mnemonic: mnemonic12))
|
||||
let mnemonic15 = "李 凉 暗 均 粒 卖 再 送 绳 勃 窗 丙 洁 危 位"
|
||||
XCTAssertNoThrow(try Mnemonic.validate(mnemonic: mnemonic15))
|
||||
let mnemonic18 = "蚀 司 档 截 硫 空 激 狱 型 影 湖 钙 柱 控 拜 元 条 扶"
|
||||
XCTAssertNoThrow(try Mnemonic.validate(mnemonic: mnemonic18))
|
||||
let mnemonic21 = "遭 霉 费 龙 依 固 征 乔 束 锋 盐 芳 走 他 声 废 带 如 套 迹 代"
|
||||
XCTAssertNoThrow(try Mnemonic.validate(mnemonic: mnemonic21))
|
||||
let mnemonic24 = "式 扬 技 书 它 锦 亦 桥 晋 尼 登 串 焦 五 溶 寿 沿 能 妹 少 旅 冬 乳 承"
|
||||
XCTAssertNoThrow(try Mnemonic.validate(mnemonic: mnemonic24))
|
||||
}
|
||||
|
||||
func testInvalidSmallWordCount() {
|
||||
let twoWordSeed = "flash tobacco"
|
||||
XCTAssertThrowsError(try Mnemonic.validate(mnemonic: twoWordSeed))
|
||||
let singleWordSeed = "flash"
|
||||
XCTAssertThrowsError(try Mnemonic.validate(mnemonic: singleWordSeed))
|
||||
}
|
||||
|
||||
func testValidWordCount() {
|
||||
let twelveWordSeed = "flash tobacco obey genius army stove desk anchor quarter reflect chalk caution"
|
||||
let fifteenWordSeed = "figure manual hunt oil unusual outer flee yellow cable bottom uncle okay deputy witness fire"
|
||||
let eighteenWordSeed = "cost raccoon apple hill success sight bag harvest lawsuit exact snow police camp faith weather squirrel defy dry"
|
||||
let twentyOneWordSeed = "beach allow aim neglect phone boring horror venture door crouch ecology tent bulb oval culture hat half easily crucial horse heart"
|
||||
let twentyFourWordSeed = "vanish dream art asset response click orphan patch property owner lawsuit sweet smoke bicycle grunt sentence dish tribe review soap chief soft bone race"
|
||||
|
||||
XCTAssertNoThrow(try Mnemonic.validate(mnemonic: twelveWordSeed), "error validating 12 word seed")
|
||||
XCTAssertNoThrow(try Mnemonic.validate(mnemonic: fifteenWordSeed), "error validating 15 word seed")
|
||||
XCTAssertNoThrow(try Mnemonic.validate(mnemonic: eighteenWordSeed), "error validating 18 word seed")
|
||||
XCTAssertNoThrow(try Mnemonic.validate(mnemonic: twentyOneWordSeed), "error validating 21 word seed")
|
||||
XCTAssertNoThrow(try Mnemonic.validate(mnemonic: twentyFourWordSeed), "error validating 24 word seed")
|
||||
}
|
||||
|
||||
func testInvalidWordCount() {
|
||||
let elevenWordSeed = "flash tobacco obey genius army stove desk anchor quarter reflect chalk"
|
||||
let fourteenWordSeed = "figure manual hunt oil unusual outer flee yellow cable bottom uncle okay deputy witness"
|
||||
let seventeenWordSeed = "cost raccoon apple hill success sight bag harvest lawsuit exact snow police camp faith weather squirrel defy"
|
||||
let twentyWordSeed = "beach aim neglect phone boring horror venture door crouch ecology tent bulb oval culture hat half easily crucial horse heart"
|
||||
let twentythreeWordSeed = "vanish dream art response click orphan patch property owner lawsuit sweet smoke bicycle grunt sentence dish tribe review soap chief soft bone race"
|
||||
|
||||
XCTAssertThrowsError(try Mnemonic.validate(mnemonic: elevenWordSeed))
|
||||
XCTAssertThrowsError(try Mnemonic.validate(mnemonic: fourteenWordSeed))
|
||||
XCTAssertThrowsError(try Mnemonic.validate(mnemonic: seventeenWordSeed))
|
||||
XCTAssertThrowsError(try Mnemonic.validate(mnemonic: twentyWordSeed))
|
||||
XCTAssertThrowsError(try Mnemonic.validate(mnemonic: twentythreeWordSeed))
|
||||
}
|
||||
|
||||
func testApparentlyValidSeedPhraseWithUppercaseCharacter() throws {
|
||||
let x = "human pulse approve subway climb stairs mind gentle raccoon warfare fog roast sponsor under absorb spirit hurdle animal original honey owner upper empower describe"
|
||||
let y = "Human pulse approve subway climb stairs mind gentle raccoon warfare fog roast sponsor under absorb spirit hurdle animal original honey owner upper empower describe"
|
||||
XCTAssertNoThrow(try Mnemonic.validate(mnemonic: x))
|
||||
XCTAssertThrowsError(try Mnemonic.validate(mnemonic: y))
|
||||
|
||||
XCTAssertNoThrow(try Mnemonic.deterministicSeedBytes(from: x))
|
||||
XCTAssertThrowsError(try Mnemonic.deterministicSeedBytes(from: y))
|
||||
|
||||
XCTAssertEqual(try Mnemonic.deterministicSeedBytes(from: x), try Mnemonic.deterministicSeedBytes(from: " " + x))
|
||||
XCTAssertEqual(try Mnemonic.deterministicSeedBytes(from: x), try Mnemonic.deterministicSeedBytes(from: x + "\n"))
|
||||
|
||||
}
|
||||
|
||||
func testSwapTwoWords() {
|
||||
let phrase = "human pulse approve subway climb stairs mind gentle raccoon warfare fog roast sponsor under absorb spirit hurdle animal original honey owner upper empower describe"
|
||||
var x = phrase.components(separatedBy: " ")
|
||||
let i = Int.random(in: 1 ..< x.count)
|
||||
let j = Int.random(in: 0 ..< i)
|
||||
x.swapAt(i, j)
|
||||
|
||||
let swappedPhrase = x.joined(separator: " ")
|
||||
|
||||
XCTAssertNoThrow(try Mnemonic.deterministicSeedBytes(from: phrase))
|
||||
XCTAssertThrowsError(try Mnemonic.deterministicSeedBytes(from: swappedPhrase))
|
||||
|
||||
}
|
||||
|
||||
func testBitStringArrayToData() {
|
||||
let validBitString = "10000000"+"00010000"+"00001111"+"11110000"
|
||||
|
||||
let uints: [UInt8] = [128, 16, 15, 240]
|
||||
let expectedDataArray = Data(uints)
|
||||
|
||||
let result = validBitString.bitStringToBytes()
|
||||
|
||||
XCTAssertNotNil(result)
|
||||
XCTAssertEqual(result, expectedDataArray)
|
||||
}
|
||||
|
||||
func testBitStringArrayToDataFailsOnIncorrectString() {
|
||||
|
||||
let bendersDream = "10000000"+"00010000"+"00020111"+"11110000"
|
||||
|
||||
let result = bendersDream.bitStringToBytes()
|
||||
|
||||
XCTAssertNil(result)
|
||||
|
||||
}
|
||||
|
||||
func testInvalidMnemonicData() {
|
||||
let validMnemonicData = "f1f1f1"
|
||||
|
||||
let invalidMnemonicData = "f1f1f1f"
|
||||
|
||||
let veryInvalidMnemonicData = "f1f1g1"
|
||||
let expectedData = Data([0xf1, 0xf1, 0xf1])
|
||||
|
||||
XCTAssertEqual(validMnemonicData.mnemonicData(), expectedData)
|
||||
|
||||
XCTAssertEqual(invalidMnemonicData.mnemonicData(), nil)
|
||||
|
||||
XCTAssertEqual(veryInvalidMnemonicData.mnemonicData(), nil)
|
||||
|
||||
}
|
||||
|
||||
func testPad() {
|
||||
// it should pad to givesize when size is less that target size
|
||||
XCTAssertEqual("0".pad(toSize: 8), "00000000")
|
||||
XCTAssertEqual("1".pad(toSize: 8), "00000001")
|
||||
XCTAssertEqual("10".pad(toSize: 8), "00000010")
|
||||
XCTAssertEqual("10".pad(toSize: 8), "00000010")
|
||||
|
||||
// it should not pad when size of the given string is same as target size
|
||||
XCTAssertEqual("00000010".pad(toSize: 8), "00000010")
|
||||
|
||||
// it should n ot pad whe size of the given string is greater that target size
|
||||
XCTAssertEqual("100000010".pad(toSize: 8), "100000010")
|
||||
}
|
||||
|
||||
/// Test an invalid mnemonic does not generate a seed string.
|
||||
func testDeterministicSeedStringInvalidMnemonic() {
|
||||
let invalidMnemonic =
|
||||
"MnemonicSwift MnemonicSwift MnemonicSwift MnemonicSwift MnemonicSwift MnemonicSwift MnemonicSwift MnemonicSwift MnemonicSwift"
|
||||
XCTAssertNil(Mnemonic.deterministicSeedString(from: invalidMnemonic))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue