Skip to content

How it works

Mert Cobanov edited this page Jul 29, 2026 · 2 revisions

How it works

The design, and the reasoning behind the parts that look strange.

No RIM APIs

BlackBerry code signing was a runtime check inside the device VM: it fired when a module touched a protected API, and every signature had to be issued by BlackBerry's signing authority, which no longer exists. Applications that need a signature can no longer be built by anyone.

Code that touches no protected API needs no signature. So berryssh uses only standard MIDP 2.0 and CLDC 1.1, and the build needs no BlackBerry tooling at all — the API stubs come from microemu, off Maven Central.

The cost is real: this rules out BlackBerry's own UI framework. The screens are MIDP, drawn by the device in its own theme, and they are not — and cannot be — the native widgets.

Cryptography from scratch, and why the modern algorithms are easier

CLDC 1.1 has no java.security, no javax.crypto, and no BigInteger. Every primitive is implemented in the project.

That sounds worse than it is, because the algorithm choices make it tractable:

Why it suits this hardware
curve25519-sha256 Fixed 32-byte field arithmetic, no BigInteger
ssh-ed25519 Same curve arithmetic, nothing extra to carry
chacha20-poly1305 32-bit add/xor/rotate, no lookup tables; AEAD, so no separate MAC
SHA-256 Small and self-contained

The classic 2011 SSH stack is anchored on 2048-bit modular exponentiation, which is genuinely slow in interpreted Java on this class of CPU. Modern cryptography is the cheaper option here, not the more expensive one — and it means the handset reaches a current OpenSSH without that server being weakened to meet it.

The layer that makes everything testable

Connection takes an InputStream and an OutputStream. It has never known where they came from.

That one decision is why the protocol code — transport, key exchange, authentication, channels — is free of MIDP entirely, and can therefore run on a desktop JVM against a real OpenSSH server. Encodings can be proved with vectors; a protocol cannot. A key exchange either convinces OpenSSH or it does not.

It is also why the WebSocket cost almost nothing: a WebSocket-backed stream pair substitutes with no change above it at all.

Verification

Two halves, neither needing the device:

  • Compiling at -source 1.3 against the CLDC bootclasspath proves the code will run on the handset.
  • Executing on the host JVM proves it is correct.

228 offline vectors: FIPS 180-4, RFC 8439, RFC 7748, RFC 8032, RFC 4231, RFC 4251 §5, RFC 4253 §6 and §7, RFC 8731 §3, plus the terminal, the character mapping, and every path that has to reject malformed input.

Then 39 more against a real server, covering what vectors cannot: a negotiated key exchange, an authenticated session, a pty, a shell, a rekey, typing while output arrives, and a bridge refusing a wrong key.

Everything runs under a Turkish default locale, deliberately. "I".toLowerCase() is a dotless ı there, and protocol code that case-folds anything is broken in a way that raises no error. Running the suite in the condition that exposes it beats running it in the one that hides it.

Random numbers

CLDC has no SecureRandom, and java.util.Random is a 48-bit LCG seeded from the clock — useless for key material. EntropyPool harvests timing jitter into a SHA-256 based DRBG, refuses to produce anything until it holds 256 bits, and ratchets its state after every output so recovering it afterwards reveals nothing about what came before.

java.util.Random in CLDC also has no nextBytes. That kind of thing is why the build compiles against the real CLDC stubs rather than trusting a memory of what the API contains.

The terminal

VT320 is ported from BBSSH, which solved this well and is credited in the file. The rendering is not: MIDP exposes three font sizes and the smallest gives a 21×13 terminal on a 480×360 screen. berryssh renders from a bitmap font atlas instead, which is what makes 60×25 possible.

The atlas covers ASCII, Latin-1, the Turkish letters, box drawing and block elements. A character with no cell draws a hollow box rather than nothing, so a missing glyph looks like a missing glyph instead of a rendering bug.

Threads

Two, and they share a transport. A reader parked on a socket read must not block a keystroke from being sent, so the transport has separate locks per direction — one lock would have meant every keypress waiting on the network.

Channel writes wait on a window monitor rather than pumping the socket themselves; a second reader on the same socket corrupts the stream, and it does so intermittently enough to look like a network problem.

Storage

Saved connections and host keys live in RMS, the device's record store. Both formats are versioned and every older layout is still read: a connection list that empties itself on upgrade is worse than whatever feature caused it.

Everything is UTF-8 on the way in and out. The device's default encoding is ISO8859_1, which would quietly mangle a hostname or a password containing a Turkish letter.

Clone this wiki locally