Skip to main content

Profiles

Verification boundary

This page documents the committed source. Treat revision-sensitive RuneLite UI, packet, or in-game outcomes as pending live-client verification unless the page records direct evidence.

Profiles is a Packet Utils tab in the shared n3 sidebar. The tab embeds a filtered RuneLite ConfigPanel above the profile list. It renders Auto Login and the secret Unlock Password field from the existing n3profiles group. It hides encrypted payload and salt entries. Packet Utils refreshes this panel with the other shared tabs.

Architecture

  • PacketUtilsPlugin - lifecycle and shared sidebar ownership.
  • N3ProfilesPanel - login-screen profile list, add/edit/delete, unlock, paste-based bulk import, saved-proxy management/testing, and Jagex OAuth trigger.
  • N3ProfilesConfig - config group n3profiles (autoLog, unlockPassword, profilesData, salt).
  • ProfileStore - shared encrypted store (see below).
  • StoredProfile - one typed credential line plus proxy/world preferences.
  • ProfileStorageCodec - AES-GCM v3 encoding, v2 decoding, and legacy migration.
  • ProxyStore / SavedProxy - separately salted AES-GCM saved-proxy library unlocked by the same profiles password.
  • ProfileLoginService - applies a profile to the client (legacy fields or Jagex reflection).
  • JagexLoginAdapter / ReflectionJagexLoginAdapter / UnsupportedJagexLoginAdapter - revision-sensitive obfuscated-name login hooks behind a testable boundary.
  • jagexauth/ - guarded system-browser OAuth plus local HttpServer: JagexAuth, AuthFlow, CallbackHandler, OAuthReceiver, JsonParser.

Typed credential store

ProfileStore (@Inject ConfigManager) reads and writes the n3profiles group (profilesData + salt). It derives an AES key from the unlock password via PBKDF2WithHmacSHA256 (100k iterations, 128-bit) wrapped as an AES SecretKeySpec. Because its only dependency is ConfigManager (present in every plugin injector), other plugins like the Break Handler can inject their own instance and read the same blob without a cross-plugin singleton.

StoredProfile lines are typed:

LEGACY:label:login:password:pin:proxyLabel:preferredWorld:worldType
JAGEX:displayName:characterId:sessionId:userHash:proxyLabel:preferredWorld:worldType

Pre-existing untyped lines (label:login:password[:pin]) decode as LEGACY for backward compatibility. ProfileStorageCodec re-encodes them on first load (isMigrationRequired).

API

  • load(char[] password) - decrypt + migrate + parse to List<StoredProfile> (throws on bad password).
  • save(List<StoredProfile>, char[] password) - re-encrypt and persist.
  • loadWithConfiguredPassword() - decrypt with n3profiles.unlockPassword; empty list on absence/failure, never throws.
  • hasConfiguredPassword() - whether an auto-unlock password is set.

StoredProfile and its Kind/getters are public so cross-package consumers can read profiles. The factories/parse/format stay package-private to n3Profiles.

Bulk import

Unlocked Profiles panels expose Bulk Import for multiline paste input. It accepts only typed rows:

LEGACY:label:login:password[:pin][:proxyLabel][:preferredWorld][:worldType]
JAGEX:label:characterId:sessionId[:userHash][:proxyLabel][:preferredWorld][:worldType]

The importer ignores blank lines. Blank labels default to the legacy login or Jagex character ID. Legacy rows require login and password; the optional PIN must be blank or exactly four digits. Jagex rows require character ID and session ID; user hash remains optional. Preferred worlds must be non-negative integers and world types must be MEMBERS, F2P, PVP, or blank. The importer skips malformed rows and duplicate labels, including duplicates already stored, and reports them in the import summary.

Saved proxies and world selection

The unlocked Profiles tab can add, edit, delete, and test uniquely labelled SOCKS5 proxies. Proxy entries use the n3proxies config group and a separate random salt, but share the active profiles unlock password. Deleting a proxy also clears profile references to its label. Tests perform an actual SOCKS5 negotiation, optional username/password authentication, and a tunneled connection request on a background executor.

Applying a profile immediately replaces global proxy routing when that profile names a saved proxy. A preferred world is selected on the login screen before credentials are submitted. With no explicit world, a non-empty world-type preference selects the first matching RuneLite world-list entry; an explicit world that conflicts with the configured type is rejected.

Login

ProfileLoginService:

  • applyLegacyProfile(username, password, autoLogin) - sets client username/password and dispatches Enter keys when autoLogin is true on the login screen. It does not touch reflected Jagex login state: a client already on the legacy login form is applied to as-is.
  • resetToLegacyLogin() - explicitly reverts a Jagex account login screen back to the legacy username/password form by clearing reflected Jagex state, blanking pending credentials, and cancelling any pending login trigger. Used by the agent dashboard's clear action.
  • applyJagexProfile(sessionId, characterId, displayName) - applies a Jagex session via the reflection adapter, keeps login index 10, and attempts the shared welcome-screen continue helper when available (throws ReflectiveOperationException if hooks unavailable).
  • isJagexLoginAvailable() - adapter support probe.

The agentic test runner uses n3.test.unlockPassword when that JVM property is present; otherwise, it uses the configured unlock value. It calls load(char[]) directly and clears the temporary character array. The plugin never persists, logs, serializes, or screenshots the supplied password.

Profiles session, character, display-name, and account-mode fields come from the revision mapping evidence shared with Packet Utils. The adapter validates those cached field shapes against the injected class loader before use. The login index is not a cached vanilla method call: ASM inspects the active client's public getLoginIndex(): int hook, requires one static-int GETSTATIC, decoder, IMUL, IRETURN sequence, and derives the 32-bit encoder as the decoder's modular inverse.

The adapter's legacy transition clears the Jagex fields and selects logical login index 2. It is only invoked by resetToLegacyLogin(); applying a legacy profile does not trigger a transition, so a client already in Legacy mode is never reset (an unnecessary reset caused failed-to-login errors). Jagex transitions set the session fields and account discriminator, select index 10, and clear legacy credentials. A failed transition restores every reflected field and the prior logical login index. Missing, ambiguous, instance, non-integer, or non-invertible login-index hooks make the adapter unsupported; the plugin invokes no similarly shaped vanilla login-screen method.

Testing

.\gradlew.bat test --tests com.n3plugins.n3Profiles.* --console plain
  • ProfileStoreTest - encode/decode round-trip, configured-password load, salt creation (map-backed ConfigManager mock).
  • StoredProfileTest - typed/untyped parse, format, parseAll.
  • BulkProfileImportTest - strict typed paste rows, optional fields, invalid rows, duplicate skipping, and partial summaries.
  • ProfileStorageCodecTest - AES-GCM v3 round-trip, v2 compatibility, and legacy ¬ migration.
  • ReflectionJagexLoginAdapterTest - field resolution, both transition directions, rollback, and unsupported-hook behavior.
  • LoginIndexAsmResolverTest - deterministic hook discovery, encoded reads/writes, ambiguity and unsafe-shape rejection, and absence of unrelated method calls.
  • jagexauth/JsonParserTest - OAuth token JSON parsing.

Live validation (pending)

You still need rebuilt-client acceptance for the Jagex OAuth round-trip, browser-guard notifications, proxy routing and overlay health changes, preferred-world selection, legacy credential population and auto-login, and repeated Jagex → legacy → Jagex → legacy transitions. Static tests do not prove those live postconditions.