diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 680d9a9fb..0a72cceb3 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -3,6 +3,7 @@ - Extended support for iOS Shortcuts - Fixed issue where notes selection was lost when app backgrounded - Fixed an issue can activate the faceid switch without a passcode +- Added passkey authentication support 4.51 ----- diff --git a/Simplenote.xcodeproj/project.pbxproj b/Simplenote.xcodeproj/project.pbxproj index c4d3df429..5fa678f4e 100644 --- a/Simplenote.xcodeproj/project.pbxproj +++ b/Simplenote.xcodeproj/project.pbxproj @@ -420,6 +420,8 @@ BA0890A526BB9B680035CA48 /* NoteListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA0890A426BB9B680035CA48 /* NoteListRow.swift */; }; BA0890A726BB9BE20035CA48 /* ListWidgetHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA0890A626BB9BE20035CA48 /* ListWidgetHeaderView.swift */; }; BA0890A926BB9BF80035CA48 /* NewNoteButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA0890A826BB9BF80035CA48 /* NewNoteButton.swift */; }; + BA0AC5442C1A0C65002964DB /* PasskeyRegistrationResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA0AC5432C1A0C65002964DB /* PasskeyRegistrationResponse.swift */; }; + BA0AC5462C1A0C71002964DB /* PasskeyRegistrationChallenge.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA0AC5452C1A0C71002964DB /* PasskeyRegistrationChallenge.swift */; }; BA0AF10D2BE996600050EEBD /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B59560DF251A46D500A06788 /* KeychainManager.swift */; }; BA0AF10E2BE996630050EEBD /* KeychainPasswordItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FD30471FC4CFA2008D0B78 /* KeychainPasswordItem.swift */; }; BA0ED16E26D708AC002533B6 /* Color+Widgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA0ED16D26D708AC002533B6 /* Color+Widgets.swift */; }; @@ -437,6 +439,7 @@ BA12B06F26B0D0150026F31D /* SPManagedObject+Widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA12B06C26B0D0150026F31D /* SPManagedObject+Widget.swift */; }; BA12B07026B0D0150026F31D /* SPManagedObject+Widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA12B06C26B0D0150026F31D /* SPManagedObject+Widget.swift */; }; BA18532826488DBC00D9A347 /* SignupRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA18532726488DBC00D9A347 /* SignupRemoteTests.swift */; }; + BA1B70142C1CEC6F008282D7 /* SPModalActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA1B70132C1CEC6F008282D7 /* SPModalActivityIndicator.swift */; }; BA2015BB2B57384F005E59AA /* AutomatticTracks in Frameworks */ = {isa = PBXBuildFile; productRef = BA2015BA2B57384F005E59AA /* AutomatticTracks */; }; BA289B5C2BE4371A000E6794 /* ListWidgetIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA289B5A2BE4371A000E6794 /* ListWidgetIntentHandler.swift */; }; BA289B5F2BE43728000E6794 /* NoteWidgetIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA289B5D2BE43728000E6794 /* NoteWidgetIntentHandler.swift */; }; @@ -448,6 +451,10 @@ BA289B762BE45BBB000E6794 /* OpenNoteIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA289B742BE45BBB000E6794 /* OpenNoteIntentHandler.swift */; }; BA289B782BE45BFB000E6794 /* ActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B550F93022BA65CD00091939 /* ActivityType.swift */; }; BA2D82C6261522F100A1695B /* PublishNoticePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA2D82C5261522F100A1695B /* PublishNoticePresenter.swift */; }; + BA2E30E02C1B8B45002C7B10 /* PasskeyAuthChallenge.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA2E30DF2C1B8B45002C7B10 /* PasskeyAuthChallenge.swift */; }; + BA2E30E22C1B8B4E002C7B10 /* PasskeyAuthResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA2E30E12C1B8B4E002C7B10 /* PasskeyAuthResponse.swift */; }; + BA2E30E42C1B8F13002C7B10 /* PasskeyVerifyResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA2E30E32C1B8F13002C7B10 /* PasskeyVerifyResponse.swift */; }; + BA2E30E62C1B8FD3002C7B10 /* Data+Simplenote.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA2E30E52C1B8FD3002C7B10 /* Data+Simplenote.swift */; }; BA32A90F26B7469F00727247 /* WidgetError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA32A90E26B7469F00727247 /* WidgetError.swift */; }; BA32A91926B746A200727247 /* WidgetError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA32A90E26B7469F00727247 /* WidgetError.swift */; }; BA32A91A26B746A300727247 /* WidgetError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA32A90E26B7469F00727247 /* WidgetError.swift */; }; @@ -509,13 +516,13 @@ BA8FC2A5267AC7470082962E /* SharedStorageMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8FC2A4267AC7470082962E /* SharedStorageMigrator.swift */; }; BA9B19F926A8EF3200692366 /* SpinnerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA9B19F826A8EF3200692366 /* SpinnerViewController.swift */; }; BA9B59022685549F00DAD1ED /* StorageSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA9B59012685549F00DAD1ED /* StorageSettings.swift */; }; - BA9C7EFB2BF2CC3E007A8460 /* Downloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA9C7EFA2BF2CC3E007A8460 /* Downloader.swift */; }; - BA9C7EFC2BF2CCAE007A8460 /* Downloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA9C7EFA2BF2CC3E007A8460 /* Downloader.swift */; }; BA9C7EC92BED7AB1007A8460 /* CopyNoteContentIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA9C7EC82BED7AB1007A8460 /* CopyNoteContentIntentHandler.swift */; }; BA9C7ECB2BED7F7B007A8460 /* FindNoteWithTagIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA9C7ECA2BED7F7B007A8460 /* FindNoteWithTagIntentHandler.swift */; }; BA9C7ECD2BED813B007A8460 /* IntentTag+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA9C7ECC2BED813B007A8460 /* IntentTag+Helpers.swift */; }; BA9C7ED02BEE9BA7007A8460 /* ShortcutIntents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = BA9C7ECF2BEE9BA7007A8460 /* ShortcutIntents.intentdefinition */; }; BA9C7ED12BEE9BA7007A8460 /* ShortcutIntents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = BA9C7ECF2BEE9BA7007A8460 /* ShortcutIntents.intentdefinition */; }; + BA9C7EFB2BF2CC3E007A8460 /* Downloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA9C7EFA2BF2CC3E007A8460 /* Downloader.swift */; }; + BA9C7EFC2BF2CCAE007A8460 /* Downloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA9C7EFA2BF2CC3E007A8460 /* Downloader.swift */; }; BAA4856925D5E40900F3BDB9 /* SearchQuery+Simplenote.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAA4856825D5E40900F3BDB9 /* SearchQuery+Simplenote.swift */; }; BAA59E79269F9FE30068BD3D /* Date+Simplenote.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAA59E78269F9FE30068BD3D /* Date+Simplenote.swift */; }; BAA63C3325EEDA83001589D7 /* NoteLinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAA63C3225EEDA83001589D7 /* NoteLinkTests.swift */; }; @@ -554,6 +561,7 @@ BAF4A97926DB10BD00C51C1D /* NoteContentHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A64DE6F0255D1CD9001D0526 /* NoteContentHelper.swift */; }; BAF4A9AA26DB138600C51C1D /* NoteContentHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A64DE6F0255D1CD9001D0526 /* NoteContentHelper.swift */; }; BAF4A9BD26DB13B400C51C1D /* Note+Widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAF8D42326AE10F100CA9383 /* Note+Widget.swift */; }; + BAF694B22C1B753F000090E7 /* PasskeyAuthenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAF694B12C1B753F000090E7 /* PasskeyAuthenticator.swift */; }; BAF8D44B26AE10FC00CA9383 /* Tag+Widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAF8D42226AE10F100CA9383 /* Tag+Widget.swift */; }; BAF8D44C26AE10FC00CA9383 /* Note+Widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAF8D42326AE10F100CA9383 /* Note+Widget.swift */; }; BAF8D45526AE10FC00CA9383 /* Tag+Widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAF8D42226AE10F100CA9383 /* Tag+Widget.swift */; }; @@ -1141,6 +1149,8 @@ BA0890A426BB9B680035CA48 /* NoteListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteListRow.swift; sourceTree = ""; }; BA0890A626BB9BE20035CA48 /* ListWidgetHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListWidgetHeaderView.swift; sourceTree = ""; }; BA0890A826BB9BF80035CA48 /* NewNoteButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewNoteButton.swift; sourceTree = ""; }; + BA0AC5432C1A0C65002964DB /* PasskeyRegistrationResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasskeyRegistrationResponse.swift; sourceTree = ""; }; + BA0AC5452C1A0C71002964DB /* PasskeyRegistrationChallenge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasskeyRegistrationChallenge.swift; sourceTree = ""; }; BA0ED16D26D708AC002533B6 /* Color+Widgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Widgets.swift"; sourceTree = ""; }; BA0F5E0326B62A8B0098C605 /* RemoteError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteError.swift; sourceTree = ""; }; BA113E4C269E860500F3E3B4 /* markdown-light.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = "markdown-light.css"; sourceTree = ""; }; @@ -1149,12 +1159,17 @@ BA12B06C26B0D0150026F31D /* SPManagedObject+Widget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SPManagedObject+Widget.swift"; sourceTree = ""; }; BA16C6A82BC4968400C9079F /* Simplenote 7.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Simplenote 7.xcdatamodel"; sourceTree = ""; }; BA18532726488DBC00D9A347 /* SignupRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignupRemoteTests.swift; sourceTree = ""; }; + BA1B70132C1CEC6F008282D7 /* SPModalActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SPModalActivityIndicator.swift; sourceTree = ""; }; BA289B5A2BE4371A000E6794 /* ListWidgetIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListWidgetIntentHandler.swift; sourceTree = ""; }; BA289B5D2BE43728000E6794 /* NoteWidgetIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteWidgetIntentHandler.swift; sourceTree = ""; }; BA289B632BE43963000E6794 /* OpenNewNoteIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenNewNoteIntentHandler.swift; sourceTree = ""; }; BA289B702BE45A39000E6794 /* IntentsConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentsConstants.swift; sourceTree = ""; }; BA289B742BE45BBB000E6794 /* OpenNoteIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenNoteIntentHandler.swift; sourceTree = ""; }; BA2D82C5261522F100A1695B /* PublishNoticePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublishNoticePresenter.swift; sourceTree = ""; }; + BA2E30DF2C1B8B45002C7B10 /* PasskeyAuthChallenge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasskeyAuthChallenge.swift; sourceTree = ""; }; + BA2E30E12C1B8B4E002C7B10 /* PasskeyAuthResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasskeyAuthResponse.swift; sourceTree = ""; }; + BA2E30E32C1B8F13002C7B10 /* PasskeyVerifyResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasskeyVerifyResponse.swift; sourceTree = ""; }; + BA2E30E52C1B8FD3002C7B10 /* Data+Simplenote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Simplenote.swift"; sourceTree = ""; }; BA32A90E26B7469F00727247 /* WidgetError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetError.swift; sourceTree = ""; }; BA34B04F2BEAEF4800580E15 /* SimplenoteIntentsRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SimplenoteIntentsRelease.entitlements; sourceTree = ""; }; BA34B0562BEC216B00580E15 /* IntentNote+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IntentNote+Helpers.swift"; sourceTree = ""; }; @@ -1199,7 +1214,6 @@ BA8FC2A4267AC7470082962E /* SharedStorageMigrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedStorageMigrator.swift; sourceTree = ""; }; BA9B19F826A8EF3200692366 /* SpinnerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpinnerViewController.swift; sourceTree = ""; }; BA9B59012685549F00DAD1ED /* StorageSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageSettings.swift; sourceTree = ""; }; - BA9C7EFA2BF2CC3E007A8460 /* Downloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Downloader.swift; sourceTree = ""; }; BA9C7EC82BED7AB1007A8460 /* CopyNoteContentIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyNoteContentIntentHandler.swift; sourceTree = ""; }; BA9C7ECA2BED7F7B007A8460 /* FindNoteWithTagIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindNoteWithTagIntentHandler.swift; sourceTree = ""; }; BA9C7ECC2BED813B007A8460 /* IntentTag+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IntentTag+Helpers.swift"; sourceTree = ""; }; @@ -1224,6 +1238,7 @@ BA9C7EF52BEE9C3D007A8460 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/ShortcutIntents.strings; sourceTree = ""; }; BA9C7EF72BEE9C3E007A8460 /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/ShortcutIntents.strings"; sourceTree = ""; }; BA9C7EF92BEE9C3F007A8460 /* zh-Hans-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans-CN"; path = "zh-Hans-CN.lproj/ShortcutIntents.strings"; sourceTree = ""; }; + BA9C7EFA2BF2CC3E007A8460 /* Downloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Downloader.swift; sourceTree = ""; }; BAA4856825D5E40900F3BDB9 /* SearchQuery+Simplenote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchQuery+Simplenote.swift"; sourceTree = ""; }; BAA59E78269F9FE30068BD3D /* Date+Simplenote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Simplenote.swift"; sourceTree = ""; }; BAA63C3225EEDA83001589D7 /* NoteLinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteLinkTests.swift; sourceTree = ""; }; @@ -1248,6 +1263,7 @@ BAD0F1EC2BED49C200E73E45 /* FindNoteIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindNoteIntentHandler.swift; sourceTree = ""; }; BAE08625261282D1009D40CD /* Note+Publish.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Note+Publish.swift"; sourceTree = ""; }; BAF4A96E26DB085D00C51C1D /* NSURLComponents+Simplenote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSURLComponents+Simplenote.swift"; sourceTree = ""; }; + BAF694B12C1B753F000090E7 /* PasskeyAuthenticator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasskeyAuthenticator.swift; sourceTree = ""; }; BAF8D42226AE10F100CA9383 /* Tag+Widget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Tag+Widget.swift"; sourceTree = ""; }; BAF8D42326AE10F100CA9383 /* Note+Widget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Note+Widget.swift"; sourceTree = ""; }; BAF8D48126AE125B00CA9383 /* Simplenote-Widgets-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Simplenote-Widgets-Bridging-Header.h"; sourceTree = ""; }; @@ -2054,6 +2070,7 @@ BAA59E78269F9FE30068BD3D /* Date+Simplenote.swift */, BAF4A96E26DB085D00C51C1D /* NSURLComponents+Simplenote.swift */, BA6DA19026DB5F1B000464C8 /* URLComponents.swift */, + BA2E30E52C1B8FD3002C7B10 /* Data+Simplenote.swift */, ); name = Extensions; sourceTree = ""; @@ -2061,6 +2078,7 @@ B56A695122F9CCF400B90398 /* Onboarding */ = { isa = PBXGroup; children = ( + BA2E30DE2C1B8B0D002C7B10 /* Passkeys */, B56A695722F9CD1500B90398 /* SPOnboardingViewController.swift */, B56A695622F9CD1500B90398 /* SPOnboardingViewController.xib */, B56A695D22F9D53300B90398 /* SPAuthError.swift */, @@ -2384,6 +2402,19 @@ path = "Intent Handlers"; sourceTree = ""; }; + BA2E30DE2C1B8B0D002C7B10 /* Passkeys */ = { + isa = PBXGroup; + children = ( + BAF694B12C1B753F000090E7 /* PasskeyAuthenticator.swift */, + BA2E30DF2C1B8B45002C7B10 /* PasskeyAuthChallenge.swift */, + BA2E30E12C1B8B4E002C7B10 /* PasskeyAuthResponse.swift */, + BA0AC5432C1A0C65002964DB /* PasskeyRegistrationResponse.swift */, + BA0AC5452C1A0C71002964DB /* PasskeyRegistrationChallenge.swift */, + BA2E30E32C1B8F13002C7B10 /* PasskeyVerifyResponse.swift */, + ); + name = Passkeys; + sourceTree = ""; + }; BA34B0552BEC214800580E15 /* ResolutionResults */ = { isa = PBXGroup; children = ( @@ -2559,6 +2590,7 @@ B56A696422F9D54600B90398 /* SPLabel.swift */, E230E39C17D0F33B009B5EBB /* SPModalActivityIndicator.h */, E230E39D17D0F33B009B5EBB /* SPModalActivityIndicator.m */, + BA1B70132C1CEC6F008282D7 /* SPModalActivityIndicator.swift */, B5D982D122F8644000C37C29 /* SPSquaredButton.swift */, E215C79B180B228800AD36B5 /* SPTextField.h */, E215C79C180B228800AD36B5 /* SPTextField.m */, @@ -3453,6 +3485,7 @@ B56E763422BD394C00C5AA47 /* UIImage+Simplenote.swift in Sources */, B543C7E323CF76EA00003A80 /* NotesListFilter.swift in Sources */, 46A3C96717DFA81A002865AE /* NSManagedObjectContext+CoreDataExtensions.m in Sources */, + BA0AC5442C1A0C65002964DB /* PasskeyRegistrationResponse.swift in Sources */, A6C0DFA725C0992D00B9BE39 /* NoteScrollPositionCache.swift in Sources */, B59314D81A486B3800B651ED /* SPConstants.m in Sources */, 375D24B421E01131007AB25A /* escape.c in Sources */, @@ -3487,6 +3520,7 @@ B550F93622BA7E9100091939 /* NSUserActivity+Simplenote.swift in Sources */, B5BE054E1AB75902002417BF /* NSProcessInfo+Util.m in Sources */, BA4C6CFC264C744300B723A7 /* SeparatorsView.swift in Sources */, + BA2E30E42C1B8F13002C7B10 /* PasskeyVerifyResponse.swift in Sources */, 375D24B821E01131007AB25A /* hash.c in Sources */, BA5768EC269BE4D0008B510E /* AccountDeletionController.swift in Sources */, A6D5AE6525483F8A00326C76 /* NSTextStorage+Simplenote.swift in Sources */, @@ -3499,10 +3533,13 @@ B5CDE61F2150834C00C3FED4 /* Simperium+Simplenote.m in Sources */, A60A1A1E25655D840041701E /* ApplicationShortcutItemType.swift in Sources */, B51F6DD12460C3EE0074DDD9 /* AuthenticationValidator.swift in Sources */, + BAF694B22C1B753F000090E7 /* PasskeyAuthenticator.swift in Sources */, B575736A232C567000443C2E /* UIImage+Dynamic.swift in Sources */, A60DF31025A4524100FDADF3 /* PinLockBaseController.swift in Sources */, A6F487ED25A85F970050CFA8 /* TagTextFieldInputValidator.swift in Sources */, + BA2E30E62C1B8FD3002C7B10 /* Data+Simplenote.swift in Sources */, B5AB169822FA124F00B4EBA5 /* SPSheetController.swift in Sources */, + BA0AC5462C1A0C71002964DB /* PasskeyRegistrationChallenge.swift in Sources */, B511AEDA255A0A2A005B2159 /* InterlinkResultsController.swift in Sources */, B5476BC123D8E5D0000E7723 /* String+Simplenote.swift in Sources */, B5DF734222A565DA00602CE7 /* Options.swift in Sources */, @@ -3565,9 +3602,11 @@ 375D24BA21E01131007AB25A /* document.c in Sources */, BA6DA19126DB5F1B000464C8 /* URLComponents.swift in Sources */, 46A3C98317DFA81A002865AE /* main.m in Sources */, + BA1B70142C1CEC6F008282D7 /* SPModalActivityIndicator.swift in Sources */, B58C9F5723F2FCEC008C3480 /* SPPlaceholderView.swift in Sources */, BAB6C04726BA4CAF007495C4 /* WidgetController.swift in Sources */, A6E1E79E24BDE401008A44BC /* SPCardTransitioningManager.swift in Sources */, + BA2E30E02C1B8B45002C7B10 /* PasskeyAuthChallenge.swift in Sources */, 74388F4622CFFA8B001C5EC0 /* NSObject+Helpers.swift in Sources */, 46A3C98617DFA81A002865AE /* SPEntryListViewController.m in Sources */, A6ABB689256D95EB00E2A076 /* PinLockProgressView.swift in Sources */, @@ -3587,6 +3626,7 @@ B5185CFB23427F2F0060145A /* SearchDisplayController.swift in Sources */, B524AE0F2352BE9200EA11D4 /* UITableView+Simplenote.swift in Sources */, A6FDF73E2542BDE100415C87 /* NoteInformationController.swift in Sources */, + BA2E30E22C1B8B4E002C7B10 /* PasskeyAuthResponse.swift in Sources */, A604DB22255A993E00B802CA /* SearchMapView.swift in Sources */, B5E951E624FEE2A8004B10B8 /* Note+Links.swift in Sources */, A6BBDA46255034E6005C8343 /* SPEditorTextView+Simplenote.swift in Sources */, diff --git a/Simplenote/AccountRemote.swift b/Simplenote/AccountRemote.swift index d91a16121..a1fbb965d 100644 --- a/Simplenote/AccountRemote.swift +++ b/Simplenote/AccountRemote.swift @@ -51,4 +51,94 @@ class AccountRemote: Remote { return request } + + // MARK: - Passkeys + // + private func passkeyCredentialCreationRequest(withEmail email: String, password: String) -> URLRequest { + let params = [ + "email": email.lowercased(), + "password": password, + "webauthn": "true" + ] as [String: Any] + + let boundary = "Boundary-\(UUID().uuidString)" + var request = URLRequest(url: SimplenoteConstants.passkeyCredentialCreationURL) + request.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") + + request.httpMethod = RemoteConstants.Method.POST + request.httpBody = body(with: boundary, parameters: params) + + return request + } + + private func body(with boundary: String, parameters: [String: Any]) -> Data { + var body = Data() + + for param in parameters { + let paramName = param.key + body += Data("--\(boundary)\r\n".utf8) + body += Data("Content-Disposition:form-data; name=\"\(paramName)\"".utf8) + let paramValue = param.value as! String + body += Data("\r\n\r\n\(paramValue)\r\n".utf8) + } + + body += Data("--\(boundary)--\r\n".utf8) + + return body + } + + func requestChallengeResponseToCreatePasskey(forEmail email: String, password: String) async throws -> Data? { + let request = passkeyCredentialCreationRequest(withEmail: email, password: password) + + return try await performDataTask(with: request) + } + + private func passkeyCredentialRegistration(withData data: Data) -> URLRequest { + var urlRequest = URLRequest(url: SimplenoteConstants.passkeyRegistrationURL) + urlRequest.httpMethod = RemoteConstants.Method.POST + urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + + urlRequest.httpBody = data + + return urlRequest + } + + func registerCredential(with data: Data) async throws { + let request = passkeyCredentialRegistration(withData: data) + try await _ = performDataTask(with: request) + } + + private func passkeyAuthChallengeRequest(forEmail email: String) -> URLRequest { + var urlRequest = URLRequest(url: SimplenoteConstants.passkeyAuthChallengeURL) + urlRequest.httpMethod = RemoteConstants.Method.POST + urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + + let body = [ + "email": email.lowercased() + ] + let json = try? JSONEncoder().encode(body) + + urlRequest.httpBody = json + + return urlRequest + } + + func passkeyAuthChallenge(for email: String) async throws -> Data? { + let request = passkeyAuthChallengeRequest(forEmail: email) + return try await performDataTask(with: request) + } + + private func verifyPassKeyRequest(with data: Data) -> URLRequest { + var urlRequest = URLRequest(url: SimplenoteConstants.verifyPasskeyAuthChallengeURL) + urlRequest.httpMethod = RemoteConstants.Method.POST + urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + urlRequest.httpBody = data + + return urlRequest + } + + func verifyPasskeyLogin(with data: Data) async throws -> Data? { + let request = verifyPassKeyRequest(with: data) + return try await performDataTask(with: request) + } } diff --git a/Simplenote/Classes/SPAuthViewController.swift b/Simplenote/Classes/SPAuthViewController.swift index f6e27223e..46763f756 100644 --- a/Simplenote/Classes/SPAuthViewController.swift +++ b/Simplenote/Classes/SPAuthViewController.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import SafariServices +import AuthenticationServices // MARK: - SPAuthViewController // @@ -293,7 +294,7 @@ private extension SPAuthViewController { return } - performSimperiumAuthentication() + performSimperiumAuthentication(username: email, password: password) } @IBAction func performSignUp() { @@ -318,6 +319,24 @@ private extension SPAuthViewController { } } + @objc func passkeyAuthAction() { + guard ensureWarningsAreOnScreenWhenNeeded() else { + return + } + + Task { + lockdownInterface() + + let passkeyAuthenticator = SPAppDelegate.shared().passkeyAuthenticator + passkeyAuthenticator.delegate = self + do { + try await passkeyAuthenticator.attemptPasskeyAuth(for: email, in: self) + } catch { + failed(error) + } + } + } + @IBAction func presentPasswordReset() { controller.presentPasswordReset(from: self, username: email) } @@ -357,10 +376,10 @@ private extension SPAuthViewController { } } - func performSimperiumAuthentication() { + func performSimperiumAuthentication(username: String, password: String) { lockdownInterface() - controller.loginWithCredentials(username: email, password: password) { error in + controller.loginWithCredentials(username: username, password: password) { error in if let error = error { self.handleError(error: error) } else { @@ -616,6 +635,30 @@ extension SPAuthViewController: SPTextInputViewDelegate { } } +// MARK: - Passkeys +extension SPAuthViewController: ASAuthorizationControllerPresentationContextProviding { + func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { + view.window! + } +} + +extension SPAuthViewController: PasskeyDelegate { + func succeed() { + unlockInterface() + } + + func failed(_ error: any Error) { + unlockInterface() + presentPasskeyAuthError(error) + } + + private func presentPasskeyAuthError(_ error: any Error) { + let alert = UIAlertController(title: AuthenticationStrings.passkeyAuthFailureTitle, message: error.localizedDescription, preferredStyle: .alert) + alert.addCancelActionWithTitle(AuthenticationStrings.unverifiedCancelText) + present(alert, animated: true) + } +} + // MARK: - AuthenticationMode: Signup / Login // struct AuthenticationMode { @@ -627,6 +670,7 @@ struct AuthenticationMode { let secondaryActionText: String? let secondaryActionAttributedText: NSAttributedString? let isPasswordHidden: Bool + let isLogin: Bool } // MARK: - Default Operation Modes @@ -643,7 +687,8 @@ extension AuthenticationMode { secondaryActionSelector: #selector(SPAuthViewController.presentPasswordReset), secondaryActionText: AuthenticationStrings.loginSecondaryAction, secondaryActionAttributedText: nil, - isPasswordHidden: false) + isPasswordHidden: false, + isLogin: true) } /// Signup Operation Mode: Contains all of the strings + delegate wirings, so that the AuthUI handles user account creation scenarios. @@ -656,7 +701,20 @@ extension AuthenticationMode { secondaryActionSelector: #selector(SPAuthViewController.presentTermsOfService), secondaryActionText: nil, secondaryActionAttributedText: AuthenticationStrings.signupSecondaryAttributedAction, - isPasswordHidden: true) + isPasswordHidden: true, + isLogin: false) + } + + static var loginWithPasskeys: AuthenticationMode { + return .init(title: AuthenticationStrings.loginTitle, + validationStyle: .legacy, + primaryActionSelector: #selector(SPAuthViewController.passkeyAuthAction), + primaryActionText: AuthenticationStrings.passkeyActionButton, + secondaryActionSelector: #selector(SPAuthViewController.presentPasswordReset), + secondaryActionText: AuthenticationStrings.loginSecondaryAction, + secondaryActionAttributedText: nil, + isPasswordHidden: true, + isLogin: true) } } @@ -666,6 +724,7 @@ private enum AuthenticationStrings { static let loginTitle = NSLocalizedString("Log In", comment: "LogIn Interface Title") static let loginPrimaryAction = NSLocalizedString("Log In", comment: "LogIn Action") static let loginSecondaryAction = NSLocalizedString("Forgotten password?", comment: "Password Reset Action") + static let passkeyActionButton = NSLocalizedString("Log In With Passkeys", comment: "Login with Passkey action") static let signupTitle = NSLocalizedString("Sign Up", comment: "SignUp Interface Title") static let signupPrimaryAction = NSLocalizedString("Sign Up", comment: "SignUp Action") static let signupSecondaryActionPrefix = NSLocalizedString("By creating an account you agree to our", comment: "Terms of Service Legend *PREFIX*: printed in dark color") @@ -683,6 +742,7 @@ private enum AuthenticationStrings { static let unverifiedErrorMessage = NSLocalizedString("There was an preparing your verification email, please try again later", comment: "Request error alert message") static let verificationSentTitle = NSLocalizedString("Check your Email", comment: "Vefification sent alert title") static let verificationSentTemplate = NSLocalizedString("We’ve sent a verification email to %1$@. Please check your inbox and follow the instructions.", comment: "Confirmation that an email has been sent") + static let passkeyAuthFailureTitle = NSLocalizedString("Passkey Authentication Failed", comment: "Title for passkey authentication failure") } // MARK: - PasswordInsecure Alert Strings diff --git a/Simplenote/Classes/SPAuthViewController.xib b/Simplenote/Classes/SPAuthViewController.xib index ac1bcbb84..b4c8726f4 100644 --- a/Simplenote/Classes/SPAuthViewController.xib +++ b/Simplenote/Classes/SPAuthViewController.xib @@ -1,10 +1,11 @@ - + - + + @@ -28,7 +29,7 @@ - + @@ -43,7 +44,7 @@ @@ -59,13 +60,13 @@ - + - + + + + - + @@ -91,9 +110,10 @@ - + + @@ -108,7 +128,6 @@ - @@ -117,4 +136,20 @@ + + + + + + + + + + + + + + + + diff --git a/Simplenote/Classes/SimplenoteConstants.swift b/Simplenote/Classes/SimplenoteConstants.swift index f273e382e..6ff933477 100644 --- a/Simplenote/Classes/SimplenoteConstants.swift +++ b/Simplenote/Classes/SimplenoteConstants.swift @@ -44,4 +44,12 @@ class SimplenoteConstants: NSObject { static let signupURL = currentEngineBaseURL.appendingPathComponent("/account/request-signup") static let verificationURL = currentEngineBaseURL.appendingPathComponent("/account/verify-email/") static let accountDeletionURL = currentEngineBaseURL.appendingPathComponent("/account/request-delete/") + + /// Passkey: Endpoints + /// + static let currentPasskeyBaseURL = URL(string: "https://passkey-dev-dot-simple-note-hrd.appspot.com")! + static let passkeyCredentialCreationURL = currentPasskeyBaseURL.appendingPathComponent("/api2/login") + static let passkeyRegistrationURL = currentPasskeyBaseURL.appendingPathComponent("/auth/add-credential") + static let passkeyAuthChallengeURL = currentPasskeyBaseURL.appendingPathComponent("/auth/prepare-auth-challenge") + static let verifyPasskeyAuthChallengeURL = currentPasskeyBaseURL.appendingPathComponent("/auth/verify-login-credential") } diff --git a/Simplenote/Classes/String+Simplenote.swift b/Simplenote/Classes/String+Simplenote.swift index 750d31869..30f78606c 100644 --- a/Simplenote/Classes/String+Simplenote.swift +++ b/Simplenote/Classes/String+Simplenote.swift @@ -194,6 +194,14 @@ extension String { return base + host + "/" } + + func toBase64url() -> String { + let base64url = self + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + return base64url + } } // MARK: Replacing newlines with spaces diff --git a/Simplenote/Data+Simplenote.swift b/Simplenote/Data+Simplenote.swift new file mode 100644 index 000000000..e354a7cc8 --- /dev/null +++ b/Simplenote/Data+Simplenote.swift @@ -0,0 +1,21 @@ +import Foundation + +extension Data { + static func decodeUrlSafeBase64(_ value: String) throws -> Data { + var stringtoDecode: String = value.replacingOccurrences(of: "-", with: "+") + stringtoDecode = stringtoDecode.replacingOccurrences(of: "_", with: "/") + switch stringtoDecode.utf8.count % 4 { + case 2: + stringtoDecode += "==" + case 3: + stringtoDecode += "=" + default: + break + } + guard let data = Data(base64Encoded: stringtoDecode, options: [.ignoreUnknownCharacters]) else { + throw NSError(domain: "decodeUrlSafeBase64", code: 1, + userInfo: [NSLocalizedDescriptionKey: "Can't decode base64 string"]) + } + return data + } +} diff --git a/Simplenote/PasskeyAuthChallenge.swift b/Simplenote/PasskeyAuthChallenge.swift new file mode 100644 index 000000000..eac54bcad --- /dev/null +++ b/Simplenote/PasskeyAuthChallenge.swift @@ -0,0 +1,11 @@ +import Foundation + +struct PasskeyAuthChallenge: Decodable { + let relayingParty: String + let challenge: String + + enum CodingKeys: String, CodingKey { + case relayingParty = "rpId" + case challenge + } +} diff --git a/Simplenote/PasskeyAuthResponse.swift b/Simplenote/PasskeyAuthResponse.swift new file mode 100644 index 000000000..9e071601c --- /dev/null +++ b/Simplenote/PasskeyAuthResponse.swift @@ -0,0 +1,22 @@ +import Foundation +import AuthenticationServices + +struct PasskeyAuthResponse: Codable { + let id: String + let rawId: String + let response: Response + var type: String = "public-key" + + init(from credential: ASAuthorizationPlatformPublicKeyCredentialAssertion) { + self.id = credential.credentialID.base64EncodedString().toBase64url() + self.rawId = credential.credentialID.base64EncodedString().toBase64url() + self.response = PasskeyAuthResponse.Response(clientDataJSON: credential.rawClientDataJSON.base64EncodedString(), authenticatorData: credential.rawAuthenticatorData.base64EncodedString(), signature: credential.signature.base64EncodedString(), userHandle: credential.userID.base64EncodedString()) + } + + struct Response: Codable { + let clientDataJSON: String + let authenticatorData: String + let signature: String + let userHandle: String + } +} diff --git a/Simplenote/PasskeyAuthenticator.swift b/Simplenote/PasskeyAuthenticator.swift new file mode 100644 index 000000000..0b13105e5 --- /dev/null +++ b/Simplenote/PasskeyAuthenticator.swift @@ -0,0 +1,140 @@ +import Foundation +import AuthenticationServices + +enum PasskeyError: Error { + case couldNotRequestRegistrationChallenge + case couldNotEncodeRegistrationChallenge + case couldNotPrepareRegistrationData + case authFailed +} + +protocol PasskeyDelegate: AnyObject { + func succeed() + func failed(_ error: Error) +} + +@objcMembers +class PasskeyAuthenticator: NSObject { + typealias PresentationContext = ASAuthorizationControllerPresentationContextProviding + let authenticator: SPAuthenticator + let accountRemote: AccountRemote + + weak var delegate: PasskeyDelegate? = nil + + @objc + init(authenticator: SPAuthenticator) { + self.authenticator = authenticator + self.accountRemote = AccountRemote() + } + + // MARK: - Registration + // + func registerPasskey(for email: String, password: String, in presentationContext: PresentationContext) async throws { + do { + guard let data = try await accountRemote.requestChallengeResponseToCreatePasskey(forEmail: email, password: password) else { + throw PasskeyError.couldNotRequestRegistrationChallenge + } + let passkeyChallenge = try JSONDecoder().decode(PasskeyRegistrationChallenge.self, from: data) + attemptRegistration(with: passkeyChallenge, presentationContext: presentationContext) + } catch { + throw PasskeyError.couldNotEncodeRegistrationChallenge + } + } + + private func attemptRegistration(with passkeyChallenge: PasskeyRegistrationChallenge, presentationContext: PresentationContext) { + guard let challengeData = passkeyChallenge.challengeData, + let userID = passkeyChallenge.userID else { + return + } + + let platformProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: passkeyChallenge.relayingPartyIdentifier) + let platformKeyRequest = platformProvider.createCredentialRegistrationRequest(challenge: challengeData, name: passkeyChallenge.displayName, userID: userID) + let authController = ASAuthorizationController(authorizationRequests: [platformKeyRequest]) + authController.delegate = self + authController.presentationContextProvider = presentationContext + authController.performRequests() + } + + private func performPasskeyRegistration(with credential: ASAuthorizationPlatformPublicKeyCredentialRegistration) { + guard let registrationObject = PasskeyRegistrationResponse(from: credential) else { + delegate?.failed(PasskeyError.couldNotPrepareRegistrationData) + return + } + + Task { + do { + let data = try JSONEncoder().encode(registrationObject) + try await accountRemote.registerCredential(with: data) + delegate?.succeed() + } catch { + delegate?.failed(error) + } + } + } + + // MARK: - Auth + // + func attemptPasskeyAuth(for email: String, in presentationContext: PresentationContext) async throws { + guard let challenge = try await fetchAuthChallenge(for: email) else { + return + } + + let challengeData = try Data.decodeUrlSafeBase64(challenge.challenge) + let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: challenge.relayingParty) + let request = provider.createCredentialAssertionRequest(challenge: challengeData) + + let controller = ASAuthorizationController(authorizationRequests: [request]) + controller.delegate = self + controller.presentationContextProvider = presentationContext + controller.performRequests() + } + + private func fetchAuthChallenge(for email: String) async throws -> PasskeyAuthChallenge? { + guard let data = try await AccountRemote().passkeyAuthChallenge(for: email) else { + return nil + } + + let challenge = try JSONDecoder().decode(PasskeyAuthChallenge.self, from: data) + return challenge + } + + private func performPasskeyAuthentication(with response: PasskeyAuthResponse) { + let json = try! JSONEncoder().encode(response) + + Task { @MainActor in + guard let response = try? await accountRemote.verifyPasskeyLogin(with: json), + let verifyResponse = try? JSONDecoder().decode(PasskeyVerifyResponse.self, from: response) else { + self.delegate?.failed(PasskeyError.authFailed) + return + } + + authenticator.authenticate(withUsername: verifyResponse.username, token: verifyResponse.accessToken) + self.delegate?.succeed() + } + } +} + +// MARK: - ASAuthorizationControllerDelegate +// +extension PasskeyAuthenticator: ASAuthorizationControllerDelegate { + public func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: any Error) { + if let delegate { + delegate.failed(error) + } + } + + public func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { + + switch authorization.credential { + case let credential as ASAuthorizationPlatformPublicKeyCredentialRegistration: + performPasskeyRegistration(with: credential) + + case let credential as ASAuthorizationPlatformPublicKeyCredentialAssertion: + let response = PasskeyAuthResponse(from: credential) + + performPasskeyAuthentication(with: response) + default: + break + } + } +} diff --git a/Simplenote/PasskeyRegistrationChallenge.swift b/Simplenote/PasskeyRegistrationChallenge.swift new file mode 100644 index 000000000..134393f8a --- /dev/null +++ b/Simplenote/PasskeyRegistrationChallenge.swift @@ -0,0 +1,43 @@ +import Foundation + +struct PasskeyRegistrationChallenge: Decodable { + private struct User: Decodable { + let name: String + let userID: String + + enum CodingKeys: String, CodingKey { + case name + case userID = "id" + } + } + + private struct RelayingParty: Decodable { + let id: String + } + + private let relayingParty: PasskeyRegistrationChallenge.RelayingParty + private let user: PasskeyRegistrationChallenge.User + private let challenge: String + + enum CodingKeys: String, CodingKey { + case relayingParty = "rp" + case user + case challenge + } + + var relayingPartyIdentifier: String { + relayingParty.id + } + + var challengeData: Data? { + challenge.data(using: .utf8) + } + + var displayName: String { + user.name + } + + var userID: Data? { + user.userID.data(using: .utf8) + } +} diff --git a/Simplenote/PasskeyRegistrationResponse.swift b/Simplenote/PasskeyRegistrationResponse.swift new file mode 100644 index 000000000..306447cc1 --- /dev/null +++ b/Simplenote/PasskeyRegistrationResponse.swift @@ -0,0 +1,45 @@ +import Foundation +import AuthenticationServices + +struct PasskeyRegistrationResponse: Encodable { + struct Response: Encodable { + let clientDataJSON: String + let attestationObject: String + } + + private let email: String + private let id: String + private let rawId: String + private let type: String + private let response: PasskeyRegistrationResponse.Response + + init?(from credentialRegistration: ASAuthorizationPlatformPublicKeyCredentialRegistration) { + guard let email = SPAppDelegate.shared().simperium.user?.email, + let clientJson = Self.prepareJSON(from: credentialRegistration.rawClientDataJSON), + let rawAttestationObject = credentialRegistration.rawAttestationObject else { + return nil + } + + let idString = credentialRegistration.credentialID.base64EncodedString().toBase64url() + let response = Response(clientDataJSON: clientJson.base64EncodedString(), attestationObject: rawAttestationObject.base64EncodedString()) + + self.email = email + self.id = idString + self.rawId = idString + self.type = "public-key" + self.response = response + } + + private static func prepareJSON(from data: Data) -> Data? { + guard var json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let base64challenge = json["challenge"] as? String, + let challengeData = Data(base64Encoded: base64challenge + "="), + let challenge = String(data: challengeData, encoding: .utf8) else { + return nil + } + + json["challenge"] = challenge + + return try? JSONSerialization.data(withJSONObject: json) + } +} diff --git a/Simplenote/PasskeyVerifyResponse.swift b/Simplenote/PasskeyVerifyResponse.swift new file mode 100644 index 000000000..cfde692e0 --- /dev/null +++ b/Simplenote/PasskeyVerifyResponse.swift @@ -0,0 +1,13 @@ +import Foundation + +struct PasskeyVerifyResponse: Decodable { + let username: String + let accessToken: String + let verified: Bool + + enum CodingKeys: String, CodingKey { + case username + case accessToken = "access_token" + case verified + } +} diff --git a/Simplenote/Remote.swift b/Simplenote/Remote.swift index 6b9424613..e4a39acd8 100644 --- a/Simplenote/Remote.swift +++ b/Simplenote/Remote.swift @@ -16,6 +16,7 @@ class Remote { let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0 // Check for 2xx status code + print("# statusCode: \(statusCode)") guard statusCode / 100 == 2 else { let error = statusCode > 0 ? RemoteError.requestError(statusCode, dataTaskError): @@ -30,4 +31,17 @@ class Remote { dataTask.resume() } + + func performDataTask(with request: URLRequest) async throws -> Data? { + try await withCheckedThrowingContinuation { continuation in + performDataTask(with: request) { result in + switch result { + case .success(let data): + continuation.resume(returning: data) + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } } diff --git a/Simplenote/SPAppDelegate+Extensions.swift b/Simplenote/SPAppDelegate+Extensions.swift index a2babfdfa..983abeaf5 100644 --- a/Simplenote/SPAppDelegate+Extensions.swift +++ b/Simplenote/SPAppDelegate+Extensions.swift @@ -40,6 +40,10 @@ extension SPAppDelegate { func setupStoreManager() { StoreManager.shared.initialize() } + + @objc func setupPasskeyAuthenticator() { + passkeyAuthenticator = PasskeyAuthenticator(authenticator: simperium.authenticator) + } } // MARK: - Internal Methods diff --git a/Simplenote/SPAppDelegate.h b/Simplenote/SPAppDelegate.h index 3c1db17c7..aa2f64cc6 100644 --- a/Simplenote/SPAppDelegate.h +++ b/Simplenote/SPAppDelegate.h @@ -14,6 +14,7 @@ @class PublishStateObserver; @class AccountDeletionController; @class CoreDataManager; +@class PasskeyAuthenticator; NS_ASSUME_NONNULL_BEGIN @@ -41,6 +42,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nullable, strong, nonatomic) AccountDeletionController *accountDeletionController; +@property (strong, nonatomic) PasskeyAuthenticator *passkeyAuthenticator; + - (void)presentSettingsViewController; - (void)save; diff --git a/Simplenote/SPAppDelegate.m b/Simplenote/SPAppDelegate.m index 847d1c47b..acfc52125 100644 --- a/Simplenote/SPAppDelegate.m +++ b/Simplenote/SPAppDelegate.m @@ -123,6 +123,7 @@ - (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions: [self setupThemeNotifications]; [self setupSimperium]; [self setupAuthenticator]; + [self setupPasskeyAuthenticator]; [self setupAppCenter]; [self setupCrashLogging]; [self configureVersionsController]; diff --git a/Simplenote/SPModalActivityIndicator.swift b/Simplenote/SPModalActivityIndicator.swift new file mode 100644 index 000000000..68bd812a0 --- /dev/null +++ b/Simplenote/SPModalActivityIndicator.swift @@ -0,0 +1,7 @@ +import Foundation + +extension SPModalActivityIndicator { + func dismiss(_ animated: Bool) { + dismiss(animated, completion: nil) + } +} diff --git a/Simplenote/Simplenote-Bridging-Header.h b/Simplenote/Simplenote-Bridging-Header.h index d433b86f9..ab0715d12 100644 --- a/Simplenote/Simplenote-Bridging-Header.h +++ b/Simplenote/Simplenote-Bridging-Header.h @@ -33,6 +33,7 @@ #import "SPTagEntryField.h" #import "WPAuthHandler.h" #import "NSManagedObjectContext+CoreDataExtensions.h" +#import "SPModalActivityIndicator.h" #pragma mark - Extensions diff --git a/Simplenote/Supporting Files/Simplenote-Internal.entitlements b/Simplenote/Supporting Files/Simplenote-Internal.entitlements index 523982a37..58b7e3a60 100644 --- a/Simplenote/Supporting Files/Simplenote-Internal.entitlements +++ b/Simplenote/Supporting Files/Simplenote-Internal.entitlements @@ -5,6 +5,7 @@ com.apple.developer.associated-domains webcredentials:simplenote.com + webcredentials:passkey-dev-dot-simple-note-hrd.appspot.com?mode=developer com.apple.security.application-groups diff --git a/Simplenote/Supporting Files/Simplenote.entitlements b/Simplenote/Supporting Files/Simplenote.entitlements index 523982a37..58b7e3a60 100644 --- a/Simplenote/Supporting Files/Simplenote.entitlements +++ b/Simplenote/Supporting Files/Simplenote.entitlements @@ -5,6 +5,7 @@ com.apple.developer.associated-domains webcredentials:simplenote.com + webcredentials:passkey-dev-dot-simple-note-hrd.appspot.com?mode=developer com.apple.security.application-groups