~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
78 lines · 2.6 KB Raw History
// The identity half of setup: a secp256k1 signing key, its PLC operation and
// the DID derived from it. Everything here runs in the visitor's browser. The
// key is secp256k1 rather than P-256 because some third-party services verify
// only ES256K service JWTs.

import { p256 } from '@noble/curves/p256';
import { secp256k1 } from '@noble/curves/secp256k1';
import { base32Encode, bytesToHex } from '@pdsjs/core/crypto';
import {
  buildPlcOperation,
  publicKeyToDidKey,
  signPlcOperationObject,
} from '@pdsjs/core/plc';
import { cborEncodeDagCbor } from '@pdsjs/core/repo';

export function generateIdentity() {
  const privateKey = secp256k1.utils.randomPrivateKey();
  const publicKey = secp256k1.getPublicKey(privateKey, true);
  // The `#atproto_kem` key. Space syncers derive a shared secret against it.
  const kemPrivateKey = p256.utils.randomPrivateKey();
  return {
    privateKey,
    privateKeyHex: bytesToHex(privateKey),
    didKey: publicKeyToDidKey(publicKey, 'secp256k1'),
    kemKeyHex: bytesToHex(kemPrivateKey),
    kemDidKey: publicKeyToDidKey(
      p256.getPublicKey(kemPrivateKey, true),
      'p256',
    ),
    /** @type {import('@pdsjs/core/crypto').SigningKey} */
    signer: {
      curve: 'secp256k1',
      async sign(/** @type {Uint8Array} */ data) {
        const hash = new Uint8Array(
          // SigningKey takes any Uint8Array; SubtleCrypto takes one backed by
          // an ArrayBuffer. Every caller here passes the latter.
          await crypto.subtle.digest(
            'SHA-256',
            /** @type {Uint8Array<ArrayBuffer>} */ (data),
          ),
        );
        // Low-S compact r||s, the shape atproto verifies.
        return secp256k1
          .sign(hash, privateKey, { lowS: true })
          .toCompactRawBytes();
      },
      async publicKey() {
        return publicKey;
      },
    },
  };
}

/**
 * @param {ReturnType<typeof generateIdentity>} identity
 * @param {string} handle
 * @param {string} pdsUrl
 */
export async function createPlcOperation(identity, handle, pdsUrl) {
  const operation = await signPlcOperationObject(
    buildPlcOperation({
      didKey: identity.didKey,
      kemDidKey: identity.kemDidKey,
      handle,
      pdsEndpoint: pdsUrl,
    }),
    identity.signer,
  );
  // The DID covers the signature, so it is derived from the signed operation.
  const digest = new Uint8Array(
    await crypto.subtle.digest(
      'SHA-256',
      /** @type {Uint8Array<ArrayBuffer>} */ (cborEncodeDagCbor(operation)),
    ),
  );
  const did = `did:plc:${base32Encode(digest.slice(0, 15))}`;
  return { operation, did };
}