initial release

This commit is contained in:
2026-08-27 10:08:49 -04:00
commit adb8b18390
3 changed files with 438 additions and 0 deletions

24
LICENSE Normal file
View File

@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org/>

5
README.md Normal file
View File

@@ -0,0 +1,5 @@
# Notaquest
I wrote everything on the site. I don't feel like writing it again here.
Only tested on Meta Quest 3 but it might work on other Meta Quest devices.
Go to [notaquest.owendeed.com](https://notaquest.owendeed.com).

409
index.html Normal file
View File

@@ -0,0 +1,409 @@
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Notaquest</title>
<script src="https://cdn.jsdelivr.net/npm/tweetnacl@1.0.3/nacl-fast.min.js"></script>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
/>
</head>
<body>
<main class="container">
<h1>Notaquest</h1>
<p>
enabling adb on a meta quest (which is just a weird phone btw) is
PAINFUL! you have to get the slow meta horizon app, create an
organization, verify it by adding an authenticator or USING YOUR CREDIT
CARD and finally, being able to enable a thing androids could do in like
8 taps for years. it doesn't have to be a quest to enable adb. meta
still made it a quest for me to make this work for you, and it's gonna
suck, but it'll suck less than the normal way. i have uploaded 2 images
of forks for meta verification, i will update this message if it works
and i can do oauth. you need to use devtools and grab a cookie. i'm very
sorry. this seems to work better on mobile than on pc. tested on android
with google chrome. you may need to quickly accept a pairing
confirmation on the device and headset. firefox is not supported :(
</p>
<p>
i apologize for you having to give me the token, this isn't something
you should need to do to own your device, but sadly, it is required. you
can check the html (human friendly code), and you can log network
requests from the site. the only place it goes it to is my site with a
proxy to meta. yeah, i know what you're thinking if you get what that
means. the token goes to my site. i need to for this to work, i'm sorry,
there's a thing below explaining the code. i know i can't prove it, but
it's safe. if you're really paranoid, reset your password or log out of
the page you got the token from to invalidate the token. if i stole your
token i wouldn't be telling you to do this. you can even check if it
logs into your account before you reset it.
</p>
<details>
<summary>why does this use a proxy?</summary>
<p>
meta's api blocks requests from websites (CORS). the proxy forwards
your request to graph.oculus.com and nothing else. nothing is stored
or logged. you can
<a href="https://api.notaquest.owendeed.com" target="_blank"
>view the live worker source code</a
>
or check the network tab yourself. the code is a literal quine
program, if i change it, it will change.
</p>
</details>
<details>
<summary>why does this site look ancient?</summary>
<p>
this site is a one time tool, there's no point to make it look good.
</p>
</details>
<details>
<summary>what is the point?</summary>
<p>
normally, enabling ADB on your meta headset, is a PAIN to do, like the
amount of work rooting a normal phone takes if you're lucky. which is
still bad. you need to create a meta organization, and then verify
your account. verifying means a few options: enabling an authenticator
app for signing in, or using a credit card and putting a temporary
hold on it. that's for enabling the ability to control a device you
bought. and not even full access to it. this site lets you enable it
without any of that. the only annoying part is the meta token, which
sadly i don't think i can bypass. better than nothing i guess?
</p>
</details>
<details>
<summary>but i can't access devtools on my phone!</summary>
<p>
yeah. here's the best fix for that i can come up with for now. (check
back later if you really can't do this, once i get oauth it will be
much simpler). at the bottom click the generate token qr code button.
this will give you a qr code. scan it on your phone and go to the
link. it will be prefilled with your token so you don't need to type
it :)
</p>
</details>
<ol>
<li>
Log into
<a href="https://developers.meta.com/horizon/manage/" target="_blank"
>developers.meta.com/horizon/manage/</a
>
</li>
<li>Press F12 to open developer tools</li>
<li>Application → Cookies → click the site<br /></li>
<li>Find <code>oc_ac_at</code>, double-click the value, copy it</li>
<li>
Paste it below (if you need to type manually, make sure the OC or FRL
at the beginning is capitalized!)
</li>
</ol>
<input
id="token"
placeholder="paste your Meta token here"
style="width: 100%; max-width: 500px"
/>
<br /><br />
<button onclick="showQR()">
generate prefilled token qr code for mobile
</button>
<br />
<br />
<img id="qr" style="width: 300px; image-rendering: pixelated" />
<br />
<button id="go" onclick="enableADB()">enable adb</button>
<p id="nobt" style="display: none"></p>
<p id="status"></p>
</main>
<script src="https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.min.js"></script>
<script>
if (!navigator.bluetooth) {
document.getElementById("go").style.display = "none";
document.getElementById("nobt").style.display = "block";
document.getElementById("nobt").textContent =
"your browser doesn't support web bluetooth. grab your token, generate a qr code below, and scan it on your phone in chrome or another web bluetooth supported device. respect to the firefox people seeing this.";
}
var params = new URLSearchParams(location.search);
if (params.get("token"))
document.getElementById("token").value = params.get("token");
var SERVICE = "0000feb8-0000-1000-8000-00805f9b34fb";
var WRITE_UUID = "7a442881-509c-47fa-ac02-b06a37d9eb76";
function status(msg) {
document.getElementById("status").textContent = msg;
}
function showQR() {
var token = document.getElementById("token").value.trim();
if (!token) {
status("paste your token first");
return;
}
if (!token.startsWith("OC") && !token.startsWith("FRL")) {
status(
"that doesn't look like a meta token. it should start with OC or FRL.",
);
return;
}
var qr = qrcode(0, "L");
qr.addData("https://notaquest.owendeed.com/?token=" + token);
qr.make();
document.getElementById("qr").src = qr.createDataURL();
}
// protobuf helpers
function varint(n) {
var r = [];
do {
var b = n & 0x7f;
n >>>= 7;
if (n > 0) b |= 0x80;
r.push(b);
} while (n > 0);
return r;
}
function varintField(f, v) {
return [(f << 3) | 0].concat(varint(v));
}
function bytesField(f, d) {
return [(f << 3) | 2].concat(varint(d.length)).concat(Array.from(d));
}
function readVarint(buf, off) {
var r = 0,
s = 0;
while (off < buf.length) {
var b = buf[off++];
r |= (b & 0x7f) << s;
if (!(b & 0x80)) break;
s += 7;
}
return [r, off];
}
function parseProto(buf) {
var fields = {},
off = 0;
while (off < buf.length) {
var tag;
[tag, off] = readVarint(buf, off);
var fn = tag >> 3,
wt = tag & 7;
if (wt === 0) {
var val;
[val, off] = readVarint(buf, off);
fields[fn] = val;
} else if (wt === 2) {
var ln;
[ln, off] = readVarint(buf, off);
fields[fn] = buf.slice(off, off + ln);
off += ln;
} else break;
}
return fields;
}
// chunk data with 2-byte framing header
function makeChunks(data) {
var chunks = [];
for (var i = 0; i < data.length; i += 18) {
var slice = data.slice(i, i + 18);
var seq = Math.floor(i / 18);
var last = i + 18 >= data.length;
chunks.push(
new Uint8Array(
[(last ? 0x80 : 0x00) | (seq >> 8), seq & 0xff].concat(
Array.from(slice),
),
),
);
}
return chunks;
}
// send chunked data, then poll-read the response
async function sendAndReceive(writeChar, data) {
for (var c of makeChunks(Array.from(data))) {
await writeChar.writeValueWithoutResponse(c);
await new Promise((r) => setTimeout(r, 100));
}
await new Promise((r) => setTimeout(r, 3000));
var response = [];
for (var i = 0; i < 200; i++) {
await new Promise((r) => setTimeout(r, 100));
var val = new Uint8Array((await writeChar.readValue()).buffer);
if (val.length <= 1 || val[0] === 0xff) break;
response.push(...val.slice(2));
if (val[0] & 0x80) break;
}
return new Uint8Array(response);
}
// HMAC-SHA256 using Web Crypto
async function hmacSHA256(key, message) {
var k = await crypto.subtle.importKey(
"raw",
key,
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
);
return new Uint8Array(await crypto.subtle.sign("HMAC", k, message));
}
// encrypt a plaintext request with the shared key (nonce + ciphertext)
function encrypt(plaintext, sharedKey) {
var nonce = nacl.randomBytes(24);
var ciphertext = nacl.box.after(plaintext, nonce, sharedKey);
var out = new Uint8Array(24 + ciphertext.length);
out.set(nonce);
out.set(ciphertext, 24);
return out;
}
// decrypt a response (first 24 bytes = nonce, rest = ciphertext)
function decrypt(data, sharedKey) {
return nacl.box.open.after(
data.slice(24),
data.slice(0, 24),
sharedKey,
);
}
async function enableADB() {
try {
var token = document.getElementById("token").value.trim();
if (!token) {
status("paste your token first");
return;
}
if (!token.startsWith("OC") && !token.startsWith("FRL")) {
status(
"that doesn't look like a meta token. it should start with OC or FRL.",
);
return;
}
// 1. get device_secret from meta api
status("fetching device secret...");
var resp = await fetch("https://api.notaquest.owendeed.com", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body:
"access_token=" +
encodeURIComponent(token) +
"&doc_id=25735612872735053&variables=%7B%7D",
});
var json = await resp.json();
var secret = json?.data?.viewer?.user?.device_secret;
if (!secret) {
status(
"couldn't get device secret. is your token valid? sign out of the meta page, sign back in, and grab a new token. reloading might also work.",
);
return;
}
var deviceSecret = new Uint8Array(
secret.match(/.{2}/g).map((b) => parseInt(b, 16)),
);
// 2. connect bluetooth
status("select your quest...");
var device = await navigator.bluetooth.requestDevice({
acceptAllDevices: true,
optionalServices: [SERVICE],
});
var server = await device.gatt.connect();
var service = await server.getPrimaryService(SERVICE);
var writeChar = await service.getCharacteristic(WRITE_UUID);
// 3. wait for user to accept pairing on quest
status(
"accept the bluetooth pairing on your quest and mobile device if you see it. (just wait 15 seconds if you don't see it)",
);
await new Promise((r) => setTimeout(r, 15000));
// 4. send HELLO
status("connecting to quest...");
var keypair = nacl.box.keyPair();
var helloBody = bytesField(1, keypair.publicKey).concat(
bytesField(2, nacl.randomBytes(16)),
);
var hello = new Uint8Array(
varintField(1, 1).concat(
varintField(2, 1),
varintField(3, 1),
bytesField(4, helloBody),
),
);
var helloResp = await sendAndReceive(writeChar, hello);
if (!helloResp.length) {
status(
"quest didn't respond. it's battery may have died, you didn't accept a confirmation, or it went out of bluetooth range. check each and try again.",
);
return;
}
// 5. parse server key and compute shared secret
var signedData = parseProto(parseProto(parseProto(helloResp)[3])[1]);
var sharedKey = nacl.box.before(signedData[1], keypair.secretKey);
// 6. AUTHENTICATE
status("authenticating...");
var hmac = await hmacSHA256(deviceSecret, signedData[2]);
var authReq = new Uint8Array(
varintField(1, 1).concat(
varintField(2, 2),
varintField(3, 2),
bytesField(4, bytesField(1, hmac)),
),
);
var authRespRaw = await sendAndReceive(
writeChar,
encrypt(authReq, sharedKey),
);
if (!authRespRaw.length) {
status(
"no auth response, did your headset go out of bluetooth range or die? check each and try again.",
);
return;
}
var authResp = parseProto(decrypt(authRespRaw, sharedKey));
if (authResp[2] !== 0) {
status("authentication failed, try again? this shouldn't happen.");
return;
}
// 7. DEV_MODE_SET
status("enabling adb...");
var devReq = new Uint8Array(
varintField(1, 1).concat(
varintField(2, 6001),
varintField(3, 3),
bytesField(4, varintField(1, 1)),
),
);
var devRespRaw = await sendAndReceive(
writeChar,
encrypt(devReq, sharedKey),
);
if (devRespRaw.length) {
var devResp = parseProto(decrypt(devRespRaw, sharedKey));
if (devResp[2] === 0) {
status(
"done! adb should be enabled. restarting your headset may be required for it to work.",
);
return;
}
}
status("sent! check if adb is enabled.");
} catch (e) {
status(
"error: " +
e.message +
", this error isn't something i have handling for, report an issue on the link at the bottom of the page with this message.",
);
}
}
</script>
</body>
</html>