Skip to content

Instantly share code, notes, and snippets.

@stereosteve
Last active June 20, 2023 20:45
Show Gist options
  • Save stereosteve/0fc5fcb53d7ea64a1ddfe7879e3f8f8f to your computer and use it in GitHub Desktop.
Save stereosteve/0fc5fcb53d7ea64a1ddfe7879e3f8f8f to your computer and use it in GitHub Desktop.
import * as secp from "@noble/secp256k1"
import { blake3 } from "@noble/hashes/blake3"
import canonicalize from "canonicalize"
import { Address } from "micro-eth-signer"
import { base58, base64 } from "@scure/base"
// enables sync mode
import { hmac } from "@noble/hashes/hmac"
import { sha256 } from "@noble/hashes/sha256"
secp.etc.hmacSha256Sync = (k, ...m) =>
hmac(sha256, k, secp.etc.concatBytes(...m))
export type SignedObject = {
_sig: [string, string, number]
[x: string]: any
}
export const SIG_KEY = "_sig"
export function signedObjSync(privateKey: Uint8Array, obj: any) {
const wallet = Address.fromPrivateKey(privateKey)
const hash = blake3(canonicalize(obj) as string)
const sig = secp.sign(hash, privateKey)
const sigEncoded = base64.encode(sig.toCompactRawBytes())
const withSig = { ...obj, [SIG_KEY]: [wallet, sigEncoded, sig.recovery] }
return withSig
}
export function verifyObjSync(obj: SignedObject) {
// pull out signature data
const [wallet, sigEncoded, recovery] = obj[SIG_KEY]
const objCopy = { ...obj } as any
delete objCopy[SIG_KEY]
// recovery pubkey + address
const hash = blake3(canonicalize(objCopy) as string)
const sig = secp.Signature.fromCompact(base64.decode(sigEncoded))
const pubkey = sig.addRecoveryBit(recovery).recoverPublicKey(hash)
const recoveredWallet = Address.fromPublicKey(pubkey.toRawBytes())
if (recoveredWallet != wallet) {
throw new Error(
`recovered wallet : ${recoveredWallet} doesn't match _sig[0] wallet: ${wallet}`
)
}
return obj
}
/*
a simple codec for signed JSON messages.
a special `_sig` property is added to the encoded JSON, but it is otherwise the same as what you'd get from JSON.stringify:
{"action":"track.repost","id":123,"_sig":["0x09953a07689ee1959c6C13fB52AdEE57d4f74d38","L3yekPZL2pdOtoGlwa2jb4b9EhqEYgOyQo5D++WZ7bpalvFP3WUultzn7cZDRgpnKxymzYER+C1FQdnQ1dxvMw==",0]}
Using JSON Canonicalization Scheme (JCS) + blake3 means it is not dependant on orig JSON encoding.
It can for instance be stored in a JSONB column (which may change order of properties)
but signature can still be verified later.
*/
import * as secp from "@noble/secp256k1";
import { blake3 } from "@noble/hashes/blake3";
import canonicalize from "canonicalize";
import { Address } from "micro-eth-signer";
import { base64 } from "@scure/base";
const SIG_KEY = "_sig";
export async function encode(privateKey: Uint8Array, obj: any) {
const hash = blake3(canonicalize(obj) as string);
const address = Address.fromPrivateKey(privateKey);
const sig = await secp.signAsync(hash, privateKey);
const sigEncoded = base64.encode(sig.toCompactRawBytes());
const withSig = { ...obj, [SIG_KEY]: [address, sigEncoded, sig.recovery] };
return JSON.stringify(withSig);
}
export async function decode(j: string) {
const obj = JSON.parse(j);
// pull out signature data
const [address, sigEncoded, recovery] = obj[SIG_KEY];
delete obj[SIG_KEY];
// recovery pubkey + address
const hash = blake3(canonicalize(obj) as string);
const sig = secp.Signature.fromCompact(base64.decode(sigEncoded));
const pubkey = sig.addRecoveryBit(recovery).recoverPublicKey(hash);
const recoveredAddress = Address.fromPublicKey(pubkey.toRawBytes());
if (recoveredAddress != address) {
throw new Error(
`recovered address ${recoveredAddress} doesn't match signing address ${address}`
);
}
return [obj, recoveredAddress];
}
//
// test
//
(async () => {
const privKey = secp.utils.randomPrivateKey();
const encoded = await encode(privKey, { action: "track.repost", id: 123 });
console.log(encoded);
const decoded = await decode(encoded);
console.log(decoded);
// try modify message
{
const bad = encoded.replace("123", "456");
try {
const decoded = await decode(bad);
console.log(decoded);
} catch (e) {
console.log(" !!! tsk tsk", (e as Error).message);
}
}
// try to modify the signer address
{
const address = Address.fromPrivateKey(privKey);
const badWallet = "0x358Bc2D66EE2A585BB2E3f4075b235eA04893064";
const bad = encoded.replace(address, badWallet);
try {
const decoded = await decode(bad);
console.log(decoded);
} catch (e) {
console.log(" !!! tsk tsk", (e as Error).message);
}
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment