diff options
author | polwex <polwex@sortug.com> | 2025-07-16 16:54:11 +0700 |
---|---|---|
committer | polwex <polwex@sortug.com> | 2025-07-16 16:54:11 +0700 |
commit | 43db7d9fdaf9877325aae02cdad9b7bf3adc01e9 (patch) | |
tree | ad38aeed58b0fbb88031beb164ef1e321d85c2a9 | |
parent | e7d4e342d112f89bebeb08aaa502227ca7747a8c (diff) |
XOR encryption working!!! fuck yeah!!!
-rw-r--r-- | bun.lock | 3 | ||||
-rw-r--r-- | components/login/ShipCredsForm.tsx | 68 | ||||
-rw-r--r-- | lib/passkey.ts | 145 | ||||
-rw-r--r-- | lib/utils/bit.ts | 11 | ||||
-rw-r--r-- | package.json | 1 |
5 files changed, 209 insertions, 19 deletions
@@ -9,6 +9,7 @@ "@react-navigation/bottom-tabs": "^7.3.10", "@react-navigation/elements": "^2.3.8", "@react-navigation/native": "^7.1.6", + "@scure/base": "^1.2.6", "buffer": "^6.0.3", "core-js": "^3.44.0", "crypto-browserify": "^3.12.1", @@ -419,6 +420,8 @@ "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], + "@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + "@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], "@sinonjs/commons": ["@sinonjs/commons@3.0.1", "", { "dependencies": { "type-detect": "4.0.8" } }, "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ=="], diff --git a/components/login/ShipCredsForm.tsx b/components/login/ShipCredsForm.tsx index eebeeab..e4c872e 100644 --- a/components/login/ShipCredsForm.tsx +++ b/components/login/ShipCredsForm.tsx @@ -1,5 +1,4 @@ "use client"; -import "core-js/actual/typed-array"; import { ColorScheme, lightColors } from "@/constants"; import { StyleSheet, @@ -13,38 +12,32 @@ import { useState } from "react"; import { createFancyPasskey, createPasskey, + getPasskeyFromUser, pkDecrypt, + pkDecryptXOR, pkEncrypt, + pkEncryptXOR, } from "@/lib/passkey"; import toast from "react-hot-toast"; import { makeWalletFromP } from "@/lib/urbit/wallet"; -import { hex2buf } from "@/lib/utils/bit"; +import { hex2buf, hex2bytes } from "@/lib/utils/bit"; export function ShipForm() { const colors = lightColors; const styles = getStyles(colors); const [patp, setPatp] = useState("~mirtyl-wacdec"); const [ticket, setTicket] = useState("~posseg-winnub-fogluc-dirmes"); + const [ticketBytes, setTb] = useState<Uint8Array>(); + const [ticketHash, setTh] = useState(""); const [encryptedTicket, setenc] = useState(""); const [isLoading, setIsLoading] = useState(false); async function onSubmit() { const wallet = await makeWalletFromP(patp, ticket); console.log("seed", wallet.ownership.seed); const privkey = wallet.ownership.keys.private; - const clean = privkey.replace(/^0x/i, ""); - const byteLength = clean.length / 2; - toast(`bytes: ${byteLength}`); - const bufer = hex2buf(privkey); - toast(`buffer size: ${bufer.byteLength}`); - console.log(Uint8Array); - const ba = Uint8Array.fromHex(clean); - toast(`uintarray byte length: ${ba.byteLength}`); - console.log({ - bufl: bufer.length, - bufbl: bufer.byteLength, - ul: ba.length, - ubl: ba.byteLength, - }); + setTh(privkey); + const ba = hex2bytes(privkey); + setTb(ba); } async function makeFPk() { console.log("running", { patp, ticket }); @@ -58,17 +51,33 @@ export function ShipForm() { if ("error" in passkey) return toast.error(passkey.error); console.log({ passkey }); } + async function checkPk() { + const passkey = await getPasskeyFromUser(); + if ("error" in passkey) return toast.error(passkey.error); + } async function enc() { - const done = await pkEncrypt(ticket); - console.log({ encryptedTicket }); + if (!ticketBytes) return toast("gotta build the wallet first"); + const done = await pkEncryptXOR(ticketBytes); + console.log({ done }); setenc(done); toast(done); } async function dec() { - const decryptedTicket = await pkDecrypt(encryptedTicket); + const decryptedTicket = await pkDecryptXOR(encryptedTicket); console.log({ decryptedTicket }); toast(decryptedTicket); } + // async function enc() { + // const done = await pkEncrypt(ticket); + // console.log({ done}); + // setenc(done); + // toast(done); + // } + // async function dec() { + // const decryptedTicket = await pkDecrypt(encryptedTicket); + // console.log({ decryptedTicket }); + // toast(decryptedTicket); + // } return ( <View style={styles.bottomSection}> <TextInput @@ -90,6 +99,18 @@ export function ShipForm() { autoCorrect={false} secureTextEntry /> + <View> + <Text>Private Key</Text> + <View style={styles.surprise}> + <Text>{ticketHash}</Text> + </View> + </View> + <View> + <Text>Encrypted Private Key</Text> + <View style={styles.surprise}> + <Text>{encryptedTicket}</Text> + </View> + </View> <PrimaryButton label="Log in" @@ -98,6 +119,12 @@ export function ShipForm() { isLoading={isLoading} /> <PrimaryButton + label="Chec Passkey" + onPress={checkPk} + style={{ marginTop: 0 }} + isLoading={isLoading} + /> + <PrimaryButton label="Make Passkey" onPress={makePk} style={{ marginTop: 0 }} @@ -168,4 +195,7 @@ export const getStyles = (colors: ColorScheme) => color: colors.text, fontSize: 16, }, + surprise: { + backgroundColor: colors.card, + }, }); diff --git a/lib/passkey.ts b/lib/passkey.ts index 6deb656..b03ecf5 100644 --- a/lib/passkey.ts +++ b/lib/passkey.ts @@ -1,9 +1,16 @@ "use client"; +import "core-js/actual/typed-array"; import * as age from "age-encryption"; import { Platform } from "react-native"; import * as SecureStore from "expo-secure-store"; import type { AsyncRes } from "./types"; +import { randomBytes } from "ethers"; +// encreeptoor +import { sha256 } from "@noble/hashes/sha256"; +import { extract } from "@noble/hashes/hkdf"; +import { base64 } from "@scure/base"; +import { bytes2hex } from "./utils/bit"; const PASSKEY_CREDENTIAL_ID_KEY = "urbit_wallet_passkey_id"; const RELYING_PARTY_ID = "wallet.urbit.org"; // Change this to your domain @@ -212,3 +219,141 @@ export async function authenticateWithPasskey(): AsyncRes<PublicKeyCredential> { return { error: `${error}` }; } } + +export async function getPasskeyFromUser(): AsyncRes<PublicKeyCredential> { + try { + const nonce = randomBytes(16); + // Generate a random challenge + const challenge = new Uint8Array(32); + crypto.getRandomValues(challenge); + + const getCredentialOptions: CredentialRequestOptions = { + publicKey: { + allowCredentials: [], + challenge, + rpId: + window.location.hostname === "localhost" + ? "localhost" + : RELYING_PARTY_ID, + userVerification: "required", + extensions: { prf: { eval: prfInputs(nonce) } }, + }, + }; + + const credential = (await navigator.credentials.get( + getCredentialOptions, + )) as PublicKeyCredential; + console.log({ credential }); + + if (credential) { + console.log("User provided passkey:", credential); + const results = credential.getClientExtensionResults().prf; + console.log({ results }); + return { ok: credential }; + } + + return { error: "No passkey provided" }; + } catch (error) { + console.error("Error getting passkey from user:", error); + return { error: `${error}` }; + } +} + +const label = "age-encryption.org/fido2prf"; +function prfInputs(nonce: Uint8Array): AuthenticationExtensionsPRFValues { + const prefix = new TextEncoder().encode(label); + + const first = new Uint8Array(prefix.length + nonce.length + 1); + first.set(prefix, 0); + first[prefix.length] = 0x01; + first.set(nonce, prefix.length + 1); + + const second = new Uint8Array(prefix.length + nonce.length + 1); + second.set(prefix, 0); + second[prefix.length] = 0x02; + second.set(nonce, prefix.length + 1); + + return { first, second }; +} + +function deriveKey(results: AuthenticationExtensionsPRFValues): Uint8Array { + if (results.second === undefined) { + throw Error("Missing second PRF result"); + } + const prf = new Uint8Array( + results.first.byteLength + results.second.byteLength, + ); + prf.set(new Uint8Array(results.first as ArrayBuffer), 0); + prf.set( + new Uint8Array(results.second as ArrayBuffer), + results.first.byteLength, + ); + return extract(sha256, prf, label); +} + +async function getCredentialWithPRF( + nonce: Uint8Array, +): Promise<AuthenticationExtensionsPRFValues> { + const credential = (await navigator.credentials.get({ + publicKey: { + allowCredentials: [], + challenge: randomBytes(16), + rpId: + window.location.hostname === "localhost" + ? "localhost" + : RELYING_PARTY_ID, + userVerification: "required", + extensions: { prf: { eval: prfInputs(nonce) } }, + }, + })) as PublicKeyCredential; + + const results = credential.getClientExtensionResults().prf?.results; + if (results === undefined) { + throw Error("PRF extension not available (need macOS 15+, Chrome 132+)"); + } + return results; +} + +export async function pkEncryptXOR(dataBytes: Uint8Array): Promise<string> { + const nonce = randomBytes(16); + const results = await getCredentialWithPRF(nonce); + const key = deriveKey(results); + + const encrypted = new Uint8Array(dataBytes.length); + + console.log({ key: key.byteLength, data: encrypted.byteLength }); + // XOR with derived key (repeat key if data is longer) + const hash = Uint8Array.from(key, (v, i) => v ^ encrypted[i]); + for (let i = 0; i < dataBytes.length; i++) { + encrypted[i] = dataBytes[i] ^ key[i % key.length]; + } + + console.log({ hash, encrypted }); + // Return nonce + encrypted data as base64 + const result = new Uint8Array(nonce.length + encrypted.length); + result.set(nonce); + result.set(encrypted, nonce.length); + + return base64.encode(result); +} + +export async function pkDecryptXOR(encryptedData: string): Promise<string> { + const data = base64.decode(encryptedData); + const nonce = data.slice(0, 16); + const encrypted = data.slice(16); + + const results = await getCredentialWithPRF(nonce); + const key = deriveKey(results); + + const decrypted = new Uint8Array(encrypted.length); + + // XOR with derived key + for (let i = 0; i < encrypted.length; i++) { + decrypted[i] = encrypted[i] ^ key[i % key.length]; + } + const hash = Uint8Array.from(key, (v, i) => v ^ encrypted[i]); + + const one = bytes2hex(decrypted); + const two = bytes2hex(hash); + return one; +} diff --git a/lib/utils/bit.ts b/lib/utils/bit.ts index bc1ed1a..e54cd41 100644 --- a/lib/utils/bit.ts +++ b/lib/utils/bit.ts @@ -1,5 +1,7 @@ import { sha256 } from "urbit-key-generation/src/utils"; +import "core-js/actual/typed-array"; + export function shas(buf: Buffer, salt: Buffer) { return sha256(xor(salt, sha256(buf))); } @@ -33,3 +35,12 @@ export function hex2buf(hex: string) { export function buf2hex(buf: Uint8Array) { return Buffer.from(buf).reverse().toString("hex"); } +export function hex2bytes(hex: string) { + const clean = hex.replace(/^0x/i, ""); + const ba = Uint8Array.fromHex(clean); + return ba; +} + +export function bytes2hex(ta: Uint8Array) { + return ta.toHex(); +} diff --git a/package.json b/package.json index 3da8120..a5bbb4e 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@react-navigation/bottom-tabs": "^7.3.10", "@react-navigation/elements": "^2.3.8", "@react-navigation/native": "^7.1.6", + "@scure/base": "^1.2.6", "buffer": "^6.0.3", "core-js": "^3.44.0", "crypto-browserify": "^3.12.1", |