mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-15 17:16:16 +08:00
feat(ios): hashcash in swift (#8602)
This commit is contained in:
@@ -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 = "<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>"; };
|
||||
@@ -63,6 +65,7 @@
|
||||
504EC3051FED79650016851F /* Products */,
|
||||
7F8756D8B27F46E3366F6CEA /* Pods */,
|
||||
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
|
||||
9D6A85312CCF6D6B00DAB35F /* Recovered References */,
|
||||
);
|
||||
indentWidth = 2;
|
||||
sourceTree = "<group>";
|
||||
@@ -90,6 +93,7 @@
|
||||
children = (
|
||||
9D90BE172CCB9876006677DB /* CookieManager.swift */,
|
||||
9D90BE182CCB9876006677DB /* CookiePlugin.swift */,
|
||||
9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */,
|
||||
);
|
||||
path = Cookie;
|
||||
sourceTree = "<group>";
|
||||
@@ -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 */,
|
||||
);
|
||||
|
||||
@@ -10,6 +10,6 @@ class AFFiNEViewController: CAPBridgeViewController {
|
||||
|
||||
override func capacitorDidLoad() {
|
||||
bridge?.registerPluginInstance(CookiePlugin())
|
||||
bridge?.registerPluginInstance(HashcashPlugin())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ end
|
||||
target 'App' do
|
||||
capacitor_pods
|
||||
# Add your Pods here
|
||||
pod 'CryptoSwift', '~> 1.8.3'
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface HashcashPlugin {
|
||||
hash(options: {
|
||||
challenge: string;
|
||||
bits?: number;
|
||||
}): Promise<{ value: string }>;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { registerPlugin } from '@capacitor/core';
|
||||
|
||||
import type { HashcashPlugin } from './definitions';
|
||||
|
||||
const Hashcash = registerPlugin<HashcashPlugin>('Hashcash');
|
||||
|
||||
export * from './definitions';
|
||||
export { Hashcash };
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
Reference in New Issue
Block a user