From 83186de1195b25a9945567d78ec4eb924e64c745 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Tue, 20 Jan 2026 11:26:27 +0100 Subject: [PATCH] Remove a bunch of magic numbers and strings. --- src/Ed25519Keypair.ts | 10 +++--- src/cli/did-rotation-key-check.ts | 8 ++--- src/did-validation.ts | 46 ++++++++++++++++++--------- src/domain.ts | 9 ++++-- src/keyfile.ts | 18 +++++++++-- src/keys.ts | 12 ++++--- src/metadata.ts | 45 ++++++++++++++++++-------- src/signing.ts | 52 +++++++++++++++++++++---------- src/verify.ts | 2 +- 9 files changed, 141 insertions(+), 61 deletions(-) diff --git a/src/Ed25519Keypair.ts b/src/Ed25519Keypair.ts index 45d4fa7..a3ddd1a 100644 --- a/src/Ed25519Keypair.ts +++ b/src/Ed25519Keypair.ts @@ -1,4 +1,5 @@ import { bytesToMultibase, multibaseToBytes, Keypair } from '@atproto/crypto'; +import { DID_KEY_PREFIX } from './did-validation.js'; import { ed25519 } from '@noble/curves/ed25519'; import * as uint8arrays from 'uint8arrays'; @@ -8,10 +9,9 @@ import * as uint8arrays from 'uint8arrays'; export const ED25519_PUBLIC_PREFIX = new Uint8Array([0xed, 0x01]); /** - * Multibase base58btc prefix for Ed25519 public keys (z6Mk). - * This is the base58btc encoding of ED25519_PUBLIC_PREFIX. + * Length of an Ed25519 public key in bytes. */ -export const ED25519_PUBLIC_MULTIBASE_PREFIX = 'z6Mk'; +const ED25519_PUBLIC_KEY_SIZE = 32; /** * Ed25519 verification keypair. @@ -56,7 +56,7 @@ export class Ed25519Keypair implements Keypair { */ static async fromPublicKeyMultibase(publicKeyMultibase: string): Promise { const decoded = multibaseToBytes(publicKeyMultibase); - const expectedLength = ED25519_PUBLIC_PREFIX.length + 32; + const expectedLength = ED25519_PUBLIC_PREFIX.length + ED25519_PUBLIC_KEY_SIZE; // Validate minimum length before accessing array indices if (decoded.length < ED25519_PUBLIC_PREFIX.length) { @@ -106,7 +106,7 @@ export class Ed25519Keypair implements Keypair { * Get the public key as a did:key string. */ did(): string { - return `did:key:${this.publicKeyStr()}`; + return `${DID_KEY_PREFIX}${this.publicKeyStr()}`; } /** diff --git a/src/cli/did-rotation-key-check.ts b/src/cli/did-rotation-key-check.ts index 4738da1..fa6f7a9 100644 --- a/src/cli/did-rotation-key-check.ts +++ b/src/cli/did-rotation-key-check.ts @@ -3,7 +3,7 @@ import { parseArgs } from 'node:util'; import { readFile } from 'node:fs/promises'; import { getRotationPublicKeyMultibase, parseRotationPublicKeyOnly, RotationKeyInputError } from '../keys.js'; -import { validatePlcDid, DidValidationError } from '../did-validation.js'; +import { validatePlcDid, DidValidationError, DID_KEY_PREFIX } from '../did-validation.js'; import { checkRotationKey, DidLogFetchError, DidLogValidationError } from '../verify.js'; const { values } = parseArgs({ @@ -131,16 +131,16 @@ if (result.allKeys.length === 0) { if (result.valid) { console.log(`\n✓ Rotation key is valid`); - console.log(`Public key: did:key:${result.publicKeyMultibase}`); + console.log(`Public key: ${DID_KEY_PREFIX}${result.publicKeyMultibase}`); console.log(`This key can be used to sign PLC operations for ${did}`); process.exit(0); } else { console.log(`\n❌ Rotation key is not valid`); - console.log(`Public key: did:key:${result.publicKeyMultibase}`); + console.log(`Public key: ${DID_KEY_PREFIX}${result.publicKeyMultibase}`); console.log(`This key is not present in the latest operation of the DID log for ${did}`); console.log(`\nValid rotation keys for this DID:`); for (const key of result.allKeys) { - console.log(` did:key:${key}`); + console.log(` ${DID_KEY_PREFIX}${key}`); } process.exit(1); } diff --git a/src/did-validation.ts b/src/did-validation.ts index 8861fa6..9dda87a 100644 --- a/src/did-validation.ts +++ b/src/did-validation.ts @@ -8,28 +8,45 @@ export class DidValidationError extends Error {} */ export class PublicKeyValidationError extends Error {} +/** + * Prefix for did:plc URIs. + */ +export const DID_PLC_PREFIX = 'did:plc:'; + /** * Expected length of a valid did:plc: identifier. * Format: did:plc: (8 chars) + 24 character base32 hash = 32 characters total. */ -const DID_PLC_LENGTH = 32; +export const DID_PLC_LENGTH = 32; /** * Prefix for did:key URIs. */ export const DID_KEY_PREFIX = 'did:key:'; +/** + * Multibase prefix for Ed25519 public keys (verification keys). + * Format: z6Mk... + */ +export const ED25519_PUBLIC_MULTIBASE_PREFIX = 'z6Mk'; + +/** + * Multibase prefix for Secp256k1 public keys (rotation keys). + * Format: zQ3sh... + */ +export const SECP256K1_PUBLIC_MULTIBASE_PREFIX = 'zQ3sh'; + /** * Prefix for Ed25519 public keys in did:key format (verification keys). * Format: did:key:z6Mk... */ -export const ED25519_DID_KEY_PREFIX = 'did:key:z6Mk'; +export const ED25519_DID_KEY_PREFIX = DID_KEY_PREFIX + ED25519_PUBLIC_MULTIBASE_PREFIX; /** * Prefix for Secp256k1 public keys in did:key format (rotation keys). * Format: did:key:zQ3sh... */ -export const SECP256K1_DID_KEY_PREFIX = 'did:key:zQ3sh'; +export const SECP256K1_DID_KEY_PREFIX = DID_KEY_PREFIX + SECP256K1_PUBLIC_MULTIBASE_PREFIX; /** * Expected length of a did:key Ed25519 public key. @@ -44,15 +61,14 @@ export const ED25519_DID_KEY_LENGTH = 56; export const SECP256K1_DID_KEY_LENGTH = 57; /** - * Multibase prefix for Secp256k1 public keys (rotation keys). - * Format: zQ3sh... + * Multicodec prefix for secp256k1 compressed public keys (rotation keys). */ -export const SECP256K1_PUBLIC_MULTIBASE_PREFIX = 'zQ3sh'; +export const SECP256K1_PUBLIC_MULTICODEC_PREFIX = new Uint8Array([0xe7, 0x01]); /** - * Multicodec prefix for secp256k1 compressed public keys (rotation keys). + * Length of a secp256k1 compressed public key in bytes. */ -export const SECP256K1_PUBLIC_MULTICODEC_PREFIX = new Uint8Array([0xe7, 0x01]); +export const SECP256K1_COMPRESSED_PUBLIC_KEY_SIZE = 33; /** * Multicodec prefix for secp256k1 private keys (rotation keys). @@ -71,8 +87,8 @@ export const ED25519_PRIVATE_MULTICODEC_PREFIX = new Uint8Array([0x80, 0x26]); * @throws {DidValidationError} If the DID doesn't start with 'did:plc:' or has incorrect length */ export function validatePlcDid(did: string): void { - if (!did.startsWith('did:plc:')) { - throw new DidValidationError(`Invalid DID format. DID must have the prefix 'did:plc:'.`); + if (!did.startsWith(DID_PLC_PREFIX)) { + throw new DidValidationError(`Invalid DID format. DID must have the prefix '${DID_PLC_PREFIX}'.`); } if (did.length !== DID_PLC_LENGTH) { throw new DidValidationError(`Invalid DID format. DID must be ${DID_PLC_LENGTH} characters in length.`); @@ -86,8 +102,10 @@ export function validatePlcDid(did: string): void { * @throws {PublicKeyValidationError} If the key format is invalid */ export function validateVerificationKey(key: string): void { - if (!key.startsWith('did:key:')) { - throw new PublicKeyValidationError(`Invalid verification key format. Key must start with 'did:key:' prefix.`); + if (!key.startsWith(DID_KEY_PREFIX)) { + throw new PublicKeyValidationError( + `Invalid verification key format. Key must start with '${DID_KEY_PREFIX}' prefix.`, + ); } if (key.startsWith(SECP256K1_DID_KEY_PREFIX)) { @@ -116,8 +134,8 @@ export function validateVerificationKey(key: string): void { * @throws {PublicKeyValidationError} If the key format is invalid */ export function validateRotationKey(key: string): void { - if (!key.startsWith('did:key:')) { - throw new PublicKeyValidationError(`Invalid rotation key format. Key must start with 'did:key:' prefix.`); + if (!key.startsWith(DID_KEY_PREFIX)) { + throw new PublicKeyValidationError(`Invalid rotation key format. Key must start with '${DID_KEY_PREFIX}' prefix.`); } if (key.startsWith(ED25519_DID_KEY_PREFIX)) { diff --git a/src/domain.ts b/src/domain.ts index 2379e44..93c52dd 100644 --- a/src/domain.ts +++ b/src/domain.ts @@ -1,5 +1,10 @@ import { resolveTxt } from 'node:dns/promises'; +/** + * Maximum length of a domain name per DNS specification. + */ +const MAX_DOMAIN_LENGTH = 255; + const DOMAIN_REGEX = /^[a-z0-9][a-z0-9-]{0,62}(\.[a-z0-9][a-z0-9-]{0,62})+$/i; const DID_RECORD_REGEX = /^did="?([^"]+)"?$/; @@ -29,8 +34,8 @@ export function validateDomain(domain: string): void { throw new InvalidDomainError('Domain is required'); } - if (domain.length > 255) { - throw new InvalidDomainError('Domain must not exceed 255 characters'); + if (domain.length > MAX_DOMAIN_LENGTH) { + throw new InvalidDomainError(`Domain must not exceed ${MAX_DOMAIN_LENGTH} characters`); } if (!DOMAIN_REGEX.test(domain)) { diff --git a/src/keyfile.ts b/src/keyfile.ts index 4d9760c..2e3ba16 100644 --- a/src/keyfile.ts +++ b/src/keyfile.ts @@ -43,6 +43,16 @@ interface FormatKeyFileContentOptions { */ export class SaveKeyError extends Error {} +/** + * Size of the uncompressed public key prefix byte (0x04). + */ +const UNCOMPRESSED_KEY_PREFIX_SIZE = 1; + +/** + * Size of a secp256k1 coordinate (X or Y) in bytes. + */ +const SECP256K1_COORDINATE_SIZE = 32; + /** * Encodes a rotation key (secp256k1) as a PEM string in SEC1 format. * @@ -51,8 +61,12 @@ export class SaveKeyError extends Error {} */ export function encodeRotationKey(privateKey: Uint8Array): string { const uncompressedPublicKey = secp256k1.getPublicKey(privateKey, false); - const publicKeyX = uncompressedPublicKey.slice(1, 33); - const publicKeyY = uncompressedPublicKey.slice(33, 65); + // Uncompressed format: 0x04 prefix + 32-byte X + 32-byte Y + const xStart = UNCOMPRESSED_KEY_PREFIX_SIZE; + const xEnd = xStart + SECP256K1_COORDINATE_SIZE; + const yEnd = xEnd + SECP256K1_COORDINATE_SIZE; + const publicKeyX = uncompressedPublicKey.slice(xStart, xEnd); + const publicKeyY = uncompressedPublicKey.slice(xEnd, yEnd); const keyObject = crypto.createPrivateKey({ key: { diff --git a/src/keys.ts b/src/keys.ts index 0413d16..2b63dd6 100644 --- a/src/keys.ts +++ b/src/keys.ts @@ -1,10 +1,14 @@ import { Keypair, Secp256k1Keypair, verifySignature, multibaseToBytes } from '@atproto/crypto'; import { ed25519 } from '@noble/curves/ed25519'; -import { Ed25519Keypair, ED25519_PUBLIC_MULTIBASE_PREFIX } from './Ed25519Keypair.js'; +import { Ed25519Keypair } from './Ed25519Keypair.js'; import { DID_KEY_PREFIX, + ED25519_PUBLIC_MULTIBASE_PREFIX, + ED25519_DID_KEY_PREFIX, SECP256K1_PUBLIC_MULTIBASE_PREFIX, + SECP256K1_DID_KEY_PREFIX, SECP256K1_PUBLIC_MULTICODEC_PREFIX, + SECP256K1_COMPRESSED_PUBLIC_KEY_SIZE, } from './did-validation.js'; export interface KeyPairBundle { @@ -203,7 +207,7 @@ export async function getVerificationPublicKeyMultibase(keyInput: string): Promi } throw new VerificationKeyInputError( - 'Unrecognized key format. Expected a public key (did:key:z6Mk...) or private key (PEM, multibase, or hex)', + `Unrecognized key format. Expected a public key (${ED25519_DID_KEY_PREFIX}...) or private key (PEM, multibase, or hex)`, ); } @@ -237,7 +241,7 @@ function validateSecp256k1PublicKeyMultibase(multibase: string): void { } // Validate total length (2-byte prefix + 33-byte compressed public key = 35 bytes) - const expectedLength = SECP256K1_PUBLIC_MULTICODEC_PREFIX.length + 33; + const expectedLength = SECP256K1_PUBLIC_MULTICODEC_PREFIX.length + SECP256K1_COMPRESSED_PUBLIC_KEY_SIZE; if (decoded.length !== expectedLength) { throw new RotationKeyInputError( `Invalid key length: expected ${expectedLength} bytes (2-byte prefix + 33-byte key), ` + @@ -323,6 +327,6 @@ export async function getRotationPublicKeyMultibase(keyInput: string): Promise { const { fetchDidLog } = await import('./plc-log.js'); - const didKey = `did:key:${publicKeyMultibase}`; + const didKey = `${DID_KEY_PREFIX}${publicKeyMultibase}`; const ops = await fetchDidLog(did, plcUrl); if (ops.length === 0) {