Skip to content

Latest commit

 

History

History
198 lines (172 loc) · 10.3 KB

File metadata and controls

198 lines (172 loc) · 10.3 KB

Engineering notes

Status — May 2026

Working end-to-end against Juggluco's reference vectors on the M1 Mac mini AVD.

Op Function Result
1 reset
2 initECDH ✅ rc=1
4 setPatchCertificate ✅ rc=1
5 generateAppEphemeralPublicKey ✅ 64-B output (SKB returns X‖Y; the iOS adapter prepends 0x04 before BLE)
6 deriveKAuthFromPatchEphemeral ✅ rc=1
7 challengeEncrypt ✅ 40-B ciphertext
8 challengeDecrypt needs a real-session ciphertext; rc=0 against Juggluco's static vector (expected — vectors were captured under a different session key)
9 exportKAuth 149 bytes, exactly Juggluco's reference size

/health reports loaderReady: true after app start.

What made it work

Three things, in order of importance:

  1. Use Juggluco's liblibre3extension.so, not the one from Abbott's Libre 3 APK. They're different binary builds; Juggluco's loader (and this port of it) is calibrated to the .so version they ship. Extract from Juggluco-X.Y.Z-arm64.apklib/arm64-v8a/.

  2. Don't use System.loadLibrary. A small NDK module (app/src/main/cpp/loader.cpp, ~200 lines) does the load manually:

    • dlopen(soPath, RTLD_NOW | RTLD_LOCAL)
    • dlsym("JNI_OnLoad")
    • Patches a 4-byte BL instruction at JNI_OnLoad + 0x2F20 from 04 2E 05 94 to E0 03 00 AA (MOV X0, X0, effective NOP). Verbatim from Juggluco's loadlibs.cpp.
    • Invokes JNI_OnLoad with a fake JavaVM / JNIEnv whose only methods are FindClass (returns the class-name string cast to jclass) and RegisterNatives (captures process1 / process2 function pointers into globals).
    • Subsequent calls go through those captured C function pointers with the real JNIEnv of the calling thread — byte-array marshaling works, but ART's RegisterNatives table is never touched.
  3. Disable jniLibs uncompressed packaging. AGP's default (useLegacyPackaging = false) keeps native libs inside the APK as uncompressed pages, where dlopen by absolute path doesn't reach them. Setting useLegacyPackaging = true in app/build.gradle.kts makes Android extract them to applicationInfo.nativeLibraryDir at install time — the path the loader uses.

Architecture

┌──────────────────────────────────────────────────────────────────────────┐
│ iOS sample app (stock iPhone)                                            │
│  - Core NFC takeover (0xA8) + CoreBluetooth                              │
│  - HTTP client (AndroidServerClient.swift) → this shim's port 8080       │
└──────────────────────────────────────────────────────────────────────────┘
            │ JSON over HTTP (LAN / adb forward / Tailscale)
            ▼
┌──────────────────────────────────────────────────────────────────────────┐
│ dev.libre3shim  (Android — AVD on M1 Mac mini, or rooted phone)          │
│                                                                          │
│  MainActivity ─ status screen                                            │
│  ShimService  ─ foreground service, wakelock, on-boot init               │
│                                                                          │
│  HttpServer.kt  (NanoHTTPD on 0.0.0.0:8080)                              │
│       ├─ GET    /health                                                  │
│       ├─ POST   /session                       → {sessionId}             │
│       ├─ POST   /session/{id}/process1         {op,a,b}  → {rc}          │
│       ├─ POST   /session/{id}/process2         {op,a,b}  → {rc,out}      │
│       └─ DELETE /session/{id}                                            │
│                       │                                                  │
│                       ▼                                                  │
│  SkbBridge.kt  ─ ReentrantLock + session bookkeeping; calls NativeLoader │
│                       │                                                  │
│                       ▼                                                  │
│  NativeLoader.kt (System.loadLibrary "libre3loader")                     │
│                       │ JNI                                              │
│                       ▼                                                  │
│  libre3loader.so  (the NDK module here, ~200 lines)                      │
│       - dlopen + dlsym + 4-byte patch                                    │
│       - fake JavaVM/JNIEnv for capturing process1/process2               │
│       - calls captured fn pointers with real JNIEnv                      │
│                       │                                                  │
│                       ▼                                                  │
│  liblibre3extension.so  (Juggluco-shipped — ~3.94 MB, NOT in repo)       │
│       - process1 / process2 opcodes 1-9                                  │
└──────────────────────────────────────────────────────────────────────────┘

File map

