diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b5fc202..868592e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,112 +1,34 @@ use tauri::{window::Color, WebviewUrl, WebviewWindowBuilder}; -#[tauri::command] -fn navigate(webview_window: tauri::WebviewWindow, url: String) { - _ = webview_window.navigate(tauri::Url::parse(&url).unwrap()); -} - -pub const ARKOSE_INIT_SCRIPT: &str = r#" -if (window.location.origin === "https://client-api.arkoselabs.com") { - const tweak = (obj, prop, val) => obj.__defineGetter__(prop, () => val) - - tweak(navigator, "hardwareConcurrency", 4) - tweak(navigator, "userAgentData", null) - tweak(navigator, "platform", "iPhone") - tweak(navigator, "connection", null) - - tweak(window.screen, "height", 852) - tweak(window.screen, "width", 393) - - tweak(window.screen, "availHeight", 852) - tweak(window.screen, "availWidth", 393) - - tweak(window.screen, "pixelDepth", 24) - tweak(window.screen, "colorDepth", 24) - - tweak(window.screen.orientation, "type", "portrait-primary") - tweak(window, "devicePixelRatio", 3) - - tweak(window, "outerHeight", 0) - tweak(window, "outerWidth", 0) - - const originalWebGLParameter = WebGLRenderingContext.prototype.getParameter; - WebGLRenderingContext.prototype.getParameter = function (pname) { - switch (pname) { - case 0x9245: // UNMASKED_VENDOR_WEBGL - return "Apple Inc."; - case 0x9246: // UNMASKED_RENDERER_WEBGL - return "Apple GPU"; - case 0x1F01: // RENDERER - return "WebKit WebGL"; - case 0x1F00: // VENDOR - return "WebKit"; - case 0x1F02: // VERSION - return "WebGL 1.0"; - case 0x8B8C: // SHADING_LANGUAGE_VERSION - return "WebGL GLSL ES 1.0 (1.0)"; - } - - return originalWebGLParameter.call(this, pname); - } - - const originalWebGLAttributes = WebGLRenderingContext.prototype.getContextAttributes; - WebGLRenderingContext.prototype.getContextAttributes = function () { - const attributes = originalWebGLAttributes.call(this) ?? {}; - attributes.antialias = true; - return attributes; - } - - WebGLRenderingContext.prototype.getSupportedExtensions = function () { - return [ - "ANGLE_instanced_arrays", "EXT_blend_minmax", "EXT_clip_control", - "EXT_color_buffer_half_float", "EXT_depth_clamp", "EXT_float_blend", - "EXT_frag_depth", "EXT_polygon_offset_clamp", "EXT_shader_texture_lod", - "EXT_texture_compression_bptc", "EXT_texture_compression_rgtc", - "EXT_texture_filter_anisotropic", "EXT_texture_mirror_clamp_to_edge", - "EXT_sRGB", "KHR_parallel_shader_compile", "OES_element_index_uint", - "OES_fbo_render_mipmap", "OES_standard_derivatives", "OES_texture_float", - "OES_texture_float_linear", "OES_texture_half_float", - "OES_texture_half_float_linear", "OES_vertex_array_object", - "WEBGL_blend_func_extended", "WEBGL_color_buffer_float", - "WEBGL_compressed_texture_astc", "WEBGL_compressed_texture_etc", - "WEBGL_compressed_texture_etc1", "WEBGL_compressed_texture_pvrtc", - "WEBKIT_WEBGL_compressed_texture_pvrtc", "WEBGL_compressed_texture_s3tc", - "WEBGL_compressed_texture_s3tc_srgb", "WEBGL_debug_renderer_info", - "WEBGL_debug_shaders", "WEBGL_depth_texture", "WEBGL_draw_buffers", - "WEBGL_lose_context", "WEBGL_multi_draw", "WEBGL_polygon_mode" - ]; - } -} -"#; - #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - tauri::Builder::default() - .plugin(tauri_plugin_opener::init()) - .plugin(tauri_plugin_dialog::init()) - .plugin(tauri_plugin_os::init()) - .plugin(tauri_plugin_http::init()) - .plugin(tauri_plugin_internal_api::init()) - .setup(|app| { - let mut win = WebviewWindowBuilder::new(app, "main", WebviewUrl::App("index.html".into())) - .background_color(Color(0, 0, 0, 255)) - .user_agent("Mozilla/5.0 (iPhone; CPU iPhone OS 19_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148") - .initialization_script_for_all_frames(ARKOSE_INIT_SCRIPT); - - #[cfg(not(mobile))] - { - win = win - .title("StayReal") - .theme(Some(tauri::Theme::Dark)) - .inner_size(436.0, 800.0) - .min_inner_size(436.0, 600.0) - } - - win.build()?; - - Ok(()) - }) - .invoke_handler(tauri::generate_handler![navigate]) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); + tauri::Builder::default() + .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_os::init()) + .plugin(tauri_plugin_http::init()) + .plugin(tauri_plugin_internal_api::init()) + .setup(|app| { + let mut win = WebviewWindowBuilder::new(app, "main", WebviewUrl::App("index.html".into())) + .background_color(Color(0, 0, 0, 255)) + .user_agent("Mozilla/5.0 (iPhone; CPU iPhone OS 19_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"); + + #[cfg(not(mobile))] + { + win = win + .title("StayReal") + .theme(Some(tauri::Theme::Dark)) + .inner_size(436.0, 800.0) + .min_inner_size(436.0, 600.0) + } + + let webview_window = win.build()?; + + #[cfg(all(debug_assertions, not(mobile)))] + webview_window.open_devtools(); + + Ok(()) + }) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); } diff --git a/src/api/constants.ts b/src/api/constants.ts index 5be91cf..386b741 100644 --- a/src/api/constants.ts +++ b/src/api/constants.ts @@ -2,16 +2,15 @@ import { hex } from '@scure/base'; import { createBeRealSignature } from "./core/signature"; export const BEREAL_IOS_BUNDLE_ID = "AlexisBarreyat.BeReal"; -export const BEREAL_IOS_VERSION = "4.24.0"; -export const BEREAL_IOS_BUILD = "20523"; +export const BEREAL_IOS_VERSION = "4.65.0"; +export const BEREAL_IOS_BUILD = "22654"; export const BEREAL_TIMEZONE = new Intl.DateTimeFormat().resolvedOptions().timeZone; -export const BEREAL_ARKOSE_PUBLIC_KEY = "CCB0863E-D45D-42E9-A6C8-9E8544E8B17E"; export const BEREAL_HMAC_KEY = hex.decode('3536303337663461663232666236393630663363643031346532656337316233') export const BEREAL_FIREBASE_KEY = "AIzaSyCgNTZt6gzPMh-2voYXOvrt_UR_gpGl83Q"; export const BEREAL_CLIENT_SECRET = "962D357B-B134-4AB6-8F53-BEA2B7255420"; export const BEREAL_PLATFORM = "iOS"; -export const BEREAL_PLATFORM_VERSION = "19.0"; +export const BEREAL_PLATFORM_VERSION = "26.2"; export const BEREAL_DEFAULT_HEADERS = (deviceID: string) => ({ "bereal-platform": BEREAL_PLATFORM, diff --git a/src/api/requests/auth/vonage/data-exchange.ts b/src/api/requests/auth/vonage/data-exchange.ts deleted file mode 100644 index ee1bf26..0000000 --- a/src/api/requests/auth/vonage/data-exchange.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { BEREAL_DEFAULT_HEADERS } from "~/api/constants"; -import { BeRealError } from "~/api/models/errors"; -import { fetch } from "@tauri-apps/plugin-http"; - -export const postVonageDataExchange = async (inputs: { - deviceID: string - phoneNumber: string -}): Promise => { - const response = await fetch("https://auth-l7.bereal.com/api/vonage/data-exchange", { - method: "POST", - headers: { - ...BEREAL_DEFAULT_HEADERS(inputs.deviceID), - "Content-Type": "application/json" - }, - body: JSON.stringify({ - phoneNumber: inputs.phoneNumber, - }), - }); - - if (response.status !== 200) - throw new BeRealError("Not able to get data exchange for this phone number, please check the phone number and try again."); - - const json = await response.json() as { - dataExchange: string - }; - - return json.dataExchange; -}; diff --git a/src/api/requests/auth/vonage/request.ts b/src/api/requests/auth/vonage/request.ts index bb4fa06..2b58052 100644 --- a/src/api/requests/auth/vonage/request.ts +++ b/src/api/requests/auth/vonage/request.ts @@ -4,7 +4,6 @@ import { fetch } from "@tauri-apps/plugin-http"; export const VonageRequestCodeTokenIdentifier = { RECAPTCHA: "RE", - ARKOSE: "AR" } as const; export type VonageRequestCodeTokenIdentifier = typeof VonageRequestCodeTokenIdentifier[keyof typeof VonageRequestCodeTokenIdentifier]; diff --git a/src/components/arkose.tsx b/src/components/arkose.tsx deleted file mode 100644 index 37b5b67..0000000 --- a/src/components/arkose.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { BEREAL_IOS_BUNDLE_ID, BEREAL_IOS_VERSION, BEREAL_PLATFORM_VERSION } from "~/api/constants"; - -import { sha256 } from '@noble/hashes/sha2'; -import { bytesToHex, randomBytes } from '@noble/hashes/utils'; - -const between = (min: number, max: number): number => { - return Math.floor(Math.random() * (max - min + 1)) + min; -}; - -export const createArkoseURL = (key: string, dataExchange: string, deviceId: string) => { - // After the challenge, we have to go back to our Tauri app. - const callback = window.location.origin + window.location.pathname; - - // Timestamp as of right now to generate the biometric motion data. - const timestamp = Math.floor(Date.now() / 1000); - - // @see https://appledb.dev/device/iPhone-15-Pro.html - // Currently: iPhone 15 Pro - const product = "iPhone16,1"; - - const html = `` - + `` - + `` - + ``; - return "data:text/html;base64," + btoa(html); -}; diff --git a/src/views/login.tsx b/src/views/login.tsx index ba4e362..4c1b222 100644 --- a/src/views/login.tsx +++ b/src/views/login.tsx @@ -1,146 +1,175 @@ -import { createEffect, on, Show, type Component } from "solid-js"; -import { useNavigate, useSearchParams } from "@solidjs/router"; +import { Show, type Component } from "solid-js"; +import { useNavigate } from "@solidjs/router"; import { createStore } from "solid-js/store"; import { v4 as uuidv4 } from "uuid"; -import { vonage_request_code, VonageRequestCodeTokenIdentifier } from "~/api/requests/auth/vonage/request"; +import { vonage_request_code } from "~/api/requests/auth/vonage/request"; import { firebase_verify_custom_token } from "~/api/requests/auth/firebase/verify-custom-token"; import { vonage_verify_otp } from "~/api/requests/auth/vonage/verify"; import { grant_firebase } from "~/api/requests/auth/token"; -import { BEREAL_ARKOSE_PUBLIC_KEY } from "~/api/constants"; -import { createArkoseURL } from "~/components/arkose"; import auth from "~/stores/auth"; import { DEMO_ACCESS_TOKEN, DEMO_PHONE_NUMBER, DEMO_REFRESH_TOKEN } from "~/utils/demo"; -import MdiChevronLeft from '~icons/mdi/chevron-left' -import { postVonageDataExchange } from "~/api/requests/auth/vonage/data-exchange"; +import MdiChevronLeft from "~icons/mdi/chevron-left"; import Otp from "~/components/otp"; -import MdiLoading from '~icons/mdi/loading' +import MdiLoading from "~icons/mdi/loading"; import { BeRealError } from "~/api/models/errors"; -import { invoke } from "@tauri-apps/api/core"; import StayRealLogo from "~icons/stayreal/logo"; import { openUrl } from "@tauri-apps/plugin-opener"; +type LoginStep = "phone" | "otp"; +type LoginFlow = + | "idle" + | "requestingCode" + | "awaitingOtp" + | "verifyingOtp" + | "authenticated"; + const LoginView: Component = () => { const navigate = useNavigate(); - const [params, setParams] = useSearchParams<{ arkoseToken: string }>() const [state, setState] = createStore({ - step: "phone" as ("phone" | "otp"), - deviceID: localStorage.getItem("login__deviceID") || uuidv4(), + step: "phone" as LoginStep, + flow: "idle" as LoginFlow, + deviceID: uuidv4(), error: null as string | null, - loading: false, - phoneNumber: localStorage.getItem("login__phoneNumber") || "", + phoneNumber: "", requestID: "", otp: "", - }) + }); - const runAuthentication = async (): Promise => { - if (!state.phoneNumber) return; + const normalizedPhoneNumber = (): string => { + return state.phoneNumber.replace(/ /g, ""); + }; + + const mapErrorMessage = (error: unknown): string => { + if (error instanceof BeRealError) + return error.message; + + return "An error occurred while authenticating, please try again later."; + }; + + const setPhoneError = (message: string): void => { + setState({ + flow: "idle", + error: message, + }); + }; + + const setOtpError = (message: string): void => { + setState({ + flow: "awaitingOtp", + error: message, + }); + }; - // Make sure there's no whitespace in the phone number. - const phoneNumber = state.phoneNumber.replace(/ /g, ""); + const requestPhoneVerificationCode = async (phoneNumber: string): Promise => { + setState({ + error: null, + flow: "requestingCode", + }); try { - setState("loading", true); - - if (state.step === "phone") { - let requestID: string; - - if (phoneNumber === DEMO_PHONE_NUMBER) { - requestID = "demo"; - } - else { - if (!params.arkoseToken) { - const dataExchange = await postVonageDataExchange({ - deviceID: state.deviceID, - phoneNumber - }); - - // Save the state in `localStorage` to remember it - // when we come back from the Arkose challenge. - localStorage.setItem("login__phoneNumber", phoneNumber); - localStorage.setItem("login__deviceID", state.deviceID); - - const url = createArkoseURL(BEREAL_ARKOSE_PUBLIC_KEY, dataExchange, state.deviceID); - // For some odd reason, we can't use `location.href = url` here - // so we're doing it through a Tauri command instead. - return invoke("navigate", { url }); - } - - // We can safely remove the temporary state from localStorage. - localStorage.removeItem("login__phoneNumber"); - localStorage.removeItem("login__deviceID"); - - const token = params.arkoseToken; - // One time use, we don't need it anymore. - setParams({ arkoseToken: void 0 }); - - requestID = await vonage_request_code({ - deviceID: state.deviceID, - phoneNumber, - tokens: [{ - identifier: VonageRequestCodeTokenIdentifier.ARKOSE, - token, - }] - }); - } - - setState({ step: "otp", requestID }); - } - else if (state.step === "otp") { - if (phoneNumber === DEMO_PHONE_NUMBER) { - await auth.save({ - deviceId: state.deviceID, - accessToken: DEMO_ACCESS_TOKEN, - refreshToken: DEMO_REFRESH_TOKEN(0) - }); - } - else { - // fun fact: this should match `grant_firebase`'s `access_token` value - const token = await vonage_verify_otp({ - requestID: state.requestID, - deviceID: state.deviceID, - otp: state.otp.trim() - }); - - const idToken = await firebase_verify_custom_token(token); - const tokens = await grant_firebase({ - deviceID: state.deviceID, - idToken - }); - - await auth.save({ - deviceId: state.deviceID, - accessToken: tokens.access_token, - refreshToken: tokens.refresh_token - }); - } - - navigate("/feed/friends"); - } + const requestID = await vonage_request_code({ + deviceID: state.deviceID, + phoneNumber, + tokens: [], + }); + + setState({ + step: "otp", + flow: "awaitingOtp", + requestID, + otp: "", + error: null, + }); } catch (error) { - if (error instanceof BeRealError) { - setState("error", error.message); + setPhoneError(mapErrorMessage(error)); + } + }; + + const authenticateOtp = async (): Promise => { + const phoneNumber = normalizedPhoneNumber(); + if (!phoneNumber || !state.requestID) + return; + + setState({ + error: null, + flow: "verifyingOtp", + }); + + try { + if (phoneNumber === DEMO_PHONE_NUMBER) { + await auth.save({ + deviceId: state.deviceID, + accessToken: DEMO_ACCESS_TOKEN, + refreshToken: DEMO_REFRESH_TOKEN(0), + }); } else { - setState("error", "An error occurred while authenticating, please try again later."); + const token = await vonage_verify_otp({ + requestID: state.requestID, + deviceID: state.deviceID, + otp: state.otp.trim(), + }); + + const idToken = await firebase_verify_custom_token(token); + const tokens = await grant_firebase({ + deviceID: state.deviceID, + idToken, + }); + + await auth.save({ + deviceId: state.deviceID, + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token, + }); } + + setState("flow", "authenticated"); + navigate("/feed/friends"); } - finally { - setState("loading", false); + catch (error) { + setOtpError(mapErrorMessage(error)); } }; - // If we're coming back from the Arkose challenge, we need to - // re-run the authentication process. - createEffect(on(() => params.arkoseToken, (token) => { - if (token) - runAuthentication(); - })); + const runAuthentication = async (): Promise => { + if (!state.phoneNumber) + return; + + const phoneNumber = normalizedPhoneNumber(); + + if (state.step === "phone") { + if (phoneNumber === DEMO_PHONE_NUMBER) { + setState({ + step: "otp", + flow: "awaitingOtp", + requestID: "demo", + otp: "", + error: null, + }); + + return; + } + + await requestPhoneVerificationCode(phoneNumber); + return; + } + + await authenticateOtp(); + }; + + const isPhoneLoading = (): boolean => { + return state.flow === "requestingCode"; + }; + + const isOtpLoading = (): boolean => { + return state.flow === "verifyingOtp" || state.flow === "authenticated"; + }; return (
@@ -152,7 +181,10 @@ const LoginView: Component = () => { onClick={() => { setState({ step: "phone", + flow: "idle", + requestID: "", otp: "", + error: null, }); }} > @@ -171,7 +203,7 @@ const LoginView: Component = () => { class="flex flex-col gap-4 h-full pb-6" onSubmit={(event) => { event.preventDefault(); - runAuthentication(); + void runAuthentication(); }} > @@ -188,53 +220,50 @@ const LoginView: Component = () => {

By continuing, you agree that StayReal is not affiliated with BeReal and that you are using this service at your own risk. -

+

You also agree to and and .

- -

- {state.error} -

-
- -
+ { setState("otp", code); - runAuthentication(); + void runAuthentication(); }} /> - }> - - You're authenticating on the demonstration account, your OTP code is 123456 + You're authenticating on the demonstration account, your OTP code is 123456

}>

@@ -244,6 +273,12 @@ const LoginView: Component = () => { + +

+ {state.error} +

+
+