From 5709ebbb118da0e49ab0485b38b20f8108c75347 Mon Sep 17 00:00:00 2001 From: Brooooooklyn Date: Tue, 29 Oct 2024 08:40:15 +0000 Subject: [PATCH] feat(ios): hashcash in swift (#8602) --- .../ios/App/App.xcodeproj/project.pbxproj | 5 + .../ios/App/App/AFFiNEViewController.swift | 2 +- .../App/plugins/Cookie/HashcashPlugin.swift | 107 ++++++++++++++++++ packages/frontend/apps/ios/App/Podfile | 1 + packages/frontend/apps/ios/App/Podfile.lock | 9 +- packages/frontend/apps/ios/src/app.tsx | 13 ++- .../ios/src/plugins/hashcash/definitions.ts | 6 + .../apps/ios/src/plugins/hashcash/index.ts | 8 ++ .../components/affine/auth/use-captcha.tsx | 6 +- .../src/modules/cloud/services/captcha.ts | 4 +- 10 files changed, 152 insertions(+), 9 deletions(-) create mode 100644 packages/frontend/apps/ios/App/App/plugins/Cookie/HashcashPlugin.swift create mode 100644 packages/frontend/apps/ios/src/plugins/hashcash/definitions.ts create mode 100644 packages/frontend/apps/ios/src/plugins/hashcash/index.ts diff --git a/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj b/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj index f97a0346e3..c7453edb67 100644 --- a/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj +++ b/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 9D6A85332CCF6DA700DAB35F /* HashcashPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */; }; 9D90BE252CCB9876006677DB /* CookieManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE172CCB9876006677DB /* CookieManager.swift */; }; 9D90BE262CCB9876006677DB /* CookiePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE182CCB9876006677DB /* CookiePlugin.swift */; }; 9D90BE272CCB9876006677DB /* AFFiNEViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE1B2CCB9876006677DB /* AFFiNEViewController.swift */; }; @@ -21,6 +22,7 @@ /* Begin PBXFileReference section */ 504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashcashPlugin.swift; sourceTree = ""; }; 9D90BE172CCB9876006677DB /* CookieManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieManager.swift; sourceTree = ""; }; 9D90BE182CCB9876006677DB /* CookiePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookiePlugin.swift; sourceTree = ""; }; 9D90BE1B2CCB9876006677DB /* AFFiNEViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AFFiNEViewController.swift; sourceTree = ""; }; @@ -63,6 +65,7 @@ 504EC3051FED79650016851F /* Products */, 7F8756D8B27F46E3366F6CEA /* Pods */, 27E2DDA53C4D2A4D1A88CE4A /* Frameworks */, + 9D6A85312CCF6D6B00DAB35F /* Recovered References */, ); indentWidth = 2; sourceTree = ""; @@ -90,6 +93,7 @@ children = ( 9D90BE172CCB9876006677DB /* CookieManager.swift */, 9D90BE182CCB9876006677DB /* CookiePlugin.swift */, + 9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */, ); path = Cookie; sourceTree = ""; @@ -232,6 +236,7 @@ files = ( 9D90BE252CCB9876006677DB /* CookieManager.swift in Sources */, 9D90BE262CCB9876006677DB /* CookiePlugin.swift in Sources */, + 9D6A85332CCF6DA700DAB35F /* HashcashPlugin.swift in Sources */, 9D90BE272CCB9876006677DB /* AFFiNEViewController.swift in Sources */, 9D90BE282CCB9876006677DB /* AppDelegate.swift in Sources */, ); diff --git a/packages/frontend/apps/ios/App/App/AFFiNEViewController.swift b/packages/frontend/apps/ios/App/App/AFFiNEViewController.swift index 0e25db8900..bddedfec4b 100644 --- a/packages/frontend/apps/ios/App/App/AFFiNEViewController.swift +++ b/packages/frontend/apps/ios/App/App/AFFiNEViewController.swift @@ -10,6 +10,6 @@ class AFFiNEViewController: CAPBridgeViewController { override func capacitorDidLoad() { bridge?.registerPluginInstance(CookiePlugin()) + bridge?.registerPluginInstance(HashcashPlugin()) } - } diff --git a/packages/frontend/apps/ios/App/App/plugins/Cookie/HashcashPlugin.swift b/packages/frontend/apps/ios/App/App/plugins/Cookie/HashcashPlugin.swift new file mode 100644 index 0000000000..90f42d6123 --- /dev/null +++ b/packages/frontend/apps/ios/App/App/plugins/Cookie/HashcashPlugin.swift @@ -0,0 +1,107 @@ +import Capacitor +import CryptoSwift + +@objc(HashcashPlugin) +public class HashcashPlugin: CAPPlugin, CAPBridgedPlugin { + public let identifier = "HashcashPlugin" + public let jsName = "Hashcash" + public let pluginMethods: [CAPPluginMethod] = [ + CAPPluginMethod(name: "hash", returnType: CAPPluginReturnPromise) + ] + + @objc func hash(_ call: CAPPluginCall) { + DispatchQueue.global(qos: .default).async { + let challenge = call.getString("challenge") ?? "" + let bits = call.getInt("bits") ?? 20; + call.resolve(["value": Stamp.mint(resource: challenge, bits: UInt32(bits)).format()]) + } + } +} + +let SALT_LENGTH = 16 + +struct Stamp { + let version: String + let claim: UInt32 + let ts: String + let resource: String + let ext: String + let rand: String + let counter: String + + func checkExpiration() -> Bool { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyyMMddHHmmss" + guard let date = dateFormatter.date(from: ts) else { return false } + return Date().addingTimeInterval(5 * 60) <= date + } + + func check(bits: UInt32, resource: String) -> Bool { + if version == "1" && bits <= claim && checkExpiration() && self.resource == resource { + let hexDigits = Int(floor(Float(claim) / 4.0)) + + // Check challenge + let formatted = format() + let result = formatted.data(using: .utf8)!.sha3(.sha256).compactMap { String(format: "%02x", $0) }.joined() + return result.prefix(hexDigits) == String(repeating: "0", count: hexDigits) + } else { + return false + } + } + + func format() -> String { + return "\(version):\(claim):\(ts):\(resource):\(ext):\(rand):\(counter)" + } + + static func mint(resource: String, bits: UInt32? = nil) -> Stamp { + let version = "1" + let now = Date() + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyyMMddHHmmss" + let ts = dateFormatter.string(from: now) + let bits = bits ?? 20 + let rand = String((0.. 1.8.3' end post_install do |installer| diff --git a/packages/frontend/apps/ios/App/Podfile.lock b/packages/frontend/apps/ios/App/Podfile.lock index f6a8f3ed5a..b142a33061 100644 --- a/packages/frontend/apps/ios/App/Podfile.lock +++ b/packages/frontend/apps/ios/App/Podfile.lock @@ -6,12 +6,18 @@ PODS: - CapacitorBrowser (6.0.3): - Capacitor - CapacitorCordova (6.1.2) + - CryptoSwift (1.8.3) DEPENDENCIES: - "Capacitor (from `../../../../../node_modules/@capacitor/ios`)" - "CapacitorApp (from `../../../../../node_modules/@capacitor/app`)" - "CapacitorBrowser (from `../../../../../node_modules/@capacitor/browser`)" - "CapacitorCordova (from `../../../../../node_modules/@capacitor/ios`)" + - CryptoSwift (~> 1.8.3) + +SPEC REPOS: + trunk: + - CryptoSwift EXTERNAL SOURCES: Capacitor: @@ -28,7 +34,8 @@ SPEC CHECKSUMS: CapacitorApp: 0bc633b4eae40a1f32cd2834788fad3bc42da6a1 CapacitorBrowser: aab1ed943b01c0365c4810538a8b3477e2d9f72e CapacitorCordova: f48c89f96c319101cd2f0ce8a2b7449b5fb8b3dd + CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483 -PODFILE CHECKSUM: 0f32d90fb8184cf478f85b78b1c00db1059ac3aa +PODFILE CHECKSUM: 763e3dac392c17bcf42dab97a9225ea234e8416a COCOAPODS: 1.15.2 diff --git a/packages/frontend/apps/ios/src/app.tsx b/packages/frontend/apps/ios/src/app.tsx index 3317f1c7fc..9678c83f8c 100644 --- a/packages/frontend/apps/ios/src/app.tsx +++ b/packages/frontend/apps/ios/src/app.tsx @@ -4,7 +4,11 @@ import { Telemetry } from '@affine/core/components/telemetry'; import { configureMobileModules } from '@affine/core/mobile/modules'; import { router } from '@affine/core/mobile/router'; import { configureCommonModules } from '@affine/core/modules'; -import { AuthService, WebSocketAuthProvider } from '@affine/core/modules/cloud'; +import { + AuthService, + ValidatorProvider, + WebSocketAuthProvider, +} from '@affine/core/modules/cloud'; import { I18nProvider } from '@affine/core/modules/i18n'; import { configureLocalStorageStateStorageImpls } from '@affine/core/modules/storage'; import { PopupWindowProvider } from '@affine/core/modules/url'; @@ -28,6 +32,7 @@ import { RouterProvider } from 'react-router-dom'; import { configureFetchProvider } from './fetch'; import { Cookie } from './plugins/cookie'; +import { Hashcash } from './plugins/hashcash'; const future = { v7_startTransition: true, @@ -66,6 +71,12 @@ framework.impl(WebSocketAuthProvider, { }; }, }); +framework.impl(ValidatorProvider, { + async validate(_challenge, resource) { + const res = await Hashcash.hash({ challenge: resource }); + return res.value; + }, +}); const frameworkProvider = framework.provider(); // setup application lifecycle events, and emit application start event diff --git a/packages/frontend/apps/ios/src/plugins/hashcash/definitions.ts b/packages/frontend/apps/ios/src/plugins/hashcash/definitions.ts new file mode 100644 index 0000000000..cde1b6fb6c --- /dev/null +++ b/packages/frontend/apps/ios/src/plugins/hashcash/definitions.ts @@ -0,0 +1,6 @@ +export interface HashcashPlugin { + hash(options: { + challenge: string; + bits?: number; + }): Promise<{ value: string }>; +} diff --git a/packages/frontend/apps/ios/src/plugins/hashcash/index.ts b/packages/frontend/apps/ios/src/plugins/hashcash/index.ts new file mode 100644 index 0000000000..40fc789766 --- /dev/null +++ b/packages/frontend/apps/ios/src/plugins/hashcash/index.ts @@ -0,0 +1,8 @@ +import { registerPlugin } from '@capacitor/core'; + +import type { HashcashPlugin } from './definitions'; + +const Hashcash = registerPlugin('Hashcash'); + +export * from './definitions'; +export { Hashcash }; diff --git a/packages/frontend/core/src/components/affine/auth/use-captcha.tsx b/packages/frontend/core/src/components/affine/auth/use-captcha.tsx index fee59ba1c3..8546386dcc 100644 --- a/packages/frontend/core/src/components/affine/auth/use-captcha.tsx +++ b/packages/frontend/core/src/components/affine/auth/use-captcha.tsx @@ -11,10 +11,8 @@ export const Captcha = () => { const isLoading = useLiveData(captchaService.isLoading$); const verifyToken = useLiveData(captchaService.verifyToken$); useEffect(() => { - if (hasCaptchaFeature) { - captchaService.revalidate(); - } - }, [captchaService, hasCaptchaFeature]); + captchaService.revalidate(); + }, [captchaService]); const handleTurnstileSuccess = useCallback( (token: string) => { diff --git a/packages/frontend/core/src/modules/cloud/services/captcha.ts b/packages/frontend/core/src/modules/cloud/services/captcha.ts index 4a8166ea02..8f9e92aeed 100644 --- a/packages/frontend/core/src/modules/cloud/services/captcha.ts +++ b/packages/frontend/core/src/modules/cloud/services/captcha.ts @@ -7,7 +7,7 @@ import { onStart, Service, } from '@toeverything/infra'; -import { EMPTY, mergeMap, switchMap } from 'rxjs'; +import { EMPTY, exhaustMap, mergeMap } from 'rxjs'; import type { ValidatorProvider } from '../provider/validator'; import type { FetchService } from './fetch'; @@ -31,7 +31,7 @@ export class CaptchaService extends Service { } revalidate = effect( - switchMap(() => { + exhaustMap(() => { return fromPromise(async signal => { if (!this.needCaptcha$.value) { return {};