# rustenium-identity — Technical Specification
**Version:** 0.2.0
**Depends on:** `rustenium` (CDP browser automation)
**Language:** Rust (async / tokio)
**Reference implementation:** `ish-bot-fp-spoofer` (Chrome extension, JS)
---
## 1. Overview
`rustenium-identity` is an overlay crate for `rustenium` that transforms a raw
CDP browser session into a **fully-personified browser instance**. It consumes
a serialised identity record (as produced by `persona_generator.py` / MongoDB)
and applies every signal needed to make the session indistinguishable from a
real user on that device, OS, and browser.
The stealth logic is informed by `ish-bot-fp-spoofer`, replicating the same
spoofing behaviour through CDP script injection instead of a loaded extension.
The crate does **not** replace `rustenium`'s page/navigation API. It only adds
an initialization layer that wires an `Identity` struct into a launch config
and post-launch bootstrap, then exposes helpers that keep subsequent
interactions consistent with the chosen identity.
---
## 2. Scope
| UA string construction (all OS/browser combos) | Cookie management |
| CDP emulation overrides (screen, locale, timezone, touch, hardware concurrency) | Session persistence / DB writes |
| Client Hints (Sec-CH-UA) for Chrome/Edge (non-iOS) | Anti-bot challenge solving |
| JS stealth injection via `Page.addScriptToEvaluateOnNewDocument` | Browser binary management |
| Navigator, WebGL, battery, history, screen spoofing | Extension loading / CRX packaging |
| iOS nuances (CriOS/EdgiOS UA, Safari browser block, no client hints) | Multi-extension orchestration |
| GPU identity + WebGL parameter spoofing | Human-like input helpers (handled by rustenium) |
| Identity deserialization from JSON / BSON | |
| Runtime GPU update via `Runtime.evaluate` | |
---
## 3. Identity Data Model
Mirrors the schema emitted by `persona_generator.py`.
```rust
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Identity {
pub id: Option<u64>,
/// None device_model implies desktop/laptop
pub device_model: Option<String>,
pub has_battery: bool,
pub has_mouse: bool,
pub has_touch: bool,
pub os: Os,
pub os_version: String,
pub platform: PlatformInfo,
pub browser: Browser,
/// Full version parts, e.g. [124, 0, 6367, 78]
pub browser_version: Vec<u16>,
pub screen: ScreenResolution,
pub hardware_concurrency: u8,
/// deviceMemory in GiB
pub memory: u8,
pub gpu: Gpu,
/// e.g. ["en-US", "en"]
pub language: Vec<String>,
pub history_count: Option<u16>,
/// Full proxy URL with credentials, optional
pub proxy: Option<String>,
/// IANA timezone string. If None, fetched from ip-api.com via proxy.
pub timezone: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlatformInfo {
pub bitness: Option<String>,
pub architecture: Option<String>,
pub navigator_platform: NavigatorPlatform,
pub version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum NavigatorPlatform {
Win32, MacIntel, LinuxX86_64, LinuxArmV81, IPhone, Other(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScreenResolution {
pub logical_width: u16,
pub logical_height: u16,
pub original_width: u16,
pub original_height: u16,
pub density_pixel_ratio: f32,
}
/// GPU identity — drives WebGL parameter spoofing. All fields are plain strings.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Gpu {
pub vendor: String,
pub webgl_renderer: String,
pub webgl_vendor: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Os { Windows, Macintosh, Linux, Android, Ios }
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Browser { Chrome, Safari, Edge }
```
---
## 4. User-Agent Construction
`ua.rs` owns UA string assembly. The full browser version is joined from
`browser_version` parts (e.g. `[124, 0, 6367, 78]` → `"124.0.6367.78"`).
### Supported combinations
| Windows | Chrome | `Mozilla/5.0 (Windows NT {nt}; Win64; x64) ... Chrome/{full} Safari/537.36` |
| Windows | Edge | Same as Chrome + `Edg/{full}` |
| macOS | Chrome | `Mozilla/5.0 (Macintosh; Intel Mac OS X {ver}) ... Chrome/{full} Safari/537.36` |
| macOS | Edge | Same as Chrome + `Edg/{full}` |
| macOS | Safari | `Mozilla/5.0 (Macintosh; ...) AppleWebKit/605.1.15 ... Version/{full} Safari/605.1.15` |
| Linux | Chrome | `Mozilla/5.0 (X11; Linux x86_64) ... Chrome/{full} Safari/537.36` |
| Android | Chrome | `Mozilla/5.0 (Linux; Android {ver}; {model}) ... Chrome/{full} Mobile Safari/537.36` |
| Android | Edge | Same as Android Chrome + `EdgA/{full}` |
| iOS | Safari | `Mozilla/5.0 ({platform}; CPU iPhone OS {ver} ...) ... Version/{full} Mobile/15E148 Safari/604.1` |
| iOS | Chrome | Same shell as iOS Safari but with `CriOS/{full}` instead of `Version/` |
| iOS | Edge | Same shell as iOS Safari with `Version/{full} EdgiOS/{full}` |
### iOS nuances
All iOS browsers use WebKit under the hood (Apple requirement). Chrome on iOS
uses `CriOS/` in the UA, Edge uses `EdgiOS/`. Client hints are **not** sent
for any iOS browser. The stealth script uses the Safari browser block for all
iOS browsers regardless of browser enum.
---
## 5. CDP Emulation Commands
Applied immediately after browser launch and before any navigation.
| `Emulation.setUserAgentOverride` | constructed UA, `navigator_platform`, client hints (Chrome/Edge, non-iOS) |
| `Network.setUserAgentOverride` | same UA + client hints (belt-and-suspenders) |
| `Emulation.setDeviceMetricsOverride` | `screen.*`, `density_pixel_ratio`, mobile flag, computed `screenWidth`/`screenHeight` |
| `Emulation.setTouchEmulationEnabled` | `has_touch` (explicit field, not derived from mobile) |
| `Emulation.setLocaleOverride` | `language[0]` |
| `Emulation.setTimezoneOverride` | resolved timezone (explicit or from ip-api.com) |
| `Emulation.setHardwareConcurrencyOverride` | `hardware_concurrency` |
| `Page.addScriptToEvaluateOnNewDocument` | full stealth bootstrap script |
### Client Hints
Generated for Chrome and Edge browsers on non-iOS platforms. Includes:
- **brands** / **fullVersionList**: Chromium + grease brand + browser brand
- Grease brand is `"Not-A.Brand"` on Android, `"Not;A=Brand"` on other platforms
- **platform**: Windows/macOS/Linux/Android
- **platformVersion**: from `platform.version`
- **architecture** / **bitness**: from platform info on desktop, empty strings on mobile
- **model**: from `device_model` (empty on desktop)
- **mobile**: derived from `device_model.is_some()`
### Screen metrics
`screenWidth` and `screenHeight` are calculated as
`logical_dimension × density_pixel_ratio` and passed to
`SetDeviceMetricsOverride`.
---
## 6. Stealth Bootstrap Script
Injected via `Page.addScriptToEvaluateOnNewDocument` (runs before any page JS).
Built by concatenating `property_modifier.js` + template-substituted `main_world.js`.
Values are **template-substituted directly into the JS source string in Rust**
before the script is sent to CDP, using `{{PLACEHOLDER}}` syntax and
`include_str!` + `.replace()`.
### 6.1 Navigator overrides
Uses `PropertyModifier.spoofProperty()` for stealthy property spoofing that
preserves original descriptors:
- `navigator.webdriver` → `undefined`
- `navigator.platform` → identity platform
- `navigator.hardwareConcurrency` → identity value
- `navigator.deviceMemory` → identity value
- `navigator.languages` → identity languages array
- `navigator.language` → `language[0]`
- `navigator.maxTouchPoints` → 5 if `has_touch`, else 0
### 6.2 Screen available size
- `screen.availWidth` → `logical_width`
- `screen.availHeight` → `logical_height - 40`
### 6.3 WebGL parameter spoofing
Overrides `getParameter` on `WebGLRenderingContext` and `WebGL2RenderingContext`:
- Param 37445 (`UNMASKED_VENDOR_WEBGL`) → `gpu.webgl_vendor`
- Param 37446 (`UNMASKED_RENDERER_WEBGL`) → `gpu.webgl_renderer`
### 6.4 History count
When `history_count` is `Some`, pushes states via `history.pushState` to reach
the target count. Omitted from script when `None`.
### 6.5 Battery status
`navigator.getBattery` returns a spoofed `BatteryManager`:
- `charging`: `true` if no battery or has mouse
- `chargingTime`: `0` if no battery, `Infinity` if battery
- `dischargingTime`: `Infinity` if no battery, `7200` if battery
- `level`: random value between 0.20 and 1.00 (generated at script build time)
### 6.6 Browser-specific overrides
Selected based on browser type (iOS always uses Safari block):
- **Chrome** (`browser_chrome.js`): fake plugins (PDF Plugin, PDF Viewer, Native Client)
- **Safari** (`browser_safari.js`): remove `chrome` object, spoof vendor to Apple, add `safari` object, delete `userAgentData`/`deviceMemory`
- **Edge** (`browser_edge.js`): empty plugins array
---
## 7. Timezone Resolution
Timezone is resolved in order:
1. Explicit value from `identity.timezone`
2. Fetched from `http://ip-api.com/json/?fields=timezone`, optionally routed through `identity.proxy`
The resolved IANA string is passed to `Emulation.setTimezoneOverride`.
---
## 8. Public API
```rust
/// Configuration for launching an identity-spoofed browser session.
pub struct IdentityConfig {
pub identity: Identity,
pub chrome: ChromeConfig,
}
impl From<Identity> for IdentityConfig { ... }
/// A rustenium browser session with an identity applied.
pub struct IdentitySession { ... }
impl IdentitySession {
/// Launch a new Chromium instance. Accepts Identity or IdentityConfig.
pub async fn launch(config: impl Into<IdentityConfig>) -> Result<Self, IdentityError>;
/// Access the underlying rustenium ChromeBrowser.
pub fn browser(&self) -> &ChromeBrowser;
pub fn browser_mut(&mut self) -> &mut ChromeBrowser;
/// Get the identity.
pub fn identity(&self) -> &Identity;
/// Construct the User-Agent string without launching.
pub fn user_agent(identity: &Identity) -> Result<String, IdentityError>;
/// Navigate to a URL via CDP.
pub async fn navigate(&mut self, url: &str) -> Result<(), IdentityError>;
/// Push updated GPU/WebGL parameters to the running page via Runtime.evaluate.
pub async fn update_gpu(&mut self, gpu: &Gpu) -> Result<(), IdentityError>;
}
```
`IdentitySession::launch` accepts `impl Into<IdentityConfig>`, so both
`Identity` and `IdentityConfig` can be passed directly. When an `Identity` is
passed, a default `ChromeConfig` with CDP enabled and BiDi disabled is used.
If `identity.proxy` is set, a `--proxy-server` flag is added to the Chrome
launch flags automatically.
---
## 9. Runtime GPU Update
`update_gpu()` uses `js/update_gpu.js` loaded via `include_str!` with
`{{WEBGL_VENDOR}}` and `{{WEBGL_RENDERER}}` placeholders. Executed via
`Runtime.evaluate` CDP command (not BiDi).
---
## 10. Error Handling
```rust
#[derive(Debug, thiserror::Error)]
pub enum IdentityError {
#[error("required identity field missing: {0}")]
MissingField(&'static str),
#[error("CDP command failed: {0}")]
CdpError(String),
#[error("UA construction failed: {0}")]
UaError(String),
#[error("timezone resolution failed for language={0}")]
TimezoneError(String),
#[error("script injection failed: {0}")]
InjectionError(String),
#[error("gpu update dispatch failed: {0}")]
GpuUpdateError(String),
}
```
---
## 11. Module Layout
```
rustenium-identity/
├── src/
│ ├── lib.rs # Public API: IdentityConfig, IdentitySession
│ ├── identity.rs # Identity, Gpu, PlatformInfo structs + enums
│ ├── ua.rs # User-agent construction
│ ├── cdp.rs # CDP command application + client hints
│ ├── script/
│ │ └── mod.rs # Script builder: Identity → JS string
│ ├── tz.rs # Timezone resolution (explicit or ip-api.com)
│ └── error.rs # IdentityError enum
├── js/
│ ├── property_modifier.js # PropertyModifier utility (stealthy property spoofing)
│ ├── main_world.js # Main stealth script with {{PLACEHOLDER}} templates
│ ├── browser_chrome.js # Chrome-specific: fake plugins
│ ├── browser_safari.js # Safari-specific: remove chrome obj, spoof vendor
│ ├── browser_edge.js # Edge-specific: empty plugins
│ └── update_gpu.js # Runtime GPU parameter update
├── Cargo.toml
└── rustenium_identity_spec.md
```
---
## 12. Identity Deserialization
```rust
impl Identity {
pub fn from_json(s: &str) -> Result<Self, serde_json::Error>;
#[cfg(feature = "bson")]
pub fn from_bson(doc: bson::Document) -> Result<Self, bson::de::Error>;
}
```
---
## 13. Dependencies
```toml
[dependencies]
rustenium = { path = "../rustenium/rustenium" }
rustenium-cdp-definitions = { path = "../rustenium/rustenium-cdp-definitions" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["time"] }
thiserror = "2"
rand = "0.9"
reqwest = { version = "0.12", features = ["json", "socks"] }
[features]
default = []
bson = ["dep:bson"]
[dependencies.bson]
version = "2"
optional = true
```
---
## 14. Integration Example
```rust
use rustenium_identity::{Identity, IdentitySession};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let raw = std::fs::read_to_string("identity.json")?;
let identity = Identity::from_json(&raw)?;
// Launch with default ChromeConfig (Identity converts into IdentityConfig)
let mut session = IdentitySession::launch(identity).await?;
session.navigate("https://example.com").await?;
// Runtime GPU update if needed
let new_gpu = rustenium_identity::Gpu {
vendor: "NVIDIA".into(),
webgl_renderer: "ANGLE (NVIDIA, GeForce RTX 4090)".into(),
webgl_vendor: "Google Inc. (NVIDIA)".into(),
};
session.update_gpu(&new_gpu).await?;
Ok(())
}
```