initial release
This commit is contained in:
24
LICENSE
Normal file
24
LICENSE
Normal 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
5
README.md
Normal 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
409
index.html
Normal 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>
|
||||||
Reference in New Issue
Block a user