Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion off-chain/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ dist/
.eslintcache

# Misc
.DS_Store
.DS_Store

.env
2 changes: 1 addition & 1 deletion off-chain/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion off-chain/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"@noble/hashes": "^1.8.0",
"@noble/secp256k1": "^2.3.0",
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"dotenv": "^16.6.1",
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"morgan": "^1.10.0",
Expand Down
9 changes: 6 additions & 3 deletions off-chain/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import dotenv from "dotenv";
dotenv.config();
import express from "express";
// import cors from "cors";
import morgan from "morgan";
import { Request, Response, NextFunction } from "express";
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
Expand All @@ -12,10 +13,10 @@ import { secp256k1 as curve } from "@noble/curves/secp256k1";
import { fromB64 } from "@mysten/bcs";
import { keccak_256 } from "@noble/hashes/sha3";
import { CURVE, etc, getSharedSecret } from "@noble/secp256k1";
const { bytesToHex, hexToBytes, mod, hashToPrivateKey } = etc;
const { bytesToHex, hexToBytes, mod } = etc;
import { decodeSuiPrivateKey, PublicKey } from "@mysten/sui/cryptography";

import * as secp from "@noble/secp256k1";
import stealthRoute from "./routes/stealth.route";

const app = express();

Expand Down Expand Up @@ -290,6 +291,8 @@ app.get("/new-wallet-address", async (_req: Request, res: Response) => {
}
});

app.use("/stealth", stealthRoute);

// Handle all routes that are not defined
app.all("*", (req: Request, _res: Response, next: NextFunction) => {
next(new Error(`Can't find ${req.originalUrl} on this server!`));
Expand Down
11 changes: 11 additions & 0 deletions off-chain/src/crypto/ecdh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { keccak_256 } from "@noble/hashes/sha3";
import { getSharedSecret } from "@noble/secp256k1";
import { PublicKey } from "@mysten/sui/cryptography";

export function computeSharedSecret(
privKey: Uint8Array,
pubKey: PublicKey
): Uint8Array {
const secret = getSharedSecret(privKey, pubKey.toRawBytes());
return keccak_256(secret.slice(1));
}
24 changes: 24 additions & 0 deletions off-chain/src/crypto/ed25519.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
import { fromB64 } from "@mysten/bcs";

export async function signMessage(
privateKey: string,
message: string
): Promise<string> {
const kp = Ed25519Keypair.fromSecretKey(privateKey);
const bytes = new TextEncoder().encode(message);
const { signature } = await kp.signPersonalMessage(bytes);
return signature;
}

export function extractRandS(signature: string): {
r: Uint8Array;
s: Uint8Array;
} {
const raw = fromB64(signature);
const sig = raw.slice(1, 65);
return {
r: sig.slice(0, 32),
s: sig.slice(32, 64),
};
}
16 changes: 16 additions & 0 deletions off-chain/src/crypto/scalars.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { CURVE, etc, getSharedSecret } from "@noble/secp256k1";
const { bytesToHex, hexToBytes, mod, hashToPrivateKey } = etc;

export function toPrivateKey(seed32: Uint8Array): Uint8Array {
let x = BigInt("0x" + bytesToHex(seed32));
x = mod(x, CURVE.n);
if (x === 0n) x = 1n;
return hexToBytes(x.toString(16).padStart(64, "0"));
}

export function toScalar32(hash32: Uint8Array): bigint {
let x = BigInt("0x" + bytesToHex(hash32));
x = mod(x, CURVE.n);
if (x === 0n) x = 1n;
return x;
}
33 changes: 33 additions & 0 deletions off-chain/src/crypto/secp256k1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
Secp256k1Keypair,
Secp256k1PublicKey,
} from "@mysten/sui/keypairs/secp256k1";
import { CURVE, etc } from "@noble/secp256k1";
import { secp256k1 as curve } from "@noble/curves/secp256k1";

const { bytesToHex, hexToBytes, mod } = etc;

export function generateKeypair(): Secp256k1Keypair {
return new Secp256k1Keypair();
}

export function deriveStealthPublicKey(
spendPub: Secp256k1PublicKey,
hashScalar: bigint
): Secp256k1PublicKey {
const H = curve.ProjectivePoint.BASE.multiply(hashScalar);
const P = curve.ProjectivePoint.fromHex(spendPub.toRawBytes());
const Pstealth = P.add(H);
return new Secp256k1PublicKey(Pstealth.toRawBytes(true));
}

export function deriveStealthPrivateKey(
spendPriv: Uint8Array,
hashScalar: bigint
): Secp256k1Keypair {
const s = BigInt("0x" + bytesToHex(spendPriv));
const k = mod(s + hashScalar, CURVE.n);
return Secp256k1Keypair.fromSecretKey(
hexToBytes(k.toString(16).padStart(64, "0"))
);
}
3 changes: 0 additions & 3 deletions off-chain/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import * as dotenv from "dotenv";
dotenv.config();

import app from "./app";

const port = process.env.PORT ?? 3000;
Expand Down
60 changes: 60 additions & 0 deletions off-chain/src/routes/stealth.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Router } from "express";
import { signMessage, extractRandS } from "../crypto/ed25519";
import { deriveMetaKeys } from "../stealth/meta";
import { generateKeypair } from "../crypto/secp256k1";
import { senderDeriveStealthAddress } from "../stealth/sender";
import { receiverDeriveStealthKeypair } from "../stealth/receiver";
import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
import { Secp256k1PublicKey } from "@mysten/sui/dist/cjs/keypairs/secp256k1";

