Skip to content

Commit

Permalink
improved error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
tgymnich committed Aug 25, 2020
1 parent 82bedd0 commit a7e9643
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 38 deletions.
32 changes: 22 additions & 10 deletions Sources/OTPKit/Account.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,24 @@ public struct Account<OTPType: OTP>: Codable, Hashable, Identifiable {
return components.url!
}

public enum AccountError: LocalizedError {
public enum Error: LocalizedError {
case accountAlreadyExists

public var errorDescription: String? {
switch self {
case .accountAlreadyExists:
return "The account already exists"
}
}

public var failureReason: String? {
switch self {
case .accountAlreadyExists:
return "Accounts cannot share the same label and issuer"
}
}
}


/// - Parameter label: The label is used to identify which account a key is associated with. It contains an account name, which is a URI-encoded string, optionally prefixed by an issuer string identifying the provider or service managing that account. This issuer prefix can be used to prevent collisions between different accounts with different providers that might be identified using the same account name, e.g. the user's email address.
/// - Parameter otp: OTP instance used by this account.
/// - Parameter issuer: The issuer parameter is a string value indicating the provider or service this account is associated with, URL-encoded according to RFC 3986. If the issuer parameter is absent, issuer information may be taken from the issuer prefix of the label. If both issuer parameter and issuer label prefix are present, they should be equal.
Expand All @@ -60,13 +73,13 @@ public struct Account<OTPType: OTP>: Codable, Hashable, Identifiable {

/// Used to initalize a account from a URL.
/// - Parameter url: A url encoded like this: otpauth://TYPE/ISSUER:LABEL?PARAMETERS
public init?(from url: URL) {
public init(from url: URL) throws {
// otpauth://TYPE/LABEL?PARAMETERS
guard url.scheme == "otpauth" else { return nil }
guard url.scheme == "otpauth" else { throw URLDecodingError.invalidURLScheme(url.scheme) }

let components = url.pathComponents.dropFirst().first?.split(separator: ":")

guard let labelComponent = components?.last else { return nil }
guard let labelComponent = components?.last else { throw URLDecodingError.invalidURLLabel(nil) }
let label = String(labelComponent)

var issuer: String?
Expand All @@ -79,7 +92,7 @@ public struct Account<OTPType: OTP>: Codable, Hashable, Identifiable {
imageURL = URL(string: imageURLString)
}

guard let otp = OTPType(from: url) else { return nil }
let otp = try OTPType(from: url)

self.init(label: label, otp: otp, issuer: issuer, imageURL: imageURL)
}
Expand All @@ -89,7 +102,7 @@ public struct Account<OTPType: OTP>: Codable, Hashable, Identifiable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let url = try container.decode(URL.self)
self.init(from: url)!
try self.init(from: url)
}

public func encode(to encoder: Encoder) throws {
Expand All @@ -102,8 +115,7 @@ public struct Account<OTPType: OTP>: Codable, Hashable, Identifiable {
/// Saves the account to a keychain
/// - Parameter keychain
public func save(to keychain: Keychain) throws {

guard (try? keychain.get(keychainKey)) == nil else { throw AccountError.accountAlreadyExists }
guard try keychain.get(keychainKey) == nil else { throw Error.accountAlreadyExists }

try keychain
.label(label)
Expand All @@ -122,7 +134,7 @@ public struct Account<OTPType: OTP>: Codable, Hashable, Identifiable {
let items = keychain.allKeys()
let accounts = try items.compactMap { key throws -> Account? in
guard let urlString = try keychain.get(key), let url = URL(string: urlString) else { return nil }
return Account(from: url)
return try Account(from: url)
}
return accounts
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/OTPKit/Algorithm.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import Foundation
import CommonCrypto

public enum Algorithm: RawRepresentable, Hashable, Codable {
public enum Algorithm: RawRepresentable, CaseIterable, Hashable, Codable {
case md5
case sha1
case sha224
Expand Down
14 changes: 7 additions & 7 deletions Sources/OTPKit/HOTP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import Foundation
import Base32


public final class HOTP: OTP {
public static let typeString = "hotp"

Expand All @@ -34,17 +33,18 @@ public final class HOTP: OTP {
self.digits = digits ?? 6
}

required public convenience init?(from url: URL) {
guard url.scheme == "otpauth", url.host == "hotp" else { return nil }

guard let query = url.queryParameters else { return nil }
required public convenience init(from url: URL) throws {
guard url.scheme == "otpauth" else { throw URLDecodingError.invalidURLScheme(url.scheme) }
guard url.host == "hotp" else { throw URLDecodingError.invalidOTPType(url.host) }
guard let query = url.queryParameters else { throw URLDecodingError.invalidURLQueryParamters }

var algorithm: Algorithm?
if let algorithmString = query["algorithm"] {
algorithm = Algorithm(from: algorithmString)
guard let algo = Algorithm(from: algorithmString) else { throw URLDecodingError.invalidAlgorithm(algorithmString) }
algorithm = algo
}

guard let secret = query["secret"]?.base32DecodedData, secret.count != 0 else { return nil }
guard let secret = query["secret"]?.base32DecodedData, secret.count != 0 else { throw URLDecodingError.invalidSecret }

var digits: Int?
if let digitsString = query["digits"], let value = Int(digitsString), value >= 6 {
Expand Down
2 changes: 1 addition & 1 deletion Sources/OTPKit/OTP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public protocol OTP: Codable, Hashable {

/// Initalizes the OTP instance from a URL
/// - Parameter url
init?(from url: URL)
init(from url: URL) throws
}

extension OTP {
Expand Down
13 changes: 8 additions & 5 deletions Sources/OTPKit/TOTP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import Foundation


public final class TOTP: OTP {
public static let typeString = "totp"

Expand Down Expand Up @@ -52,17 +53,19 @@ public final class TOTP: OTP {
}
}

public required convenience init?(from url: URL) {
guard url.scheme == "otpauth", url.host == "totp" else { return nil }
public required convenience init(from url: URL) throws {
guard url.scheme == "otpauth" else { throw URLDecodingError.invalidURLScheme(url.scheme) }
guard url.host == "totp" else { throw URLDecodingError.invalidOTPType(url.host) }

guard let query = url.queryParameters else { return nil }
guard let query = url.queryParameters else { throw URLDecodingError.invalidURLQueryParamters }

var algorithm: Algorithm?
if let algorithmString = query["algorithm"] {
algorithm = Algorithm(from: algorithmString)
guard let algo = Algorithm(from: algorithmString) else { throw URLDecodingError.invalidAlgorithm(algorithmString) }
algorithm = algo
}

guard let secret = query["secret"]?.base32DecodedData, secret.count != 0 else { return nil }
guard let secret = query["secret"]?.base32DecodedData, secret.count != 0 else { throw URLDecodingError.invalidSecret }

var digits: Int?
if let digitsString = query["digits"], let value = Int(digitsString), value >= 6 {
Expand Down
80 changes: 80 additions & 0 deletions Sources/OTPKit/URLDecodingError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//
// URLDecodingError.swift
//
//
// Created by Tim Gymnich on 25.08.20.
//

import Foundation

public enum URLDecodingError: LocalizedError {
case invalidURLScheme(String?)
case invalidURLLabel(String?)
case invalidOTPType(String?)
case invalidURLQueryParamters
case invalidSecret
case invalidAlgorithm(String?)

public var errorDescription: String? {
switch self {
case .invalidURLScheme:
return "Invalid URL scheme"
case .invalidOTPType:
return "Invalid OTP type"
case .invalidURLQueryParamters:
return "Invalid URL query paramters"
case .invalidSecret:
return "Invalid secret"
case .invalidAlgorithm:
return "Invalid algorithm"
case .invalidURLLabel:
return "Invalid label"
}
}

public var failureReason: String? {
switch self {
case let .invalidURLScheme(scheme):
if let scheme = scheme {
return "The URL scheme \"\(scheme)\" is not supported"
} else {
return "The URL scheme is missing or not supported"
}
case let .invalidOTPType(type):
if let type = type {
return "The OTP type \"\(type)\" is not supported"
} else {
return "The OTP type is missing or not supported"
}
case .invalidURLQueryParamters:
return nil
case .invalidSecret:
return nil
case let .invalidAlgorithm(algorithm):
if let algorithm = algorithm {
return "The algorithm \"\(algorithm)\" is not supported"
} else {
return "The algorithm is missing or not supported"
}
case .invalidURLLabel:
return nil
}
}

public var recoverySuggestion: String? {
switch self {
case .invalidURLScheme:
return "Try using one of the supported URL schemes. Supported URL schemes are: otpauth"
case .invalidOTPType:
return "Try using one of the supported OTP types"
case .invalidURLQueryParamters:
return nil
case .invalidSecret:
return nil
case .invalidAlgorithm:
return "Try using one of the supported algorithms. Supported algorithms are: \(Algorithm.allCases.map { $0.string }.joined(separator: ","))"
case .invalidURLLabel:
return nil
}
}
}
8 changes: 4 additions & 4 deletions Tests/OTPKitTests/AccountTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ final class AccountTests: XCTestCase {
func testBasicTOTPAccount() {
let url = URL(string: "otpauth://totp/foo?secret=wew3k6ztd7kuh5ucg4pejqi4swwrrneh72ad2sdovikfatzbc5huto2j&algorithm=SHA256&digits=6&period=30")!

let account = Account<TOTP>(from: url)
let account = try? Account<TOTP>(from: url)

XCTAssertNotNil(account)
XCTAssertEqual(account?.label, "foo")
Expand All @@ -23,7 +23,7 @@ final class AccountTests: XCTestCase {
func testAdvancedTOTPAccount() {
let url = URL(string: "otpauth://totp/www.example.com:foo?secret=avelj2f3hqxgbm5gi7rvrfskdvxnia72rt7kxwfa5l5yuisqfpjlezm5&algorithm=SHA256&digits=7&period=30&image=http%3A%2F%2Fwww.example.com%2Fimage")!

let account = Account<TOTP>(from: url)
let account = try? Account<TOTP>(from: url)

XCTAssertNotNil(account)
XCTAssertEqual(account?.label, "foo")
Expand All @@ -34,7 +34,7 @@ final class AccountTests: XCTestCase {
func testBasicHOTPAccount() {
let url = URL(string: "otpauth://hotp/foo?secret=qtezwnbabbgdb3kspqx3kjp5z6n7qtc5xcrkvk3p4scbyeuzwlfpbnhe&algorithm=SHA1&digits=6&counter=0")!

let account = Account<HOTP>(from: url)
let account = try? Account<HOTP>(from: url)

XCTAssertNotNil(account)
XCTAssertEqual(account?.label, "foo")
Expand All @@ -43,7 +43,7 @@ final class AccountTests: XCTestCase {
func testAdvancedHOTPAccount() {
let url = URL(string: "otpauth://hotp/www.example.com:foo?secret=vutgq34hz4fi4ljm2ycg6im6sd5pl6jmy4rihpvzaddliiqoi64gnquq&algorithm=SHA256&digits=7&counter=0&image=http%3A%2F%2Fwww.example.com%2Fimage")!

let account = Account<HOTP>(from: url)
let account = try? Account<HOTP>(from: url)

XCTAssertNotNil(account)
XCTAssertEqual(account?.label, "foo")
Expand Down
10 changes: 5 additions & 5 deletions Tests/OTPKitTests/HOTPTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ final class HOTPTests: XCTestCase {
func testInitFromURLBasic() {
let url = URL(string: "otpauth://hotp/foo?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&algorithm=SHA1&digits=6")!

let hotp = HOTP(from: url)
let hotp = try? HOTP(from: url)

XCTAssertNotNil(hotp)
XCTAssertEqual(hotp?.algorithm, Algorithm.sha1)
Expand All @@ -37,7 +37,7 @@ final class HOTPTests: XCTestCase {
func testInitFromURLAdvanced() {
let url = URL(string: "otpauth://hotp/www.example.com:foo?secret=rk7xql2piogveotejq2ulv7d2aicbpzlh33xeaqnkqjck4iyz2cm6xzg&algorithm=SHA256&digits=7&period=30&counter=34&image=http%3A%2F%2Fwww.example.com%2Fimage")!

let hotp = HOTP(from: url)
let hotp = try? HOTP(from: url)

XCTAssertNotNil(hotp)
XCTAssertEqual(hotp?.algorithm, Algorithm.sha256)
Expand All @@ -48,21 +48,21 @@ final class HOTPTests: XCTestCase {

func testBrokenURL() {
let url = URL(string: "otpauth://hotp/foo?secret=&algorithm=SHA1&digits=6")!
let hotp = HOTP(from: url)
let hotp = try? HOTP(from: url)

XCTAssertNil(hotp)
}

func testWrongURLType() {
let url = URL(string: "otpauth://totp/foo?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ")!
let hotp = HOTP(from: url)
let hotp = try? HOTP(from: url)

XCTAssertNil(hotp)
}

func testWrongURLScheme() {
let url = URL(string: "http://hotp/foo?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&algorithm=SHA1&digits=6")!
let hotp = HOTP(from: url)
let hotp = try? HOTP(from: url)

XCTAssertNil(hotp)
}
Expand Down
10 changes: 5 additions & 5 deletions Tests/OTPKitTests/TOTPTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ final class TOTPTests: XCTestCase {

func testInitFromURLBasic() {
let url = URL(string: "otpauth://totp/foo?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ")!
let totp = TOTP(from: url)
let totp = try? TOTP(from: url)

XCTAssertNotNil(totp)
XCTAssertEqual(totp?.algorithm, Algorithm.sha1)
Expand All @@ -88,7 +88,7 @@ final class TOTPTests: XCTestCase {
func testInitFromURLAdvanced() {
let url = URL(string: "otpauth://totp/www.example.com:foo?secret=ahkzlrgopti4qd2u5olxmj6dj6d3ag6zxddbutu6oaukrkuup2r7wklw&algorithm=SHA256&digits=7&period=15&image=http%3A%2F%2Fwww.example.com%2Fimage")!

let totp = TOTP(from: url)
let totp = try? TOTP(from: url)

XCTAssertNotNil(totp)
XCTAssertEqual(totp?.algorithm, Algorithm.sha256)
Expand All @@ -99,21 +99,21 @@ final class TOTPTests: XCTestCase {

func testBrokenURL() {
let url = URL(string: "otpauth://totp/foo?secret=")!
let totp = TOTP(from: url)
let totp = try? TOTP(from: url)

XCTAssertNil(totp)
}

func testWrongURLType() {
let url = URL(string: "otpauth://hotp/foo?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&algorithm=SHA1&digits=6")!
let totp = TOTP(from: url)
let totp = try? TOTP(from: url)

XCTAssertNil(totp)
}

func testWrongURLScheme() {
let url = URL(string: "http://totp/foo?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ")!
let totp = TOTP(from: url)
let totp = try? TOTP(from: url)

XCTAssertNil(totp)
}
Expand Down

0 comments on commit a7e9643

Please sign in to comment.