@synet/did
Version:
Secure, minimal, standards-compliant DID library for production environments. Supports did:key and did:web methods with strict validation and cryptographic security.
301 lines (300 loc) • 9.85 kB
JavaScript
;
/**
*
* @synet/did - Production DID Library
*
* Secure, minimal, standards-compliant DID creation for production environments.
* Supports only did:key and did:web methods following W3C DID Core specification.
* Stable, maintained and robust.
*
* Security Features:
* - No cryptographic fallbacks to weak sources
* - Strict input validation and sanitization
* - Minimal attack surface
* - Standards-compliant multicodec encoding
*
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.createDIDKey = createDIDKey;
exports.createDIDWeb = createDIDWeb;
exports.createDID = createDID;
exports.createDIDDocument = createDIDDocument;
const types_1 = require("./types");
const utils_1 = require("./utils");
/**
* Official multicodec codes from https://github.com/multiformats/multicodec
*/
const MULTICODEC_CODES = {
"ed25519-pub": 0xed, // 237 - Ed25519 public key
"secp256k1-pub": 0xe7, // 231 - Secp256k1 public key (compressed)
"x25519-pub": 0xec, // 236 - Curve25519 public key
};
/**
* Base58 alphabet (Bitcoin/IPFS standard)
*/
const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
/**
* Secure base58 encoding implementation
*/
function encodeBase58(bytes) {
if (bytes.length === 0)
return "";
// Convert to big integer
let value = 0n;
for (const byte of bytes) {
value = value * 256n + BigInt(byte);
}
// Convert to base58
const digits = [];
while (value > 0) {
const remainder = Number(value % 58n);
digits.unshift(BASE58_ALPHABET[remainder]);
value = value / 58n;
}
// Handle leading zeros
for (const byte of bytes) {
if (byte !== 0)
break;
digits.unshift("1");
}
return digits.join("");
}
/**
* Encode an unsigned integer using varint (unsigned LEB128) encoding
*/
function encodeVarint(value) {
const bytes = [];
let remaining = value;
while (remaining >= 0x80) {
bytes.push((remaining & 0x7f) | 0x80);
remaining >>>= 7;
}
bytes.push(remaining & 0x7f);
return new Uint8Array(bytes);
}
/**
* Create multibase-encoded string with multicodec prefix
*/
function encodeMultibase(keyBytes, keyType) {
const codecCode = MULTICODEC_CODES[keyType];
// Use varint encoding for the multicodec prefix as per standards
const codecBytes = encodeVarint(codecCode);
// Combine multicodec prefix with key bytes
const combined = new Uint8Array(codecBytes.length + keyBytes.length);
combined.set(codecBytes, 0);
combined.set(keyBytes, codecBytes.length);
// Return base58btc encoded (prefix 'z')
return `z${encodeBase58(combined)}`;
}
/**
* Validate hex string format
*/
function validateHexString(hex) {
// Remove 0x prefix if present
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
// Validate hex format
if (!/^[0-9a-fA-F]+$/.test(clean)) {
throw new types_1.DIDError("Invalid hexadecimal format");
}
// Ensure even length
if (clean.length % 2 !== 0) {
throw new types_1.DIDError("Hexadecimal string must have even length");
}
return clean;
}
/**
* Convert hex string to Uint8Array
*/
function hexToBytes(hex) {
const clean = validateHexString(hex);
const bytes = new Uint8Array(clean.length / 2);
for (let i = 0; i < clean.length; i += 2) {
bytes[i / 2] = Number.parseInt(clean.substr(i, 2), 16);
}
return bytes;
}
/**
* Validate key length for specific key type
*/
function validateKeyLength(keyBytes, keyType) {
const expectedLengths = {
"ed25519-pub": [32], // Ed25519 is always 32 bytes
"secp256k1-pub": [33, 65], // Compressed (33) or uncompressed (65)
"x25519-pub": [32], // X25519 is always 32 bytes
};
const allowed = expectedLengths[keyType];
if (!allowed.includes(keyBytes.length)) {
throw new types_1.DIDError(`Invalid key length for ${keyType}: expected ${allowed.join(" or ")} bytes, got ${keyBytes.length}`);
}
}
/**
* Validate domain for did:web
*/
function validateDomain(domain) {
if (!domain || typeof domain !== "string") {
throw new types_1.DIDError("Domain is required");
}
// Basic domain validation
if (domain.includes("://") || domain.includes(" ")) {
throw new types_1.DIDError("Invalid domain format");
}
// Must contain at least one dot (reject localhost, etc.)
if (!domain.includes(".")) {
throw new types_1.DIDError("Domain must be a valid FQDN");
}
// Basic length check
if (domain.length > 253) {
throw new types_1.DIDError("Domain too long");
}
}
/**
* Create a did:key DID from a public key
*
* @param publicKeyHex - Public key in hexadecimal format
* @param keyType - Type of cryptographic key (legacy names supported)
* @returns Standards-compliant did:key DID
*/
function createDIDKey(publicKeyHex, keyType = "ed25519-pub") {
if (!publicKeyHex || typeof publicKeyHex !== "string") {
throw new types_1.DIDError("Public key is required");
}
// Map legacy key types to new format
const keyTypeMap = {
Ed25519: "ed25519-pub",
secp256k1: "secp256k1-pub",
X25519: "x25519-pub",
"ed25519-pub": "ed25519-pub",
"secp256k1-pub": "secp256k1-pub",
"x25519-pub": "x25519-pub",
};
const normalizedKeyType = keyTypeMap[keyType];
if (!normalizedKeyType) {
throw new types_1.DIDError(`Unsupported key type: ${keyType}`);
}
try {
const keyBytes = hexToBytes(publicKeyHex);
validateKeyLength(keyBytes, normalizedKeyType);
const multibaseId = encodeMultibase(keyBytes, normalizedKeyType);
return `did:key:${multibaseId}`;
}
catch (error) {
if (error instanceof types_1.DIDError) {
throw error;
}
throw new types_1.DIDError(`Failed to create did:key: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
/**
* Create a did:web DID
*
* @param domain - Domain name for the DID
* @param path - Optional path component
* @returns Standards-compliant did:web DID
*/
function createDIDWeb(domain, path) {
validateDomain(domain);
// Encode domain (replace : with %3A for ports)
const encodedDomain = domain.replace(/:/g, "%3A");
let identifier = encodedDomain;
if (path) {
// Validate and encode path
if (typeof path !== "string") {
throw new types_1.DIDError("Path must be a string");
}
// Replace forward slashes with colons per did:web spec
const encodedPath = path.replace(/\//g, ":");
identifier += `:${encodedPath}`;
}
return `did:web:${identifier}`;
}
/**
* Create a DID using the specified method
*
* @param options - DID creation options
* @returns DID string
*/
function createDID(options) {
switch (options.method) {
case "key": {
if (!options.publicKey) {
throw new types_1.DIDError("publicKey is required for did:key");
}
return createDIDKey(options.publicKey, options.keyType);
}
case "web": {
if (!options.domain) {
throw new types_1.DIDError("domain is required for did:web");
}
return createDIDWeb(options.domain, options.path);
}
default:
throw new types_1.DIDError(`Unsupported DID method: ${options.method}`);
}
}
/**
* Create a basic DID document for a given DID
*
* @param did - DID string
* @param options - Optional document options
* @returns DID document
*/
function createDIDDocument(did, options = {}) {
const validation = (0, utils_1.validateDID)(did);
if (!validation.isValid) {
throw new types_1.DIDError(`Invalid DID: ${validation.error}`);
}
const { "@context": contextOption, controller, verificationMethod, authentication, assertionMethod, keyAgreement, capabilityInvocation, capabilityDelegation, service, mediaType, } = options;
const document = {
id: did,
controller: controller || did,
};
// Only add @context for JSON-LD or when explicitly specified
if (contextOption !== undefined) {
document["@context"] = contextOption;
}
else if (mediaType === "application/did+ld+json" ||
mediaType === undefined) {
// Default to JSON-LD context unless explicitly requesting plain JSON
document["@context"] = [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/suites/ed25519-2020/v1",
];
}
// Add verification method if provided
if (verificationMethod) {
document.verificationMethod = Array.isArray(verificationMethod)
? verificationMethod
: [verificationMethod];
}
// Add capability sections if provided
if (authentication) {
document.authentication = authentication;
}
if (assertionMethod) {
document.assertionMethod = assertionMethod;
}
if (keyAgreement) {
document.keyAgreement = keyAgreement;
}
if (capabilityInvocation) {
document.capabilityInvocation = capabilityInvocation;
}
if (capabilityDelegation) {
document.capabilityDelegation = capabilityDelegation;
}
// Add services if provided
if (service && service.length > 0) {
document.service = service.map((svc) => {
// If the service id is a fragment (starts with #), make it fully qualified
if (svc.id.startsWith("#")) {
return {
...svc,
id: `${did}${svc.id}`,
};
}
// Otherwise, keep the service as is
return svc;
});
}
return document;
}