const RECEIVER_PRIVATE_KEY = process.env.RECEIVER_PRIVATE_KEY;
const STEALTH_MESSAGE = process.env.STEALTH_MESSAGE;

if (!RECEIVER_PRIVATE_KEY || !STEALTH_MESSAGE) {
throw new Error("RECEIVER_PRIVATE_KEY or STEALTH_MESSAGE is not set");
}

const router = Router();

router.get("/", async (_req, res) => {
/*
a random message is signed by the receiver private key and generate a signature
*/
const receiverPriv = RECEIVER_PRIVATE_KEY;
const message = STEALTH_MESSAGE;

const signature = await signMessage(receiverPriv, message);

//

const { r, s } = extractRandS(signature);

const meta = deriveMetaKeys(r, s);


const emp = generateKeypair();

const { secretKey: empPrivBytes } = decodeSuiPrivateKey(emp.getSecretKey());

const sender = senderDeriveStealthAddress(
empPrivBytes,
meta.viewPub,
meta.spendPub as Secp256k1PublicKey
);

const receiver = receiverDeriveStealthKeypair(
meta.viewPriv,
meta.spendPriv,
emp.getPublicKey()
);

res.json({
stealthMetaKeys: meta,
senderStealthAddress: sender.stealthAddress,
receiverStealthAddress: receiver.stealthAddress,
match: sender.stealthAddress === receiver.stealthAddress,
receiverSecretKey: receiver.stealthKeypair.getSecretKey(),
});
});

export default router;
29 changes: 29 additions & 0 deletions off-chain/src/stealth/meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { keccak_256 } from "@noble/hashes/sha3";
import { toPrivateKey } from "../crypto/scalars";
import { Secp256k1Keypair } from "@mysten/sui/keypairs/secp256k1";

export function deriveMetaKeys(r: Uint8Array, s: Uint8Array) {
/*
derive the view and spend private keys from the random numbers r and s
*/
const viewPriv = toPrivateKey(keccak_256(r));
const spendPriv = toPrivateKey(keccak_256(s));

/*
derive the view and spend public keys from the view and spend private keys
*/
const view = Secp256k1Keypair.fromSecretKey(viewPriv);
const spend = Secp256k1Keypair.fromSecretKey(spendPriv);

return {
viewPriv,
spendPriv,
viewPub: view.getPublicKey(),
spendPub: spend.getPublicKey(),
/*
derive the view and spend addresses
*/
viewAddress: view.getPublicKey().toSuiAddress(),
spendAddress: spend.getPublicKey().toSuiAddress(),
};
}
19 changes: 19 additions & 0 deletions off-chain/src/stealth/receiver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { computeSharedSecret } from "../crypto/ecdh";
import { toScalar32 } from "../crypto/scalars";
import { deriveStealthPrivateKey } from "../crypto/secp256k1";
import { PublicKey } from "@mysten/sui/cryptography";

export function receiverDeriveStealthKeypair(
viewPriv: Uint8Array,
spendPriv: Uint8Array,
empPub: PublicKey
) {
const shared = computeSharedSecret(viewPriv, empPub);
const h = toScalar32(shared);
const kp = deriveStealthPrivateKey(spendPriv, h);
return {
stealthKeypair: kp,
stealthAddress: kp.getPublicKey().toSuiAddress(),
shared,
};
}
20 changes: 20 additions & 0 deletions off-chain/src/stealth/sender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { computeSharedSecret } from "../crypto/ecdh";
import { toScalar32 } from "../crypto/scalars";
import { deriveStealthPublicKey } from "../crypto/secp256k1";
import { Secp256k1PublicKey } from "@mysten/sui/keypairs/secp256k1";
import { PublicKey } from "@mysten/sui/cryptography";

export function senderDeriveStealthAddress(
empPriv: Uint8Array,
viewPub: PublicKey,
spendPub: Secp256k1PublicKey
) {
const shared = computeSharedSecret(empPriv, viewPub);
const h = toScalar32(shared);
const stealthPub = deriveStealthPublicKey(spendPub, h);
return {
stealthPub,
stealthAddress: stealthPub.toSuiAddress(),
shared,
};
}
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion stealth_address/.gitignore

This file was deleted.

48 changes: 0 additions & 48 deletions stealth_address/Move.lock

This file was deleted.

36 changes: 0 additions & 36 deletions stealth_address/Move.toml

This file was deleted.

Empty file.
Loading