// did:key encoding/decoding for the two curves atproto accepts.
//
// did:key:z<base58btc( <multicodec-varint> ++ <compressed-pubkey> )>
//
// Multicodec key-type prefixes (unsigned-varint):
// secp256k1-pub = 0xe7 -> varint [0xe7, 0x01] -> did:key:zQ3s...
// p256-pub = 0x1200 -> varint [0x80, 0x24] -> did:key:zDna...
//
// The pubkey is the 33-byte SEC1 compressed point (0x02/0x03 ++ x). This matches
// @atproto/crypto's formatDidKey. Validated by the P-256 golden round-trip test.
import { base58btc } from 'multiformats/bases/base58'
export const CURVE_SECP256K1 = 'secp256k1'
export const CURVE_P256 = 'p256'
const MULTICODEC = {
[CURVE_SECP256K1]: 0xe7,
[CURVE_P256]: 0x1200
}
const BY_CODE = { 0xe7: CURVE_SECP256K1, 0x1200: CURVE_P256 }
function varintEncode (n) {
const out = []
while (n >= 0x80) { out.push((n & 0x7f) | 0x80); n >>>= 7 }
out.push(n)
return Uint8Array.from(out)
}
function varintDecode (bytes) {
let n = 0, shift = 0, i = 0
for (;;) {
const b = bytes[i++]
n |= (b & 0x7f) << shift
if ((b & 0x80) === 0) break
shift += 7
}
return { code: n >>> 0, length: i }
}
// compressed pubkey bytes (33) -> did:key string
export function encodeDidKey (curve, compressedPubkey) {
const code = MULTICODEC[curve]
if (code === undefined) throw new Error(`unsupported curve: ${curve}`)
const prefix = varintEncode(code)
const bytes = new Uint8Array(prefix.length + compressedPubkey.length)
bytes.set(prefix, 0)
bytes.set(compressedPubkey, prefix.length)
return `did:key:${base58btc.encode(bytes)}` // base58btc.encode already prepends 'z'
}
// did:key string -> { curve, pubkey (compressed bytes) }
export function decodeDidKey (didKey) {
if (!didKey.startsWith('did:key:')) throw new Error('not a did:key')
const mb = didKey.slice('did:key:'.length)
const bytes = base58btc.decode(mb) // expects leading 'z'
const { code, length } = varintDecode(bytes)
const curve = BY_CODE[code]
if (!curve) throw new Error(`unknown multicodec 0x${code.toString(16)}`)
return { curve, pubkey: bytes.slice(length) }
}