Keyrxng

Automated Payment Permit Generation

Ubiquity OS · 2024 · Standalone module for secure ERC20/ERC721 permit generation. Involved deep cryptography work (X25519/TweetNaCl), three different implementations due to changing requirements, and eventual management dysfunction leading to project abandonment.

So what?
2 libsodium + TweetNaCl cryptography libraries mastered · abandoned due to management dysfunction project status
Role
Product Engineer (Cryptography & Infrastructure)
Year
2024
Stack
TypeScript, X25519 Cryptography, libsodium, TweetNaCl, blake2b, @uniswap/permit2-sdk, ethers.js, Supabase, Jest, Cloudflare Workers
Read narrative
2 libsodium + TweetNaCl cryptography libraries masteredabandoned due to management dysfunction project status

Problem

The Ubiquity ecosystem required secure permit generation for ERC20 and ERC721 token payments while isolating cryptographic operations from plugin logic. The challenge: GitHub App permissions are all-or-nothing, requiring X25519 encryption for private keys. Additional complexity emerged when libsodium proved incompatible with Cloudflare Workers, necessitating deep cryptographic research and implementation of alternative solutions.

Approach

System diagram

flowchart LR
  Req[Plugin Request] --> Validate[Input Validation]
  Validate --> Lookup[User Lookup GitHub API]
  Lookup --> Wallet[Wallet Resolution Supabase]
  Wallet --> Decrypt[Private Key Decryption X25519]
  Decrypt --> Token[Token Contract Interaction]
  Token --> Permit[Permit2 Signature Generation]
  Permit --> DB[Database Record]
  DB --> Resp[Permit Response]

Outcome

Management challenges

Proof

Code excerpt — ERC20 permit (Permit2)

const permitTransferFromData: PermitTransferFrom = {
  permitted: { token: tokenAddress, amount: utils.parseUnits(amount.toString(), tokenDecimals) },
  spender: walletAddress,
  nonce: BigInt(utils.keccak256(utils.toUtf8Bytes(`${userId}-${issueNodeId}`))),
  deadline: MaxUint256,
};
const { domain, types, values } = SignatureTransfer.getPermitData(permitTransferFromData, PERMIT2_ADDRESS, evmNetworkId);
const signature = await adminWallet._signTypedData(domain, types, values);

Code excerpt — ERC721 permit with metadata

const erc721SignatureData: Erc721PermitSignatureData = {
  beneficiary: _walletAddress,
  deadline: MaxUint256.toBigInt(),
  keys: metadata.map(([key]) => utils.keccak256(utils.toUtf8Bytes(key))),
  nonce: BigInt(utils.keccak256(utils.toUtf8Bytes(`${_userId}-${_issueNodeId}`))),
  values: metadata.map(([, value]) => value),
};

Code excerpt — libsodium to TweetNaCl cryptographic port

import { scalarMult, box } from "tweetnacl";
import blake2b from "blake2b";

// libsodium hashes the epk with the rpk to create a nonce
// tweetnacl enforces manual nonce handling
function deriveNonce(epk: Uint8Array, recipientPubKey: Uint8Array) {
  return blake2b(24).update(epk).update(recipientPubKey).digest();
}

export async function decryptKeys(cipherText: string) {
  const binaryCipher = SODIUM.from_base64(cipherText, SODIUM.base64_variants.URLSAFE_NO_PADDING);
  const epk = binaryCipher.slice(0, 32);
  const nonce = deriveNonce(epk, binaryPublic);
  const actualEncryptedMessage = binaryCipher.slice(32);
  const decryptedMessage = box.open(actualEncryptedMessage, nonce, epk, binaryPrivate);
  // ...
}

Test evidence — comprehensive ERC20 permit validation

expect(result).toEqual({
  tokenType: "ERC20", tokenAddress: "0xe91D...a97d", beneficiary: "0xefC0...b7dd", 
  nonce: "28290...862062", deadline: "11579...9639935", amount: "100000000000000000000", 
  owner: "0xf39F...2266", signature: "0xf528...89ea1c", networkId: 100
});

References