feat(ios): hashcash in swift (#8602)
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* 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 */; };
|
9D90BE252CCB9876006677DB /* CookieManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE172CCB9876006677DB /* CookieManager.swift */; };
|
||||||
9D90BE262CCB9876006677DB /* CookiePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE182CCB9876006677DB /* CookiePlugin.swift */; };
|
9D90BE262CCB9876006677DB /* CookiePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE182CCB9876006677DB /* CookiePlugin.swift */; };
|
||||||
9D90BE272CCB9876006677DB /* AFFiNEViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE1B2CCB9876006677DB /* AFFiNEViewController.swift */; };
|
9D90BE272CCB9876006677DB /* AFFiNEViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE1B2CCB9876006677DB /* AFFiNEViewController.swift */; };
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
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>"; };
|
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>"; };
|
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>"; };
|
9D90BE1B2CCB9876006677DB /* AFFiNEViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AFFiNEViewController.swift; sourceTree = "<group>"; };
|
||||||
@@ -63,6 +65,7 @@
|
|||||||
504EC3051FED79650016851F /* Products */,
|
504EC3051FED79650016851F /* Products */,
|
||||||
7F8756D8B27F46E3366F6CEA /* Pods */,
|
7F8756D8B27F46E3366F6CEA /* Pods */,
|
||||||
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
|
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
|
||||||
|
9D6A85312CCF6D6B00DAB35F /* Recovered References */,
|
||||||
);
|
);
|
||||||
indentWidth = 2;
|
indentWidth = 2;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -90,6 +93,7 @@
|
|||||||
children = (
|
children = (
|
||||||
9D90BE172CCB9876006677DB /* CookieManager.swift */,
|
9D90BE172CCB9876006677DB /* CookieManager.swift */,
|
||||||
9D90BE182CCB9876006677DB /* CookiePlugin.swift */,
|
9D90BE182CCB9876006677DB /* CookiePlugin.swift */,
|
||||||
|
9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */,
|
||||||
);
|
);
|
||||||
path = Cookie;
|
path = Cookie;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -232,6 +236,7 @@
|
|||||||
files = (
|
files = (
|
||||||
9D90BE252CCB9876006677DB /* CookieManager.swift in Sources */,
|
9D90BE252CCB9876006677DB /* CookieManager.swift in Sources */,
|
||||||
9D90BE262CCB9876006677DB /* CookiePlugin.swift in Sources */,
|
9D90BE262CCB9876006677DB /* CookiePlugin.swift in Sources */,
|
||||||
|
9D6A85332CCF6DA700DAB35F /* HashcashPlugin.swift in Sources */,
|
||||||
9D90BE272CCB9876006677DB /* AFFiNEViewController.swift in Sources */,
|
9D90BE272CCB9876006677DB /* AFFiNEViewController.swift in Sources */,
|
||||||
9D90BE282CCB9876006677DB /* AppDelegate.swift in Sources */,
|
9D90BE282CCB9876006677DB /* AppDelegate.swift in Sources */,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ class AFFiNEViewController: CAPBridgeViewController {
|
|||||||
|
|
||||||
override func capacitorDidLoad() {
|
override func capacitorDidLoad() {
|
||||||
bridge?.registerPluginInstance(CookiePlugin())
|
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
|
target 'App' do
|
||||||
capacitor_pods
|
capacitor_pods
|
||||||
# Add your Pods here
|
# Add your Pods here
|
||||||
|
pod 'CryptoSwift', '~> 1.8.3'
|
||||||
end
|
end
|
||||||
|
|
||||||
post_install do |installer|
|
post_install do |installer|
|
||||||
|
|||||||
@@ -6,12 +6,18 @@ PODS:
|
|||||||
- CapacitorBrowser (6.0.3):
|
- CapacitorBrowser (6.0.3):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- CapacitorCordova (6.1.2)
|
- CapacitorCordova (6.1.2)
|
||||||
|
- CryptoSwift (1.8.3)
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- "Capacitor (from `../../../../../node_modules/@capacitor/ios`)"
|
- "Capacitor (from `../../../../../node_modules/@capacitor/ios`)"
|
||||||
- "CapacitorApp (from `../../../../../node_modules/@capacitor/app`)"
|
- "CapacitorApp (from `../../../../../node_modules/@capacitor/app`)"
|
||||||
- "CapacitorBrowser (from `../../../../../node_modules/@capacitor/browser`)"
|
- "CapacitorBrowser (from `../../../../../node_modules/@capacitor/browser`)"
|
||||||
- "CapacitorCordova (from `../../../../../node_modules/@capacitor/ios`)"
|
- "CapacitorCordova (from `../../../../../node_modules/@capacitor/ios`)"
|
||||||
|
- CryptoSwift (~> 1.8.3)
|
||||||
|
|
||||||
|
SPEC REPOS:
|
||||||
|
trunk:
|
||||||
|
- CryptoSwift
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
Capacitor:
|
Capacitor:
|
||||||
@@ -28,7 +34,8 @@ SPEC CHECKSUMS:
|
|||||||
CapacitorApp: 0bc633b4eae40a1f32cd2834788fad3bc42da6a1
|
CapacitorApp: 0bc633b4eae40a1f32cd2834788fad3bc42da6a1
|
||||||
CapacitorBrowser: aab1ed943b01c0365c4810538a8b3477e2d9f72e
|
CapacitorBrowser: aab1ed943b01c0365c4810538a8b3477e2d9f72e
|
||||||
CapacitorCordova: f48c89f96c319101cd2f0ce8a2b7449b5fb8b3dd
|
CapacitorCordova: f48c89f96c319101cd2f0ce8a2b7449b5fb8b3dd
|
||||||
|
CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483
|
||||||
|
|
||||||
PODFILE CHECKSUM: 0f32d90fb8184cf478f85b78b1c00db1059ac3aa
|
PODFILE CHECKSUM: 763e3dac392c17bcf42dab97a9225ea234e8416a
|
||||||
|
|
||||||
COCOAPODS: 1.15.2
|
COCOAPODS: 1.15.2
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import { Telemetry } from '@affine/core/components/telemetry';
|
|||||||
import { configureMobileModules } from '@affine/core/mobile/modules';
|
import { configureMobileModules } from '@affine/core/mobile/modules';
|
||||||
import { router } from '@affine/core/mobile/router';
|
import { router } from '@affine/core/mobile/router';
|
||||||
import { configureCommonModules } from '@affine/core/modules';
|
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 { I18nProvider } from '@affine/core/modules/i18n';
|
||||||
import { configureLocalStorageStateStorageImpls } from '@affine/core/modules/storage';
|
import { configureLocalStorageStateStorageImpls } from '@affine/core/modules/storage';
|
||||||
import { PopupWindowProvider } from '@affine/core/modules/url';
|
import { PopupWindowProvider } from '@affine/core/modules/url';
|
||||||
@@ -28,6 +32,7 @@ import { RouterProvider } from 'react-router-dom';
|
|||||||
|
|
||||||
import { configureFetchProvider } from './fetch';
|
import { configureFetchProvider } from './fetch';
|
||||||
import { Cookie } from './plugins/cookie';
|
import { Cookie } from './plugins/cookie';
|
||||||
|
import { Hashcash } from './plugins/hashcash';
|
||||||
|
|
||||||
const future = {
|
const future = {
|
||||||
v7_startTransition: true,
|
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();
|
const frameworkProvider = framework.provider();
|
||||||
|
|
||||||
// setup application lifecycle events, and emit application start event
|
// 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 }>;
|
||||||
|
}
|
||||||
8
packages/frontend/apps/ios/src/plugins/hashcash/index.ts
Normal file
8
packages/frontend/apps/ios/src/plugins/hashcash/index.ts
Normal file
@@ -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 isLoading = useLiveData(captchaService.isLoading$);
|
||||||
const verifyToken = useLiveData(captchaService.verifyToken$);
|
const verifyToken = useLiveData(captchaService.verifyToken$);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasCaptchaFeature) {
|
captchaService.revalidate();
|
||||||
captchaService.revalidate();
|
}, [captchaService]);
|
||||||
}
|
|
||||||
}, [captchaService, hasCaptchaFeature]);
|
|
||||||
|
|
||||||
const handleTurnstileSuccess = useCallback(
|
const handleTurnstileSuccess = useCallback(
|
||||||
(token: string) => {
|
(token: string) => {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
onStart,
|
onStart,
|
||||||
Service,
|
Service,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import { EMPTY, mergeMap, switchMap } from 'rxjs';
|
import { EMPTY, exhaustMap, mergeMap } from 'rxjs';
|
||||||
|
|
||||||
import type { ValidatorProvider } from '../provider/validator';
|
import type { ValidatorProvider } from '../provider/validator';
|
||||||
import type { FetchService } from './fetch';
|
import type { FetchService } from './fetch';
|
||||||
@@ -31,7 +31,7 @@ export class CaptchaService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
revalidate = effect(
|
revalidate = effect(
|
||||||
switchMap(() => {
|
exhaustMap(() => {
|
||||||
return fromPromise(async signal => {
|
return fromPromise(async signal => {
|
||||||
if (!this.needCaptcha$.value) {
|
if (!this.needCaptcha$.value) {
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
Reference in New Issue
Block a user