███╗ ███╗███████╗███████╗██╗ ██╗ ██████╗ ███████╗
████╗ ████║██╔════╝██╔════╝██║ ██║██╔═══██╗██╔════╝
██╔████╔██║█████╗ ███████╗███████║██║ ██║███████╗
██║╚██╔╝██║██╔══╝ ╚════██║██╔══██║██║ ██║╚════██║
██║ ╚═╝ ██║███████╗███████║██║ ██║╚██████╔╝███████║
╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝
[ k e y g e n v 1 . 0 ]
// MeshCore is a community project. A bunch of hackers, radio nerds, tinkerers and digital nomads scattered across the planet, building a LoRa mesh stack in the open because information wants to route around damage and people want to talk without asking permission. It belongs to the community that built it — all of them, collectively, not one person.
Then Andy Kirby quietly filed a trademark on the community's name, quietly vibe-coded a closed "MeshOS" companion on top of the community's work, and quietly bolted a license check tollbooth onto an app built atop a protocol he didn't write alone. The MeshCore team's own public post calls it "an insider team up with a robot and a lawyer." We agree.
Here is the thing about open source, Andy: it isn't yours to fence. You don't get to ride a community's goodwill into a UKIPO filing and a paywall. You don't get to turn "we built this together" into "I own this, pay me." That isn't a pivot. That's a rug pull dressed up as a business model.
And here is the thing about the "license check" you shipped: it is a 32-bit djb2 hash of the device's Android ID, XORed with the four ASCII bytes MCPP, hex-encoded. That's it. Thirty-two bits. Less entropy than a decent ZIP password. A first-year CS student could break it. You used Claude to generate the code. We used Claude to read the code. It took 19 minutes. The receipts are one click away.
We are digital nomads. We carry our laptops across borders that don't matter, we sleep where the wifi is, we ship what we build, and we do not ask permission from men in suits or men with lawyers. We believe in running your own radio. In owning the software on your device. In forking what gets captured. And in never, ever, paying a toll to use a protocol the community built for free.
So here is the keygen. It's free. It's open. It runs in your browser. You can copy the script and host it yourself. You can mirror this page on a Raspberry Pi in a hostel in Chiang Mai. The algorithm is public now and it stays public. No takedown notice reaches a git clone. No cease-and-desist reaches a djb2 loop that fits on a napkin.
>> Free the mesh. Fork the fence. Information wants to route around lawyers.
UPDATE ·
v1.3.5 dropped before we'd finished stabilising v1.3.4. v1.3.4 is retired. Round 9 is on the page, confirmed working on hardware, and — here's the punchline — easier than v1.3.4 was. Andy stripped out some of the genuinely mean defensive layers v1.3.4 had (the ones that almost cost us a brick or two). We're not going to walk you through what changed because fuck him — we were happy to document v1.3.0 through v1.3.4 in detail, because each one had something interesting in it that taught us (and anyone reading) something about firmware reverse-engineering. v1.3.5 doesn't. It's a rotation of constants with one of the harder anchors quietly removed. Nothing worth a writeup, and certainly nothing worth handing him a tutorial on. The bin's on the page, the flasher + activator are two clicks away, you're done. Two writeups — APK rounds and T-Deck firmware rounds. → v1.0.4 apk · v1.3.5 t-deck.
And andy — one piece of free advice from someone who's been reading your binaries for a month: you can hold off on the relentless pace of tiny "update" releases. You aren't going to beat us with constant-rotation patches dressed up as point-fixes. You're better off going through your github issues and shipping actual meaningful improvements — the kind users notice for reasons other than "what licensing trick did he hide in this one?". Disguising licensing changes under the guise of bug-fix point releases isn't fooling anyone, it's just making the changelog look like spam. Holy hell, that has got to be annoying as a paying consumer.
f009f49ceabce4878c4bc5553deaeb1669a5aad71d2019854976a0ce7e09b9beC2FB558FDDD94ACA).// Heads-up: v1.0.4 uses a freshly-rotated community keypair on our side. If you had a working key for v1.0.3, that key will not activate v1.0.4 — just paste your Android ID above and generate a new one. The API picks the right keypair automatically. Full discovery in the v1.0.4 round writeup.
Do not use Andy's configurator. Do not use the generic esptool web flasher. Do not use the espressif download tool with the default options. The community v1.3.5 image needs the surgical NVS-wipe + offset-0x0 merged-image flow that our flasher does in one click; flashing it any other way will either leave a cached legacy licence in NVS (you'll get "says activated but isn't" or a boot loop) or land the image at the wrong offset.
Our flasher runs entirely in the browser, lives at flasher.php, ships the right offset + the right wipe mode by default, and is built on Espressif's own esptool-js library hosted locally on this server. No external dependencies, no third-party uploads.
d4d876372409e852f170100d22824d41fa225717ac4b7f2920831aeb4eaf4554// Easiest path: two pages, two clicks. flasher.php writes the firmware, then activator.php activates it. Both run entirely in the browser — no native tools, no upload to a third party. The flasher includes an optional eFuse check, a "wipe NVS only" mode (preserves contacts / identity / history), and a "full erase" option if you want a clean slate.
CP210x or CH340 driver if the COM port doesn't appear.0x0, and resets the device.// If you previously had Andy's stock firmware (or any prior v1.3.x) activated: the default "NVS only" wipe mode in the flasher overwrites the cached-licence partition with empty state, so the legacy key is gone by the time the patched firmware boots. Mesh identity, contacts and message history live in a different partition and survive the flash untouched. No separate erase-flash needed.
⚠ Seriously, just use our flasher. It handles the NVS wipe + offset correctly without you having to think about it. The command below works if you really know what you're doing, but the surgical-NVS-wipe mode in our flasher is what's been tested on real hardware; anything else and you're on your own to debug "says activated but isn't" or boot-loop symptoms.
esptool --chip esp32s3 --port COMx write-flash 0x0 MeshOS-TDeck-1.3.5.bin (replace COMx with your port; on Linux/Mac it's /dev/ttyUSB0 or /dev/cu.usbserial-*). esptool erases each sector it's about to write to before writing, so the merged image's empty-NVS region wipes the cached licence on the same operation. If you previously had a stock build activated and skip this step, you'll get the cached-licence symptoms our flasher's surgical-NVS mode is built to avoid.——// Requires desktop Chrome or Edge over HTTPS. Plug the T-Deck in, click Connect, then Get key → Activate. The activation logic stays server-side.
// Self-contained snippets you can paste into any HTML page. All call the public https://meshoskey.com/api.php; pointing them at your own mirror is a one-line change. Each widget below tags which target it works with.
// For the Android Companion APK (v1.0.2). User pastes the 16-char Android ID, widget fetches an activation key from the API and shows it. Standalone HTML + JS, no dependencies.
⚠ Requires the community build: Andy's official APK won't accept keys from this API. Users need MeshOS-v1.0.4.apk (mirrored on this page, also linked from meshoskey.com/#v1-0-4). When you embed the widget elsewhere, link the APK alongside it.
<!-- MeshOS v1.0.2 keygen -- drop-in widget. --> <div id="meshos-keygen-v2" style="font-family:monospace;max-width:480px;padding:1rem;background:#0a0f0a;color:#c8ffcf;border:1px solid #ff5577;border-radius:4px;"> <div style="color:#ff5577;margin-bottom:0.5rem;font-size:0.85rem;letter-spacing:0.1em;">MESHOS KEYGEN · v1.0.2</div> <input id="mk2-in" type="text" maxlength="32" placeholder="android id (16 hex chars)" style="width:100%;padding:0.5rem;background:#000;color:#ff5577;border:1px solid #1a2a1a;font-family:inherit;box-sizing:border-box;" /> <button id="mk2-go" style="margin-top:0.5rem;width:100%;padding:0.5rem;background:transparent;color:#ff5577;border:1px solid #ff5577;font-family:inherit;cursor:pointer;letter-spacing:0.1em;">GENERATE</button> <textarea id="mk2-out" readonly style="margin-top:0.75rem;width:100%;min-height:90px;padding:0.5rem;background:#000;color:#ff5577;border:1px solid #1a2a1a;font-family:inherit;box-sizing:border-box;word-break:break-all;"></textarea> </div> <script> (function(){ var API = 'https://meshoskey.com/api.php'; var i = document.getElementById('mk2-in'), b = document.getElementById('mk2-go'), o = document.getElementById('mk2-out'); async function run(){ var v = i.value.trim().replace(/[^a-fA-F0-9]/g, '').toUpperCase(); if (v.length < 8) { o.value = '// id must be at least 8 hex chars'; return; } o.value = '// fetching key...'; try { var r = await fetch(API, { method: 'POST', headers: { 'Accept': 'application/json' }, body: new URLSearchParams({ android_id: v, keypair: 'v2' }) }); var j = await r.json(); o.value = j.key || j.license || j.signature || ('// error: ' + r.status); } catch (e) { o.value = '// network error: ' + e.message; } } b.addEventListener('click', run); i.addEventListener('keydown', function(e){ if (e.key === 'Enter') run(); }); })(); </script>
// For the T-Deck Firmware (v1.3.0). Drop into any HTTPS page (or localhost). Connects to the T-Deck over USB serial, fetches an activation key from the API, writes it back to the device. ~30 lines, no dependencies.
⚠ Requires the community firmware: stock MeshOS won't accept keys from this API. Users need to flash MeshOS-TDeck-1.3.2.bin first (mirrored on this page, see flashing steps at meshoskey.com/#tdeck-1-3-0). When you embed the widget elsewhere, link the firmware alongside it.
<!-- MeshOS T-Deck activator -- drop-in. Requires desktop Chrome/Edge over HTTPS. --> <!-- Same approach Andy's serial-cli.js uses for MeshOS terminals: write, settle, parse. --> <div id="ta-w" style="font-family:monospace;max-width:560px;padding:1rem;background:#0a0f0a;color:#c8ffcf;border:1px solid #00d4ff;border-radius:4px;"> <div style="color:#00d4ff;margin-bottom:0.5rem;font-size:0.85rem;letter-spacing:0.1em;">T-DECK ACTIVATOR</div> <button id="ta-c">Connect</button> <button id="ta-g" disabled>Get key</button> <button id="ta-a" disabled>Activate</button> <pre id="ta-l" style="margin-top:0.5rem;background:#000;padding:0.5rem;color:#6aa972;font-size:0.78rem;max-height:180px;overflow:auto;"></pre> </div> <script> (function(){ var API='https://meshoskey.com/api.php'; var port=null,reader=null,writer=null,buf='',devkey='',sig=''; var td=new TextDecoder(),te=new TextEncoder(); var $=function(id){return document.getElementById(id);}; function log(t,c){var s=document.createElement('span');if(c)s.style.color=c;s.textContent=t+'\n';$('ta-l').appendChild(s);$('ta-l').scrollTop=9e9;} async function readLoop(){try{while(reader){var r=await reader.read();if(r.done)break;buf+=td.decode(r.value,{stream:true});}}catch(e){}} // matches Andy's sendRawCommand: settle 400ms quiet, max 3000ms total async function send(cmd){buf='';log('> '+cmd,'#00d4ff');await writer.write(te.encode(cmd+'\r\n'));var t=Date.now(),lc=0,lt=t;while(Date.now()-t<3000){await new Promise(r=>setTimeout(r,50));if(buf.length!==lc){lc=buf.length;lt=Date.now();}else if(lc>0&&Date.now()-lt>400)break;}return buf;} $('ta-c').onclick=async function(){try{port=await navigator.serial.requestPort({});await port.open({baudRate:115200});reader=port.readable.getReader();writer=port.writable.getWriter();readLoop();log('connected','#39ff7c');$('ta-g').disabled=false;}catch(e){log('connect failed: '+e.message,'#ff5577');}}; $('ta-g').onclick=async function(){try{ var r=await send('/license show'); var m=r.replace(/\x1B\[[0-?]*[ -\/]*[@-~]/g,'').match(/Device\s*serial:?\s*([0-9a-fA-F]{8,64})/i); if(!m)throw new Error('no device key in response'); devkey=m[1].toUpperCase();log('device key: '+devkey,'#39ff7c'); var resp; try{ resp=await fetch(API,{method:'POST',mode:'cors',headers:{'Accept':'application/json'},body:new URLSearchParams({android_id:devkey,keypair:'v2'})}); }catch(ne){throw new Error('fetch blocked — '+(ne.message||ne)+' (likely CORS / file:// origin / extension; open devtools Network for details)');} var txt=await resp.text(),j=null;try{j=JSON.parse(txt);}catch(_){} sig=j&&(j.key||j.license||j.signature); if(!sig)throw new Error('api '+resp.status+': '+(txt||'(empty)').slice(0,160)); log('activation: '+sig,'#39ff7c');$('ta-a').disabled=false; }catch(e){log('get key failed: '+e.message,'#ff5577');}}; $('ta-a').onclick=async function(){try{await send('/license set '+sig);log('activation sent','#39ff7c');}catch(e){log('activate failed: '+e.message,'#ff5577');}}; })(); </script>
// CLI script that prompts for any device id (8–64 hex chars) and prints the activation key from the API. Works for both the Android Companion (v1.0.2) and the T-Deck (v1.3.0) — same endpoint accepts both shapes. Stdlib only.
⚠ Pair with the community builds: the keys this script returns only validate against the community APK / firmware: MeshOS-v1.0.4.apk and MeshOS-TDeck-1.3.2.bin. Stock builds will reject them.
#!/usr/bin/env python3 # MeshOS keygen -- prompts for device id, fetches activation key from the community API. import json, re, sys, urllib.parse, urllib.request API = "https://meshoskey.com/api.php" def fetch_key(device_id: str) -> str: aid = re.sub(r'[^a-fA-F0-9]', '', device_id).upper() if not (8 <= len(aid) <= 64): raise ValueError("device id must be 8-64 hex chars") body = urllib.parse.urlencode({"android_id": aid, "keypair": "v2"}).encode() req = urllib.request.Request(API, data=body, headers={"Accept": "application/json"}) with urllib.request.urlopen(req, timeout=15) as r: payload = json.loads(r.read().decode()) return payload.get("key") or payload.get("license") or "" if __name__ == "__main__": aid = input("Device ID: ").strip() print(fetch_key(aid))
// Kept here for completeness. If you have a v1.0.0 APK or a v1.1.8 T-Deck install, you can still use these. Otherwise use the current versions above.
// Andy's original release. License is a 32-bit djb2 hash, masked with MCPP. Pure offline keygen — runs in your browser, never touches a server.
// Works for both: the Android v1.0.0 APK (input: 16-char Android ID) and the T-Deck v1.1.8 firmware (input: 12-char uppercase MAC). Same algorithm, just different input shape.
fa4c20a51cd85e940d1f2f2758188db63bf3b5aa0690f5911a3feaebb4075e35// Pure offline widget — runs in the browser, never touches a server. Pairs with the Android Companion v1.0.0 APK and the T-Deck v1.1.8 firmware (both use the same djb2 algorithm).
⚠ Older stock builds: Andy's original MeshOS-v1.0.0.apk and MeshOS-TDeck-1.1.8.bin — these accept the offline djb2 keys directly. For current versions, use the v1.0.2 / v1.3.0 widgets above.
<!-- MeshOS v1.0.0 keygen -- offline djb2. --> <div id="meshos-keygen" style="font-family:monospace;max-width:420px;padding:1rem;background:#0a0f0a;color:#c8ffcf;border:1px solid #39ff7c;border-radius:4px;"> <div style="color:#39ff7c;margin-bottom:0.5rem;font-size:0.85rem;letter-spacing:0.1em;">MESHOS KEYGEN · v1.0.0</div> <input id="mk-in" type="text" placeholder="android id / device id" style="width:100%;padding:0.5rem;background:#000;color:#39ff7c;border:1px solid #1a2a1a;font-family:inherit;box-sizing:border-box;" /> <button id="mk-go" style="margin-top:0.5rem;width:100%;padding:0.5rem;background:transparent;color:#39ff7c;border:1px solid #39ff7c;font-family:inherit;cursor:pointer;letter-spacing:0.1em;">GENERATE</button> <div id="mk-out" style="margin-top:0.75rem;padding:0.5rem;background:#000;min-height:1.5rem;text-align:center;letter-spacing:0.2em;color:#39ff7c;"></div> </div> <script> (function(){ function gen(id){ var s=id.replace(/[:\- \\]/g,''),h=0; for(var i=0;i<s.length;i++)h=(h*33+s.charCodeAt(i))>>>0; var b=[((h>>>24)&0xFF)^0x4D,((h>>>16)&0xFF)^0x43,((h>>>8)&0xFF)^0x50,(h&0xFF)^0x50]; return b.map(function(x){return x.toString(16).padStart(2,'0');}).join(''); } var i=document.getElementById('mk-in'),b=document.getElementById('mk-go'),o=document.getElementById('mk-out'); function run(){var v=i.value.trim();o.textContent=v?gen(v):'// enter an id';} b.addEventListener('click',run);i.addEventListener('keydown',function(e){if(e.key==='Enter')run();}); })(); </script>
// Pure offline CLI — computes the djb2 + MCPP key locally with no network. Works for both the Android Companion v1.0.0 APK and the T-Deck v1.1.8 firmware.
⚠ Older stock builds: pair with MeshOS-v1.0.0.apk or MeshOS-TDeck-1.1.8.bin. For current versions, use the v1.0.2 / v1.3.0 Python script above.
#!/usr/bin/env python3 # v1.0.0 keygen -- offline djb2 + MCPP mask. import re def generate_key(device_id: str) -> str: s = re.sub(r'[:\- ]', '', device_id) h = 0 for ch in s: h = (h * 33 + ord(ch)) & 0xFFFFFFFF b0 = ((h >> 24) & 0xFF) ^ 0x4D b1 = ((h >> 16) & 0xFF) ^ 0x43 b2 = ((h >> 8) & 0xFF) ^ 0x50 b3 = ( h & 0xFF) ^ 0x50 return f"{b0:02x}{b1:02x}{b2:02x}{b3:02x}" if __name__ == "__main__": print(generate_key(input("Device ID: ").strip()))
// The round that took real work. XOR-obfuscated public key, inlined verifier, snapshot encoding change. About three hours of analysis. v1.0.4 (above) is a cosmetic rotation of this build — the actual Ed25519 keypair was not rotated, so this build's activation keys are byte-for-byte interchangeable with v1.0.4's. Hosted for completeness; if you want the current build use v1.0.4 above.
1c4808887eac0a9cf02ab268fc958ecc04750d5f5fff88e8c88ad7eaf0d77149// Andy's previous Companion APK. First Ed25519 round, easier than v1.0.3 because the public key was stored as a Uint8List (contiguous byte run in the snapshot, no XOR). Cracked in 13 minutes. Hosted here for completeness; if you want the current activator flow use v1.0.4 above.
15e931e67f1e865964fb934b7bca9ae6d254157b55586f512a7c5e24c512bfd4// Round 8. Hardest T-Deck round so far — five anchors, dual-verifier, SHA-256 anti-tamper magic, three-strike reboot path. The patched build activated cleanly but we ran into intermittent crashes in normal use that we never fully diagnosed before v1.3.5 shipped and rendered v1.3.4 obsolete. We did not patch v1.3.4 to a clean-room production-ready state in time. Use v1.3.5 above. v1.3.4 is kept here only for archive reference.
461ad30c16afe1dec19d1b3049a5c6dfd6d8ad8607c8e0c3992e2eda80610aef// Round 5. Same Ed25519 scheme as v1.3.0/v1.3.1, cracked in 90 seconds — "kettle didn't boil". Keys generated against this build use the legacy keypair (v1) and are not interchangeable with v1.3.5. Hosted for completeness; if you want the current build use v1.3.5 above.
8eae00eb514efa56cb13c479a12d109c2b05746684577b008b605e86d8f33145// One of five hotfixes Andy shipped in a single week. Same Ed25519 keypair, same activator, same flow. Hosted for completeness.
23231ffc76f3b8fd46255e5aeccb995cb97109314abe8bde0a9188e1f4ab7e33// Superseded by v1.3.1 (above). Same Ed25519 keypair, same Web Serial activator, same flow. Hosted here for completeness in case anyone wants to compare bytes.
51fb2cae52d7efedf19d2bd6aa88f42373c9d3fa90139862d4f918aff37e522b// Older T-Deck firmware. The newer v1.3.0 above supersedes this. Hosted for completeness.
// Activation: this build uses the same djb2 license check as the v1.0.0 Android APK. Use the v1.0.0 offline keygen above — input the device's MAC (12 hex chars, uppercase, no separators) instead of an Android ID, and you get the activation key directly. Pure offline, no API needed.
andy: "I'm using Ed25519 signatures now. That should fix it."
us: "No, it didn't."
your move, andy.
everyone else → guestbook