summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpolwex <polwex@sortug.com>2025-07-16 16:54:11 +0700
committerpolwex <polwex@sortug.com>2025-07-16 16:54:11 +0700
commit43db7d9fdaf9877325aae02cdad9b7bf3adc01e9 (patch)
treead38aeed58b0fbb88031beb164ef1e321d85c2a9
parente7d4e342d112f89bebeb08aaa502227ca7747a8c (diff)
XOR encryption working!!! fuck yeah!!!
-rw-r--r--bun.lock3
-rw-r--r--components/login/ShipCredsForm.tsx68
-rw-r--r--lib/passkey.ts145
-rw-r--r--lib/utils/bit.ts11
-rw-r--r--package.json1
5 files changed, 209 insertions, 19 deletions
diff --git a/bun.lock b/bun.lock
index 02019f2..99a744d 100644
--- a/bun.lock
+++ b/bun.lock
@@ -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",