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:
Francisco Gindre 2020-09-09 14:36:34 -03:00 committed by GitHub
parent 939b557c5d
commit f1377b9321
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 562 additions and 183 deletions

View File

@ -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

View File

@ -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">

View File

@ -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>

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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))
}
}