Nokia 3310 上的 Claude:J2ME 客戶端以 HMAC 取代 TLS,並附 41 款迷你遊戲
Claude on a Nokia 3310: a J2ME client, HMAC instead of TLS, and 41 bundled games
VibeNokia 為 2017 年 Nokia 3310 製作 Java ME 客戶端,讓功能手機連接 FastAPI 伺服器與 Claude 聊天,並提供 Telegram、語音訊息及 41 款迷你遊戲。客戶端使用未加密的 HTTP,專案以 HMAC-SHA256 簽署請求並防止重播,但簽署無法防止對話被竊聽。
I wrote VibeNokia: a Java ME client for the 2017 Nokia 3310 feature phone that turns it into a chat terminal for Claude, plus a small FastAPI server that stores history, verifies signatures and runs claude -p. The menu also has Telegram, voice messages and a catalogue of 41 mini games. It works on my physical phone, and along the way I had to re-solve several things that a smartphone gets from a one-line library call.
A caveat up front: the client has been tested on one real Nokia 3310 (TA-1030). I haven't tried other Series 30+ or Series 40 phones. The channel between the phone and the server is not encrypted; more on that below, without softening it.
The platform
The 2017 Nokia 3310 is Series 30+ on a MediaTek MT6260. It runs Java ME with CLDC 1.1 / MIDP 2.0, has a 240x320 screen, mobile data only and no Wi-Fi. It has no usable browser and no app store. A plain HttpConnection with POST and JSON does work (I've seen this on my device), and the battery lasts a week.
So the design is thin: the phone runs a client that draws the screen and sends requests, and my server does all the heavy work. The app installs as a .jad/.jar pair from the memory card, or over the air: the server itself serves the files at /ota/VibeNokia.jad.
docs/platform-notes.md collects observations I wish I'd had before starting. The ones that cost me time:
- CLDC has no cryptography at all. SHA-256 and HMAC in
Crypto.javaare written by hand. - The standard
ListandFormdon't deliverkeyRepeated, i.e. a held key. Because of that the whole UI is a customCanvas, with home-screen tiles, scrolling and input. - There is no timeout API on the HTTP connection, and the system timeout is measured in minutes. The client runs its own watchdog threads and aborts hung requests itself.
- Server responses are cut at roughly 25 KB. The screen shows far less anyway.
- Voice is recorded through JSR-135 as AMR, about 1.5 KB per second; the server converts and transcribes it.
Why not HTTPS
This is where I hit a wall. According to public sources, the Java stack in these phones stops at old TLS versions and an old certificate store, so modern HTTPS URLs don't open. In my notes this is marked "reported": I did not deliberately re-verify it on my own device. The outcome for the project is the same either way: the client uses plain HTTP.
That means you can't rely on channel secrecy, so the app is built not to depend on it. Hence signed requests.
Signing instead of TLS
The phone is paired with the server once and receives its own key. After that the key never crosses the network. Every request carries an HMAC-SHA256 computed over:
- the method and path;
- a request counter;
- a hash of the body;
- the server address.
The server checks the signature and the counter. A replayed request is dropped: a window of the 64 most recent counters works like the sliding window in IPsec. The signature includes the server address (VIBENOKIA_PUBLIC_HOST), so a request signed for a different address is rejected. Repeated wrong signatures or codes get the source banned for 15 minutes.
Pairing works like this. Set VIBENOKIA_PAIRING=1 in .env. Press 5 on the phone, and the server writes a one-time six-digit code to data/pair_code.txt (mode 600); if a Telegram token and my chat are configured, the code is also sent there. The code lives for 10 minutes and works once. You type it on the phone, the device gets its key and stores it. Then you set VIBENOKIA_PAIRING=0 and the /pair/* paths answer 404. A phone can be revoked locally with tools/devices.py against the database file; there is deliberately no network admin API.
What the signature does not give you
Signing protects against forgery, guessing and replay. It does not protect against eavesdropping at all. Anyone on the path between the phone and the server can read your questions and Claude's answers. Don't send anything secret from this phone. I say this in the README and in the deployment docs and repeat it here, because it's worse when people discover such things themselves.
Server and security
The server is Python with FastAPI and SQLite: sessions, devices and contacts live in one file. To answer, it runs claude -p with a short system prompt written for a tiny screen. What matters:
- By default the server listens only on
127.0.0.1:8085, and Docker publishes the port on loopback only. To expose it, do it deliberately through a proxy that forwards only the phone's paths (/ping,/api/*,/pair/*,/device/*,/ota/*) and answers 404 for everything else. The docs have Caddy and nginx examples. - No default passwords. The admin key is generated on first run.
- Claude has no shell by default: it runs with
--permission-mode dontAskand no pre-approved tools. If you allow tools viaVIBENOKIA_CLAUDE_ARGS, a paired phone gets access to them. Treat such a phone like an SSH key and keep the allow-list narrow. The server removes its own secrets from Claude's environment. - The second engine (OpenCode) is off by default, because its agents may run tools according to your own configuration.
-
VIBENOKIA_FEATURESsets a ceiling on features: if you don't need Telegram or voice, their API paths answer 404 for every phone.
Before opening the port, the docs have a short check: ss -ltn | grep -v 127.0.0.1 should show only the proxy port, and curl against /ping and /api/chat without a signature should return 403 and never 200.
The menu: two tiles instead of a cockpit
The first version opened with every feature at once. In the second (v0.2.0) the app starts with a simple menu of two tiles: chat with Claude, and Settings. Everything else (Telegram, voice, sessions, a second tile for OpenCode, and the "advanced" group: server list, in-app update, key rotation) is switched on in Settings > Features. The choice is kept in the phone's RMS storage. The server-side ceiling is applied on top: a feature the server forbids is hidden in the app, and the list arrives in the /ping reply. Upgrading from an older version keeps everything that was visible before, while a fresh install starts simple.
Always available: chat, model choice (Opus, Sonnet or Haiku), new chat, language (Russian or English) and font size. Tiles are numbered in the order shown, the digit opens a tile, and 5 opens the selected one. Hold * to dictate: the recording goes to the server, is transcribed there (Gemini via OpenRouter, Groq Whisper as a fallback) and lands in the chat.
The screenshots are from MicroEmulator at 240x320.
41 games
The catalogue has 41 standalone apps with no permissions and no network. Nine were written in this repository; the other 32 are MIT-licensed games from the agneay/j2me-100-games archive. Third-party components are listed in THIRD_PARTY_NOTICES.md. One script, scripts/build-games.sh, builds them all.
Running it
You need Docker with Compose, make, and either a Claude API key or a Claude Code login:
git clone https://github.com/meshoklv/vibenokia && cd vibenokia
./scripts/quickstart.sh # creates .env with a fresh key and starts the server on 127.0.0.1:8085
make -C client SERVER_URL=http://your.host:8080 # -> client/dist/VibeNokia.jar + VibeNokia.jad
./scripts/build-games.sh # optional -> games/dist/
The client builds inside a public Docker image with a J2ME toolchain. Install VibeNokia.jad from the memory card, then pair with a code as described above. Without Docker the server runs with scripts/run-local.sh; you then need Python 3.10+, ffmpeg for voice and claude on PATH.
What is and isn't tested
Tested: the client on a real Nokia 3310. For this release the client and all 41 games build in the J2ME container, and I drove the new menu and Features screen by hand in MicroEmulator (the screenshots come from there). The server image builds and answers 403 on loopback without auth, and pairing plus signed chat were exercised end to end with the phone emulator and a stub in place of claude.
Not tested: a real Claude call inside the published Docker image, other Series 30+ and 40 phones, and a TLS front together with the phone. Planned: prebuilt .jar files in releases and per-device tool allow-lists.
About the AI assistant
Part of the code was written together with an AI assistant (Claude); the commit history shows this through a Co-Authored-By line.
Links
Repository: github.com/meshoklv/vibenokia, MIT license. Issues and PRs are open; the rules are in CONTRIBUTING.md. If you have another J2ME phone and try it, tell me what happened. I post notes on Telegram at t.me/meshoklv.
Mike Meshok - github.com/meshoklv
來源:DEV Community · dev.to