android-server/
├── build.gradle.kts                 root project — AGP 9.2.1
├── settings.gradle.kts              foojay-resolver 1.0.0 for JDK 17 toolchain
├── gradle.properties
├── gradlew + gradle/wrapper/        Gradle 9.5 wrapper
└── app/
    ├── build.gradle.kts             NDK + CMake + useLegacyPackaging
    └── src/main/
        ├── AndroidManifest.xml
        ├── cpp/
        │   ├── loader.cpp           Juggluco-derived loader
        │   └── CMakeLists.txt       builds libre3loader.so
        ├── jniLibs/arm64-v8a/       (gitignored — sourced from Juggluco APK)
        │   ├── liblibre3extension.so
        │   ├── libcrl_dp.so
        │   ├── libinit.so
        │   └── …
        ├── java/dev/libre3shim/
        │   ├── MainActivity.kt    status screen, starts the service
        │   ├── ShimService.kt     foreground service, calls SkbBridge.prepare()
        │   ├── HttpServer.kt      NanoHTTPD routes
        │   ├── SkbBridge.kt       mutex + session bookkeeping
        │   └── NativeLoader.kt    JNI surface for cpp/loader.cpp
        └── res/                     strings.xml, network_security_config.xml

HTTP API

Verb Path Body Returns
GET /health { "ok": <bool>, "version": "0.1", "port": 8080, "device": "…", "loaderReady": <bool>, "loaderError": <string|null>, "activeSession": <uuid|null> }
POST /session { "sessionId": "<uuid>" }
POST /session/{id}/process1 { "op": <int>, "a": "<b64>|null", "b": "<b64>|null" } { "rc": <int> }
POST /session/{id}/process2 same { "rc": 1, "out": "<b64>" }
DELETE /session/{id} 204

Opcodes (matching Juggluco's Libre3SKBCryptoLib JNI surface):

Op Method a b Out
1 process1 null null rc=1 (reset)
2 process1 appPrivKey blob (165 B) cached kAuth or null rc=1
4 process1 patchCert (140 B) null rc=1
5 process2 null null 64-B X‖Y (P-256 ephemeral; iOS adapter prepends 04)
6 process1 patchEphemeral (65 B with 04 prefix) null rc=1
7 process2 nonce1 (7 B) plaintext (36 B) 40-B ciphertext
8 process2 nonce (7 B) ct+tag (60 B) 56-B plaintext (r2‖r1‖kEnc‖ivEnc)
9 process2 null null ~149-B exported kAuth

Emulator setup (M1 Mac mini)

brew install --cask temurin
brew install --cask android-commandlinetools
export ANDROID_HOME="/opt/homebrew/share/android-commandlinetools"
export PATH="$ANDROID_HOME/emulator:$ANDROID_HOME/platform-tools:$PATH"

yes | sdkmanager --licenses
sdkmanager "platform-tools" "emulator" \
           "system-images;android-34;google_apis;arm64-v8a" \
           "platforms;android-34"
avdmanager create avd -n libre3srv \
  -k "system-images;android-34;google_apis;arm64-v8a" \
  -d pixel_6

emulator -avd libre3srv -no-window -no-audio -no-boot-anim &
adb wait-for-device

LAN bridge for the iPhone

brew install socat
# Replace 192.168.1.42 with the Mac mini's LAN IP.
nohup socat TCP-LISTEN:8080,bind=192.168.1.42,fork,reuseaddr \
            TCP:127.0.0.1:8080 \
            > /tmp/socat-libre3.log 2>&1 &
curl -sS http://192.168.1.42:8080/health

Outstanding details

These don't block the handshake from progressing, but tighten the loop:

  • 04 prefix on op 5 / op 6. The SKB returns 64 bytes (X‖Y); downstream code needs the 65-byte uncompressed form (04‖X‖Y). The iOS adapter prepends the byte before sending over BLE and includes it in op 6 input. Handled in iOS-side Swift code.
  • Op 8 against Juggluco's static vector returns rc=0. Expected: the reference bytes60 was AES-CCM-encrypted under a specific session key captured during Juggluco's test, not the one this shim derives. Real-sensor traffic produces a matching ciphertext.
  • NFC libcrl_dp.so loaded but not yet wired. The iOS app builds its own takeover payload (time ‖ accountId ‖ CRC16, no SKB involvement). This path isn't needed for the BLE handshake; it would matter only for first-time activation, which the iOS app delegates to Abbott's official app.
  • Session reuse across BLE reconnects. Each new /session open currently does op 1 (reset) and re-runs the full handshake. Once op 9 returns a real kAuth blob, the iOS app can persist it and on reconnect pass it as b to op 2 (cached path) — server-side this Just Works because the SKB accepts a cached kAuth.