Skip to content

Commit

Permalink
feat: Enhanced token validations (#71)
Browse files Browse the repository at this point in the history
* bump packages

* Updated show error

* optimised code

* Fixed OtpAuthUrlParser and tests

* Added TokenValidator tests

* Fixed lint
  • Loading branch information
joeldavidw authored Oct 6, 2024
1 parent 1a38989 commit b184737
Show file tree
Hide file tree
Showing 19 changed files with 542 additions and 314 deletions.
30 changes: 23 additions & 7 deletions Chronos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
objects = {

/* Begin PBXBuildFile section */
6B09CEBF2CB2DEA10054AB61 /* TokenValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B09CEBE2CB2DEA00054AB61 /* TokenValidator.swift */; };
6B09CEC12CB2DF060054AB61 /* OtpAuthUrlParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B09CEC02CB2DF060054AB61 /* OtpAuthUrlParser.swift */; };
6B09CEC32CB2EE9C0054AB61 /* TokenValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B09CEC22CB2EE980054AB61 /* TokenValidator.swift */; };
6B12B0A02C19DB7800E9ED2D /* ExportService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B12B09F2C19DB7800E9ED2D /* ExportService.swift */; };
6B12B0A22C19F1E600E9ED2D /* ExportVault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B12B0A12C19F1E600E9ED2D /* ExportVault.swift */; };
6B193C8A2C27F03300E759B7 /* CloudKitSyncMonitor in Frameworks */ = {isa = PBXBuildFile; productRef = 6B193C892C27F03300E759B7 /* CloudKitSyncMonitor */; };
Expand Down Expand Up @@ -73,7 +76,6 @@
6BC5F0522C52429100BA106F /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = 6BC5F0512C52429100BA106F /* SwiftProtobuf */; };
6BC5F0552C52464600BA106F /* GoogleAuth.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC5F0542C5242B500BA106F /* GoogleAuth.pb.swift */; };
6BC5F0572C529A2A00BA106F /* GoogleAuthenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC5F0562C529A2A00BA106F /* GoogleAuthenticator.swift */; };
6BD6D2012C11FEB4004512BF /* OTPService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BD6D2002C11FEB4004512BF /* OTPService.swift */; };
6BD90AA52B8E34BB00FABD91 /* PasswordLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BD90AA42B8E34BB00FABD91 /* PasswordLoginView.swift */; };
6BE122922BD6413D008636D2 /* ChronosCrypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BE122912BD6413D008636D2 /* ChronosCrypto.swift */; };
6BF53E4F2C317AA400356461 /* ZipArchive in Frameworks */ = {isa = PBXBuildFile; productRef = 6BF53E4E2C317AA400356461 /* ZipArchive */; };
Expand All @@ -94,6 +96,9 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
6B09CEBE2CB2DEA00054AB61 /* TokenValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenValidator.swift; sourceTree = "<group>"; };
6B09CEC02CB2DF060054AB61 /* OtpAuthUrlParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtpAuthUrlParser.swift; sourceTree = "<group>"; };
6B09CEC22CB2EE980054AB61 /* TokenValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenValidator.swift; sourceTree = "<group>"; };
6B12B09F2C19DB7800E9ED2D /* ExportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportService.swift; sourceTree = "<group>"; };
6B12B0A12C19F1E600E9ED2D /* ExportVault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportVault.swift; sourceTree = "<group>"; };
6B19CA712B7A70B800690390 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -152,7 +157,6 @@
6BC5F0492C4FDE6E00BA106F /* ParseOtpAuthUrl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseOtpAuthUrl.swift; sourceTree = "<group>"; };
6BC5F0542C5242B500BA106F /* GoogleAuth.pb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleAuth.pb.swift; sourceTree = "<group>"; };
6BC5F0562C529A2A00BA106F /* GoogleAuthenticator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleAuthenticator.swift; sourceTree = "<group>"; };
6BD6D2002C11FEB4004512BF /* OTPService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OTPService.swift; sourceTree = "<group>"; };
6BD90AA42B8E34BB00FABD91 /* PasswordLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordLoginView.swift; sourceTree = "<group>"; };
6BE122912BD6413D008636D2 /* ChronosCrypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChronosCrypto.swift; sourceTree = "<group>"; };
6BF53E512C317F1C00356461 /* ExportSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportSelectionView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -192,6 +196,15 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
6B09CEBD2CB2DE7A0054AB61 /* Helper */ = {
isa = PBXGroup;
children = (
6B09CEBE2CB2DEA00054AB61 /* TokenValidator.swift */,
6B09CEC02CB2DF060054AB61 /* OtpAuthUrlParser.swift */,
);
path = Helper;
sourceTree = "<group>";
};
6B19CA6E2B7A695200690390 /* Onboarding */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -241,6 +254,7 @@
6BD0562B2BDF7F190099616B /* Extensions */,
6B7383E12B9C3975008E8867 /* Services */,
6B8581BB2B99FB4400A19CE1 /* Data */,
6B09CEBD2CB2DE7A0054AB61 /* Helper */,
6B2583B02B975D3200938F3A /* Chronos.entitlements */,
6B7A546F2B94584E0057DCF9 /* Info.plist */,
6B3BB1062B503CB900DCEF0B /* Database */,
Expand Down Expand Up @@ -361,7 +375,6 @@
6BA5DA5B2B9E94F8009908E5 /* SecureEnclaveService.swift */,
6B39629D2BF63F27000410B0 /* SwiftDataService.swift */,
6B39629F2BF6423B000410B0 /* CryptoService.swift */,
6BD6D2002C11FEB4004512BF /* OTPService.swift */,
6B9D74642C14ADDC008E6582 /* StateService.swift */,
6B12B09F2C19DB7800E9ED2D /* ExportService.swift */,
6B3F92AA2C1C7987004125A8 /* VaultService.swift */,
Expand All @@ -380,6 +393,7 @@
6B8132F82C4C0F3B00DB367E /* Token */ = {
isa = PBXGroup;
children = (
6B09CEC22CB2EE980054AB61 /* TokenValidator.swift */,
6B8132F92C4C0F6300DB367E /* TokenToOtpAuthUrl.swift */,
6B8132FC2C4E5F6B00DB367E /* QrCodeGenerationAndParsing.swift */,
6BC5F0492C4FDE6E00BA106F /* ParseOtpAuthUrl.swift */,
Expand Down Expand Up @@ -660,13 +674,15 @@
6B7A54742B94B7A30057DCF9 /* PrivacyView.swift in Sources */,
6BD90AA52B8E34BB00FABD91 /* PasswordLoginView.swift in Sources */,
6B9D74682C1553A1008E6582 /* SecureBytes.swift in Sources */,
6B09CEC12CB2DF060054AB61 /* OtpAuthUrlParser.swift in Sources */,
6B3BB0E22B4ED19300DCEF0B /* TokensTab.swift in Sources */,
6BB37D832C480B07008DA122 /* ImportConfirmationView.swift in Sources */,
6BF53E542C31856A00356461 /* EncryptedExportPasswordView.swift in Sources */,
6B12B0A02C19DB7800E9ED2D /* ExportService.swift in Sources */,
6B8132F42C4BAD5A00DB367E /* TokenQRView.swift in Sources */,
6B3BB0E02B4ECE6F00DCEF0B /* TOTPRowView.swift in Sources */,
6B65F80B2C21C2EA00AC8606 /* VaultSetupView.swift in Sources */,
6B09CEBF2CB2DEA10054AB61 /* TokenValidator.swift in Sources */,
6B5E41CE2BD790F80045DBC6 /* EncryptedToken.swift in Sources */,
6B4B48F32BD7BB3C007D357D /* Token.swift in Sources */,
6BB37D7C2C457059008DA122 /* ImportSourceDetailView.swift in Sources */,
Expand All @@ -675,7 +691,6 @@
6BA5DA5C2B9E94F8009908E5 /* SecureEnclaveService.swift in Sources */,
6B66D5EF2B52BAC2006DB79D /* HOTPRowView.swift in Sources */,
6B39629E2BF63F27000410B0 /* SwiftDataService.swift in Sources */,
6BD6D2012C11FEB4004512BF /* OTPService.swift in Sources */,
6B39629A2BF5E935000410B0 /* MainAppView.swift in Sources */,
6BB37D852C483066008DA122 /* ImportFailureView.swift in Sources */,
6BF53E522C317F1C00356461 /* ExportSelectionView.swift in Sources */,
Expand All @@ -697,6 +712,7 @@
6B8132FD2C4E5F6B00DB367E /* QrCodeGenerationAndParsing.swift in Sources */,
6BBF32032C562F20003CBA66 /* Aegis.swift in Sources */,
6B4CBF2F2C490FB700983D44 /* Chronos.swift in Sources */,
6B09CEC32CB2EE9C0054AB61 /* TokenValidator.swift in Sources */,
6B2F7A842C636CE500DB1450 /* Ente.swift in Sources */,
6BC5F0572C529A2A00BA106F /* GoogleAuthenticator.swift in Sources */,
6B4987282C569A6E00A7D97A /* LastPass.swift in Sources */,
Expand Down Expand Up @@ -871,7 +887,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 14.0;
MARKETING_VERSION = 15.0;
PRODUCT_BUNDLE_IDENTIFIER = com.joeldavidw.ChronosDevDebug;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down Expand Up @@ -918,7 +934,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 14.0;
MARKETING_VERSION = 15.0;
PRODUCT_BUNDLE_IDENTIFIER = com.joeldavidw.ChronosDevRelease;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down Expand Up @@ -1244,7 +1260,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 14.0;
MARKETING_VERSION = 15.0;
PRODUCT_BUNDLE_IDENTIFIER = com.joeldavidw.Chronos;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down
4 changes: 1 addition & 3 deletions Chronos/App/Tabs/Tokens/AddToken/AddManualTokenView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,6 @@ extension AddManualTokenView {
tempToken.counter = counter
tempToken.period = period

let valid = validateToken(token: tempToken)

return valid.isValid
return tempToken.isValid
}
}
8 changes: 4 additions & 4 deletions Chronos/App/Tabs/Tokens/AddToken/AddTokenView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ struct AddTokenView: View {
@State private var showTokenManualAddSheet = false

let cryptoService = Container.shared.cryptoService()
let otpService = Container.shared.otpService()
let vaultService = Container.shared.vaultService()

var body: some View {
Expand Down Expand Up @@ -80,6 +79,8 @@ struct AddTokenView: View {
func handleScan(result: Result<ScanResult, ScanError>) {
switch result {
case let .success(result):
dismiss()

let otpAuthStr = result.string
guard otpAuthStr.starts(with: "otpauth://") else {
AlertKitAPI.present(
Expand All @@ -92,12 +93,10 @@ struct AddTokenView: View {
}

do {
let newToken = try otpService.parseOtpAuthUrl(otpAuthStr: otpAuthStr)
let newToken = try OtpAuthUrlParser.parseOtpAuthUrl(otpAuthStr: otpAuthStr)
let newEncToken = cryptoService.encryptToken(token: newToken)
vaultService.insertEncryptedToken(newEncToken)

dismiss()

AlertKitAPI.present(
title: "Successfully added \(!newToken.issuer.isEmpty ? newToken.issuer : newToken.account)",
icon: .done,
Expand All @@ -107,6 +106,7 @@ struct AddTokenView: View {
} catch {
AlertKitAPI.present(
title: "Invalid 2FA QR Code",
subtitle: error.localizedDescription.description,
icon: .error,
style: .iOS17AppleMusic,
haptic: .error
Expand Down
9 changes: 4 additions & 5 deletions Chronos/App/Tabs/Tokens/Row/HOTPRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,20 @@ struct HOTPRowView: View {
@State private var disableIncrementBtn = false

let cryptoService = Container.shared.cryptoService()
let otpService = Container.shared.otpService()

var token: Token
var encryptedToken: EncryptedToken

var body: some View {
Text(!otp.isEmpty ? formatOtp(otp: otp) : otpService.generateHOTP(token: token))
Text(!otp.isEmpty ? formatOtp(otp: otp) : token.generateOtp())
.font(.largeTitle)
.fontWeight(.light)
.lineLimit(1)
.onAppear {
otp = otpService.generateHOTP(token: token)
otp = token.generateOtp()
}
.onChange(of: token.counter) { _, _ in
otp = otpService.generateHOTP(token: token)
otp = token.generateOtp()
}
Spacer()
Button {
Expand All @@ -31,7 +30,7 @@ struct HOTPRowView: View {
token.counter += 1
cryptoService.updateEncryptedToken(encryptedToken: encryptedToken, token: token)

otp = otpService.generateHOTP(token: token)
otp = token.generateOtp()
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
disableIncrementBtn = false
}
Expand Down
5 changes: 2 additions & 3 deletions Chronos/App/Tabs/Tokens/Row/TOTPRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ struct TOTPRowView: View {
@State private var progress: Double = 1.0

let timer: Publishers.Autoconnect<Timer.TimerPublisher>
let otpService = Container.shared.otpService()

var body: some View {
Group {
Text(!otp.isEmpty ? formatOtp(otp: otp) : otpService.generateTOTP(token: token))
Text(!otp.isEmpty ? formatOtp(otp: otp) : token.generateOtp())
.font(.largeTitle)
.fontWeight(.light)
.lineLimit(1)
Expand Down Expand Up @@ -51,7 +50,7 @@ struct TOTPRowView: View {
}

private func updateOtp() {
otp = otpService.generateTOTP(token: token)
otp = token.generateOtp()
}

private func updateProgress() {
Expand Down
4 changes: 1 addition & 3 deletions Chronos/App/Tabs/Tokens/Row/TokenQRView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ struct TokenQRView: View {

@State var token: Token

let otpService = Container.shared.otpService()

var body: some View {
NavigationView {
VStack {
Expand All @@ -29,7 +27,7 @@ struct TokenQRView: View {
}
.padding(.bottom, 8)

if let otpAuthUrl = otpService.tokenToOtpAuthUrl(token: token),
if let otpAuthUrl = token.otpAuthUrl(),
let imageData = try? QRCode.build
.text(otpAuthUrl)
.generate
Expand Down
86 changes: 50 additions & 36 deletions Chronos/App/Tabs/Tokens/Row/TokenRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ struct TokenRowView: View {
return tokenPair.encToken
}

private let otpService = Container.shared.otpService()
private let cryptoService = Container.shared.cryptoService()

var body: some View {
Expand Down Expand Up @@ -66,11 +65,19 @@ struct TokenRowView: View {
}
} else {
HStack {
switch token.type {
case TokenTypeEnum.TOTP:
TOTPRowView(token: token, timer: timer)
case TokenTypeEnum.HOTP:
HOTPRowView(token: token, encryptedToken: encryptedToken)
if token.isValid {
switch token.type {
case TokenTypeEnum.TOTP:
TOTPRowView(token: token, timer: timer)
case TokenTypeEnum.HOTP:
HOTPRowView(token: token, encryptedToken: encryptedToken)
}
} else {
Text("Invalid Token")
.font(.title)
.fontWeight(.light)
.opacity(0.5)
.lineLimit(1)
}
}
}
Expand All @@ -80,19 +87,24 @@ struct TokenRowView: View {
.listRowBackground(Color(red: 0.04, green: 0, blue: 0.11))
.onTapGesture {
if !stateTapToRevealEnabled {
switch token.type {
case TokenTypeEnum.TOTP:
UIPasteboard.general.string = otpService.generateTOTP(token: token)
case TokenTypeEnum.HOTP:
UIPasteboard.general.string = otpService.generateHOTP(token: token)
if token.isValid {
UIPasteboard.general.string = token.generateOtp()

AlertKitAPI.present(
title: "Copied",
icon: .done,
style: .iOS17AppleMusic,
haptic: .success
)
} else {
AlertKitAPI.present(
title: "Invalid Token",
subtitle: token.validationError?.localizedDescription.description,
icon: .error,
style: .iOS17AppleMusic,
haptic: .success
)
}

AlertKitAPI.present(
title: "Copied",
icon: .done,
style: .iOS17AppleMusic,
haptic: .success
)
} else {
tokenRevealed.toggle()
}
Expand Down Expand Up @@ -160,27 +172,29 @@ struct TokenRowView: View {
}
.tint(.blue)

Button {
token.pinned = !(token.pinned ?? false)
cryptoService.updateEncryptedToken(encryptedToken: encryptedToken, token: token)
triggerSortAndFilterTokenPairs()
} label: {
VStack(alignment: .center) {
Image(systemName: token.pinned ?? false ? "pin.slash" : "pin")
Text(token.pinned ?? false ? "Unpin" : "Pin")
if token.isValid {
Button {
token.pinned = !(token.pinned ?? false)
cryptoService.updateEncryptedToken(encryptedToken: encryptedToken, token: token)
triggerSortAndFilterTokenPairs()
} label: {
VStack(alignment: .center) {
Image(systemName: token.pinned ?? false ? "pin.slash" : "pin")
Text(token.pinned ?? false ? "Unpin" : "Pin")
}
}
}
.tint(.indigo)

Button {
self.showTokenQRSheet = true
} label: {
VStack(alignment: .center) {
Image(systemName: "qrcode")
Text("QR")
.tint(.indigo)

Button {
self.showTokenQRSheet = true
} label: {
VStack(alignment: .center) {
Image(systemName: "qrcode")
Text("QR")
}
}
.tint(.gray)
}
.tint(.gray)
}
}

Expand Down
4 changes: 1 addition & 3 deletions Chronos/App/Tabs/Tokens/Row/UpdateTokenView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,7 @@ extension UpdateTokenView {
tempToken.counter = counter
tempToken.period = period

let valid = validateToken(token: tempToken)

return valid.isValid
return tempToken.isValid
}

var hasChanged: Bool {
Expand Down
Loading

0 comments on commit b184737

Please sign in to comment.