Last active
June 20, 2023 20:45
-
-
Save stereosteve/0fc5fcb53d7ea64a1ddfe7879e3f8f8f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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