diff --git a/enzevalos_iphone.xcodeproj/project.pbxproj b/enzevalos_iphone.xcodeproj/project.pbxproj index ef55c294dfbef8f4999444691e8789418f78ba4e..bfcfe04c6dd8ea115edc305cb3e043f7fad22235 100644 --- a/enzevalos_iphone.xcodeproj/project.pbxproj +++ b/enzevalos_iphone.xcodeproj/project.pbxproj @@ -152,6 +152,7 @@ 47F867E22052B48E00AA832F /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 47F867E12052B48E00AA832F /* libz.tbd */; }; 47F867E42052B49800AA832F /* libbz2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 47F867E32052B49800AA832F /* libbz2.tbd */; }; 50F2E7D66366C779705987A7 /* Pods_enzevalos_iphoneUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF67EF30BB065CC9C0D17940 /* Pods_enzevalos_iphoneUITests.framework */; }; + 676C2D3024321F8100B631B3 /* TyposquattingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 676C2D2F24321F8100B631B3 /* TyposquattingTests.swift */; }; 6789425F2430C3B300C746D1 /* MailComparison.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6789425E2430C3B300C746D1 /* MailComparison.swift */; }; 678942612430C3D600C746D1 /* Typosquatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 678942602430C3D600C746D1 /* Typosquatting.swift */; }; 678942632430C40600C746D1 /* MailComparisonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 678942622430C40600C746D1 /* MailComparisonTests.swift */; }; @@ -553,6 +554,7 @@ 48C250BB32BF11B683003BA1 /* Pods-enzevalos_iphone-enzevalos_iphoneUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-enzevalos_iphone-enzevalos_iphoneUITests.debug.xcconfig"; path = "../enzevalos_iphone_workspace/Pods/Target Support Files/Pods-enzevalos_iphone-enzevalos_iphoneUITests/Pods-enzevalos_iphone-enzevalos_iphoneUITests.debug.xcconfig"; sourceTree = "<group>"; }; 66E758F271CD65AB3E5FE7A7 /* Pods-enzevalos_iphoneUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-enzevalos_iphoneUITests.debug.xcconfig"; path = "../enzevalos_iphone_workspace/Pods/Target Support Files/Pods-enzevalos_iphoneUITests/Pods-enzevalos_iphoneUITests.debug.xcconfig"; sourceTree = "<group>"; }; 670159DF240FB4E800797FA5 /* enzevalos_iphone 9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "enzevalos_iphone 9.xcdatamodel"; sourceTree = "<group>"; }; + 676C2D2F24321F8100B631B3 /* TyposquattingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TyposquattingTests.swift; sourceTree = "<group>"; }; 6789425E2430C3B300C746D1 /* MailComparison.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailComparison.swift; sourceTree = "<group>"; }; 678942602430C3D600C746D1 /* Typosquatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Typosquatting.swift; sourceTree = "<group>"; }; 678942622430C40600C746D1 /* MailComparisonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailComparisonTests.swift; sourceTree = "<group>"; }; @@ -1177,6 +1179,7 @@ 988C9C5C240D507A006213F0 /* UrlStringExtensionTests.swift */, 71DF08972421520D00162B74 /* EmailStringExtensionTests.swift */, 678942622430C40600C746D1 /* MailComparisonTests.swift */, + 676C2D2F24321F8100B631B3 /* TyposquattingTests.swift */, ); path = phishing; sourceTree = "<group>"; @@ -2148,6 +2151,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 676C2D3024321F8100B631B3 /* TyposquattingTests.swift in Sources */, 8428A8831F436AC9007649A5 /* GamificationDataUnitTest.swift in Sources */, 71DF08982421520D00162B74 /* EmailStringExtensionTests.swift in Sources */, 3EC35F302003838E008BDF95 /* InvitationTests.swift in Sources */, diff --git a/enzevalos_iphone/StringExtension.swift b/enzevalos_iphone/StringExtension.swift index c9b8c118ed6d037a11935898bf5708a01b4502d8..4d06992f15aaa5e970cd1c8b9ec01f8c556531b0 100644 --- a/enzevalos_iphone/StringExtension.swift +++ b/enzevalos_iphone/StringExtension.swift @@ -180,8 +180,8 @@ extension String { } /** - Returns a list of E-Mail Addresses extracted from a given string - */ + Returns a list of E-Mail Addresses extracted from a given string + */ func findMailAddress() -> [String] { let splitString = self.split(separator: " ").map(String.init) var results:[String] = [] @@ -544,18 +544,6 @@ extension String { return domainEditDic } - /** - Compares two strings with each other. Return true if the edit distance is in the allowed range. - Not allowed distance: 0 < distance < allowedEditDistance. - */ - func isAllowedDistance(str: String, allowedEditDistance: Int) -> Bool { - let distance = self.levenshtein(str) - if distance < allowedEditDistance && distance != 0 { - return false - } - return true - } - /** Returns true if a String contains upper case letters. */ @@ -625,7 +613,19 @@ extension String { editDic.updateValue(String(editDistance), forKey: elm) } return editDic - } + } + + /** + Compares two strings with each other. Return true if the edit distance is in the allowed range. + Not allowed distance: 0 < distance < allowedEditDistance. + */ + func isAllowedDistance(str: String, allowedEditDistance: Int) -> Bool { + let distance = self.levenshtein(str) + if distance < allowedEditDistance && distance != 0 { + return false + } + return true + } /** Levenshtein Algorithm diff --git a/enzevalos_iphone/Typosquatting.swift b/enzevalos_iphone/Typosquatting.swift index 1dd5e0caa833c84ac94c2592e2c3b1814fc33893..7f6ee372abd5abbb4857c9f187eac3b62c8e5f67 100644 --- a/enzevalos_iphone/Typosquatting.swift +++ b/enzevalos_iphone/Typosquatting.swift @@ -7,3 +7,204 @@ // import Foundation +import TLDExtract + +class Typosquatting { + + /** + Compares a url's root domain with all root domain of all elements in a given url list. + false: if the URL's root domain has an unallowed edit distance with any other root domain. + */ + func compareURLs(url: String, urls: [String]) -> Bool { + let rootDomain = url.getRootDomain() + var isAllowedEditDistance = true + for url in urls { + let rd = url.getRootDomain() + if !rootDomain.isAllowedDistance(str: rd, allowedEditDistance: 4) { + // 2 domains contain less than 4 differences --> warning here needed + print("Warning!! \n Edit distance is not in the allowed range between link: " + url + " and link: " + url) + isAllowedEditDistance = false + } + } + return isAllowedEditDistance + } + + /** + Compares a url's root domain with all root domain of all elements in a given url list. + Returns an dictionary with the urls as the keys and and a bollean if it fits the allowed editdistance as value + isAllowedDistance is false: if the URL's root domain has an unallowed edit distance with any other root domain. + */ + func compareURLsDic(url: String, urls: [String]) -> [String:String] { + let rootDomain = url.getRootDomain() + var isAllowedEditDistance = true + var typoDic: [String:String] = [:] + for url in urls { + let rd = url.getRootDomain() + if !rootDomain.isAllowedDistance(str: rd, allowedEditDistance: 4) { + // 2 domains contain less than 4 differences --> warning here needed + print("Warning!! \n Edit distance is not in the allowed range between link: " + url + " and link: " + url) + isAllowedEditDistance = false + typoDic.updateValue(String(isAllowedEditDistance), forKey : url) + isAllowedEditDistance = true + } + else{ + typoDic.updateValue(String(isAllowedEditDistance), forKey : url) + } + } + return typoDic + } + + /** + Compares a url's root domain with all root domain of all elements in a givin url list. + Retruns an dictionary with the URLs as keys and the Editdistance as value + */ + func getURLEditdistance(url: String, urls: [String]) -> [String:String] { + let rootDomain = url.getRootDomain() + var urlEditDic: [String:String] = [:] + var editDistance = 0 + for url in urls { + let rd = url.getRootDomain() + editDistance = rootDomain.getEditDistance(str: rd) + urlEditDic.updateValue(String(editDistance), forKey : url) + } + return urlEditDic + } + + /** + Compares a URLs second-level domain with all elements of a givin domain list. + false: if the URL's second-level domain has an unallowed edit distance with any other domain. + */ + func compareURLWithSLDList(url: String, domains: [String]) -> Bool { + let secondLevelDomain = url.getSecondLevelDomain() + var isAllowedEditDistance = true + for domain in domains { + if !secondLevelDomain.isAllowedDistance(str: domain, allowedEditDistance: 4) { + // 2 domains contain less than 4 differences --> warning here needed + print("Warning!! \n Edit distance is not in the allowed range between domain: " + secondLevelDomain + " and domain: " + domain) + isAllowedEditDistance = false + } + } + return isAllowedEditDistance + } + + /** + Compares a url's second level domain with all domains of all elements in a givin url list. + Returns an dictionary with the domains as the keys and and a bollean if it fits the allowed editdistance as value + isAllowedDistance is false: if the URL's second level domain has an unallowed edit distance with any other domain. + */ + func compareURLWithSLDListDic(url: String, domains: [String]) -> [String:String] { + let secondLevelDomain = url.getSecondLevelDomain() + var isAllowedEditDistance = true + var typoDic: [String:String] = [:] + for domain in domains { + if !secondLevelDomain.isAllowedDistance(str: domain, allowedEditDistance: 4) { + // 2 domains contain less than 4 differences --> warning here needed + print("Warning!! \n Edit distance is not in the allowed range between link: " + url + " and link: " + domain) + isAllowedEditDistance = false + typoDic.updateValue(String(isAllowedEditDistance), forKey : domain) + isAllowedEditDistance = true + } + else{ + typoDic.updateValue(String(isAllowedEditDistance), forKey : domain) + } + } + return typoDic + } + + /** + Compares a url's second-level domain with all elements of a givin domain list. + Retruns an dictionary with the second level domain as keys and the editdistance as value + */ + func getSLDEditdistance(url: String, domains: [String]) -> [String:String] { + let secondLevelDomain = url.getSecondLevelDomain() + var sldEditDic: [String:String] = [:] + var editDistance = 0 + for domain in domains { + editDistance = secondLevelDomain.getEditDistance(str: domain) + sldEditDic.updateValue(String(editDistance), forKey : domain) + } + return sldEditDic + } + + /** + Compares a second-level domain with all elements of a givin second-level domain list. + false: if the second-level domain has an unallowed edit distance with any other domain. + */ + func compareDomainWithDomians(secondLvlDomain: String, domains: [String]) -> Bool { + var isAllowedEditDistance = true + for domain in domains { + if !secondLvlDomain.isAllowedDistance(str: domain, allowedEditDistance: 4) { + // 2 domains contain less than 4 differences --> warning here needed + print("Warning!! \n Edit distance is not in the allowed range between domain: " + secondLvlDomain + " and domain: " + domain) + isAllowedEditDistance = false + } + } + return isAllowedEditDistance + } + + /** + Compares a second level domain with all elements of a givin second-level domain list. + Returns an dictionary with the domains as the keys and and a bollean if it fits the allowed editdistance as value + isAllowedDistance is false: if the second level domain has an unallowed edit distance with any other domain. + */ + func compareDomainWithDomiansDic(secondLvlDomain: String, domains: [String]) -> [String:String] { + var isAllowedEditDistance = true + var typoDic: [String:String] = [:] + for domain in domains { + if !secondLvlDomain.isAllowedDistance(str: domain, allowedEditDistance: 4) { + // 2 domains contain less than 4 differences --> warning here needed + print("Warning!! \n Edit distance is not in the allowed range between link: " + secondLvlDomain + " and link: " + domain) + isAllowedEditDistance = false + typoDic.updateValue(String(isAllowedEditDistance), forKey : domain) + isAllowedEditDistance = true + } + else{ + typoDic.updateValue(String(isAllowedEditDistance), forKey : domain) + } + } + return typoDic + } + + /** + Compares a domain with all elements of a givin domain list. + Retruns an dictionary with domains as keys and the editdistance as value + */ + func getDomainEditDistance(domain: String, domains: [String]) -> [String:String] { + var domainEditDic: [String:String] = [:] + var editDistance = 0 + for dom in domains { + editDistance = domain.getEditDistance(str: dom) + domainEditDic.updateValue(String(editDistance), forKey : dom) + } + return domainEditDic + } + + /** + Receives a mail body and takes out the URLs and filters valid URLs by verifying their Root Domain + Return a list of valid URLs + */ + func isValidRD(mailBody: String) -> [String] { + let urls = mailBody.findURL() + var foundRDs: [String] = [] + for url in urls { + let urlString: String = url! + if !urlString.containsUpperCaseLetter() { + guard let result: TLDResult = extractor.parse(urlString) else { continue } + if let rd = result.rootDomain { + let rdPatternRegEx = "^([a-z0-9])*([a-z0-9-]+\\.)*[a-z0-9]*([a-z0-9-]+)*[a-z0-9]+\\.[a-z]{2,11}?$" // for hostname + let range = NSRange(location: 0, length: rd.utf16.count) + let regex = try! NSRegularExpression(pattern: rdPatternRegEx) + if regex.firstMatch(in: rd, options:[], range: range) != nil { + foundRDs.append(rd) + } else { + // domain did not pass the regex --> warning here needed + } + } + } else { + // domain contains capital letters --> warning here needed + } + } + // Returns non-Critical Root Domain list + return (foundRDs) + } +} diff --git a/enzevalos_iphoneTests/phishing/TyposquattingTests.swift b/enzevalos_iphoneTests/phishing/TyposquattingTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..52c391546b0f941d1d64df784c834ce7b445f5e3 --- /dev/null +++ b/enzevalos_iphoneTests/phishing/TyposquattingTests.swift @@ -0,0 +1,36 @@ +// +// TyposquattingTests.swift +// enzevalos_iphoneTests +// +// Created by Lauren Elden on 30.03.20. +// Copyright © 2020 fu-berlin. All rights reserved. +// + +import XCTest + +@testable import enzevalos_iphone + +class TyposquattingTests: XCTestCase { + + let typosquatting = Typosquatting() + + var vaildRD = ["fu-berlin.de", "google.de", "nljbjkjk.de", "w3schools.com", "w2schools.com", "w8schools.com", "w33schools.com", "w22schools.com", "w99schools.eu.com", "3schools.com"] + // Setup for Critical Pattern of a Root Domain + var testMailText = "https://git.imp.fu-berlin.de/enzevalos/enzevalos_iphone/issues/240http:// www.google.de http://nljbjkjk.de https://www.w3schools.com https://www.w2schools.com https://www.w8schools.com https://www.w33schools.com https://www.w22schools.com https://www.w99schools.eu.com https://git.imp.fu-ber_lin.de/enzevalos/enzevalos_iphone/issues/240 http://www.google-.de http://nljbj#kjk.de https://www.3schools.com https://www.w2sc..hools.com https://www.w8sch?ools.com https://www.w33sc_hools.com https://www.w22schooIs.com https://www.W99schools$.com" + + func testIsValidRD(){ + let pattern = typosquatting.isValidRD(mailBody: testMailText) + XCTAssertNotNil(pattern) + XCTAssertEqual(pattern[0], vaildRD[0]) + XCTAssertEqual(pattern[1], vaildRD[1]) + XCTAssertEqual(pattern[2], vaildRD[2]) + XCTAssertEqual(pattern[3], vaildRD[3]) + XCTAssertEqual(pattern[4], vaildRD[4]) + XCTAssertEqual(pattern[5], vaildRD[5]) + XCTAssertEqual(pattern[6], vaildRD[6]) + XCTAssertEqual(pattern[7], vaildRD[7]) + XCTAssertEqual(pattern[8], vaildRD[8]) + XCTAssertEqual(pattern[9], vaildRD[9]) + } + +}