Skip to content

Commit

Permalink
feat(ios): hashcash in swift (#8602)
Browse files Browse the repository at this point in the history
  • Loading branch information
Brooooooklyn committed Oct 29, 2024
1 parent efee4df commit 5709ebb
Show file tree
Hide file tree
Showing 10 changed files with 152 additions and 9 deletions.
5 changes: 5 additions & 0 deletions packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand All @@ -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 = "<group>"; };
9D90BE172CCB9876006677DB /* CookieManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieManager.swift; sourceTree = "<group>"; };
9D90BE182CCB9876006677DB /* CookiePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookiePlugin.swift; sourceTree = "<group>"; };
9D90BE1B2CCB9876006677DB /* AFFiNEViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AFFiNEViewController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -63,6 +65,7 @@
504EC3051FED79650016851F /* Products */,
7F8756D8B27F46E3366F6CEA /* Pods */,
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
9D6A85312CCF6D6B00DAB35F /* Recovered References */,
);
indentWidth = 2;
sourceTree = "<group>";
Expand Down Expand Up @@ -90,6 +93,7 @@
children = (
9D90BE172CCB9876006677DB /* CookieManager.swift */,
9D90BE182CCB9876006677DB /* CookiePlugin.swift */,
9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */,
);
path = Cookie;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ class AFFiNEViewController: CAPBridgeViewController {

override func capacitorDidLoad() {
bridge?.registerPluginInstance(CookiePlugin())
bridge?.registerPluginInstance(HashcashPlugin())
}

}
107 changes: 107 additions & 0 deletions packages/frontend/apps/ios/App/App/plugins/Cookie/HashcashPlugin.swift
Original file line number Diff line number Diff line change
@@ -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..<SALT_LENGTH).map { _ in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".randomElement()! })
let challenge = "\(version):\(bits):\(ts):\(resource)::\(rand)"

let hexDigits = Int(ceil(Float(bits) / 4.0))
let zeros = String(repeating: "0", count: hexDigits)
var counter = 0
var counterHex = ""
var hasher = SHA3(variant: .sha256)

while true {
let toHash = "\(challenge):\(String(format: "%x", counter))"
let hashed = try! hasher.finish(withBytes: toHash.data(using: .utf8)!.bytes)
let result = hashed.compactMap { String(format: "%02x", $0) }.joined()

if result.prefix(hexDigits) == zeros {
counterHex = String(format: "%x", counter)
break
}
counter += 1
}

return Stamp(version: version, claim: bits, ts: ts, resource: resource, ext: "", rand: rand, counter: counterHex)
}
}

extension Stamp {
init?(from string: String) throws {
let parts = string.split(separator: ":")
guard parts.count == 7 else {
throw NSError(domain: "StampError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Malformed stamp, expected 7 parts, got \(parts.count)"])
}

guard let claim = UInt32(parts[1]) else {
throw NSError(domain: "StampError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Malformed stamp"])
}

self.version = String(parts[0])
self.claim = claim
self.ts = String(parts[2])
self.resource = String(parts[3])
self.ext = String(parts[4])
self.rand = String(parts[5])
self.counter = String(parts[6])
}
}
1 change: 1 addition & 0 deletions packages/frontend/apps/ios/App/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ end
target 'App' do
capacitor_pods
# Add your Pods here
pod 'CryptoSwift', '~> 1.8.3'
end

post_install do |installer|
Expand Down
9 changes: 8 additions & 1 deletion packages/frontend/apps/ios/App/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -28,7 +34,8 @@ SPEC CHECKSUMS:
CapacitorApp: 0bc633b4eae40a1f32cd2834788fad3bc42da6a1
CapacitorBrowser: aab1ed943b01c0365c4810538a8b3477e2d9f72e
CapacitorCordova: f48c89f96c319101cd2f0ce8a2b7449b5fb8b3dd
CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483

PODFILE CHECKSUM: 0f32d90fb8184cf478f85b78b1c00db1059ac3aa
PODFILE CHECKSUM: 763e3dac392c17bcf42dab97a9225ea234e8416a

COCOAPODS: 1.15.2
13 changes: 12 additions & 1 deletion packages/frontend/apps/ios/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface HashcashPlugin {
hash(options: {
challenge: string;
bits?: number;
}): Promise<{ value: string }>;
}
8 changes: 8 additions & 0 deletions packages/frontend/apps/ios/src/plugins/hashcash/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { registerPlugin } from '@capacitor/core';

import type { HashcashPlugin } from './definitions';

const Hashcash = registerPlugin<HashcashPlugin>('Hashcash');

export * from './definitions';
export { Hashcash };
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
4 changes: 2 additions & 2 deletions packages/frontend/core/src/modules/cloud/services/captcha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -31,7 +31,7 @@ export class CaptchaService extends Service {
}

revalidate = effect(
switchMap(() => {
exhaustMap(() => {
return fromPromise(async signal => {
if (!this.needCaptcha$.value) {
return {};
Expand Down

0 comments on commit 5709ebb

Please sign in to comment.