@instun/sm2-multikey
Version:
A JavaScript library for generating and working with SM2Multikey key pairs and digital signatures. Compatible with both Node.js and fibjs runtimes.
339 lines (324 loc) • 9.15 kB
JavaScript
/*!
* Copyright (c) 2024 Instun, Inc. All rights reserved.
*/
/**
* @fileoverview Multicodec and Multibase Key Encoding Utilities
*
* This module implements the Multicodec and Multibase specifications for
* SM2 cryptographic key encoding. It provides a self-describing format
* that combines type information with key data, enabling reliable key
* identification and handling.
*
* Key Features:
* - Multicodec compliance
* - Multibase support
* - Zero-copy operations
* - Type preservation
* - Format validation
*
* Security Considerations:
* - Key type validation
* - Format verification
* - Memory safety
* - Error handling
* - Buffer bounds
*
* Performance Notes:
* - Minimal copying
* - Early validation
* - Efficient encoding
* - Buffer reuse
* - Type preservation
*
* Key Format Structure:
* ```
* | Multibase | Multicodec | Key Data |
* |-----------|------------|------------|
* | 'z' | 0x8624 | Public Key |
* | 'z' | 0x9026 | Secret Key |
*
* Example:
* Public: z86240123... (Base58BTC('z') + SM2-pub(0x8624) + key)
* Private: z90260123... (Base58BTC('z') + SM2-priv(0x9026) + key)
* ```
*
* Standards Compliance:
* - Multicodec v1.0
* - Multibase v1.0
* - IPFS CID v1
* - GM/T 0009-2012
* - GB/T 32918.1-2016
*
* Common Applications:
* - Key storage
* - Key exchange
* - IPFS content
* - Key identification
* - Format conversion
*
* Usage Example:
* ```javascript
* import {
* encodeKey,
* decodeKey,
* MULTICODEC_SM2_PUB_HEADER
* } from './codec.js';
*
* // Encode a public key
* const publicKey = new Uint8Array([1, 2, 3, 4]); // Example bytes
* const encoded = encodeKey(MULTICODEC_SM2_PUB_HEADER, publicKey);
* console.log(encoded); // 'z8624...'
*
* // Decode and validate
* const { key, prefix } = decodeKey(encoded);
* if (prefix.equals(MULTICODEC_SM2_PUB_HEADER)) {
* console.log('Valid SM2 public key');
* }
* ```
*
* @module formats/codec
* @see {@link https://github.com/multiformats/multicodec|Multicodec}
* @see {@link https://github.com/multiformats/multibase|Multibase}
* @see {@link https://github.com/multiformats/cid|CID}
*/
import { base58btc } from 'multiformats/bases/base58';
import { ArgumentError, KeyError, SM2Error, ErrorCodes } from '../core/errors.js';
import { isValidBinaryData, toBuffer, matchBinaryType } from '../utils/binary.js';
/**
* Multiformat Constants
*
* These constants define the prefixes and headers used in the multiformat
* encoding scheme. They follow the official multicodec and multibase
* specifications for consistent key identification.
*
* Format Details:
* ```
* Multibase:
* 'z' - Base58BTC encoding prefix
*
* Multicodec (varint encoded):
* 0x8624 - SM2 public key (0x1206)
* 0x9026 - SM2 private key (0x1310)
* ```
*
* @constant
* @readonly
*/
export const MULTIBASE_BASE58BTC_HEADER = 'z';
/**
* SM2 Public Key Multicodec Header
*
* This constant defines the multicodec prefix for SM2 public keys.
* The value 0x8624 is the varint encoding of code 0x1206.
*
* Header Structure:
* ```
* | 1st byte | 2nd byte |
* | 0x86 | 0x24 |
* | cont.bit | value |
* ```
*
* @constant {Uint8Array}
* @readonly
*/
export const MULTICODEC_SM2_PUB_HEADER = new Uint8Array([0x86, 0x24]);
/**
* SM2 Private Key Multicodec Header
*
* This constant defines the multicodec prefix for SM2 private keys.
* The value 0x9026 is the varint encoding of code 0x1310.
*
* Header Structure:
* ```
* | 1st byte | 2nd byte |
* | 0x90 | 0x26 |
* | cont.bit | value |
* ```
*
* @constant {Uint8Array}
* @readonly
*/
export const MULTICODEC_SM2_PRIV_HEADER = new Uint8Array([0x90, 0x26]);
/**
* Encode a cryptographic key with multiformat prefixes
*
* This function implements the multiformat encoding scheme, combining
* multicodec and multibase specifications to create a self-describing
* key format. It ensures proper type identification and encoding.
*
* Processing Steps:
* 1. Input validation
* 2. Type verification
* 3. Prefix assembly
* 4. Key concatenation
* 5. Base58BTC encoding
*
* Security Considerations:
* - Key validation
* - Prefix verification
* - Buffer safety
* - Memory bounds
* - Type checking
*
* Performance Notes:
* - Single allocation
* - Early validation
* - Efficient encoding
* - Buffer reuse
* - Type preservation
*
* Encoding Process:
* ```
* Input: prefix=[0x86,0x24] key=[k1,k2,...]
* Step 1: Validate inputs
* Step 2: Combine [prefix|key]
* Step 3: Add multibase prefix
* Output: "z" + base58btc([prefix|key])
* ```
*
* @param {Uint8Array} prefix - Multicodec prefix for key type
* @param {Buffer|Uint8Array} key - Raw key data to encode
* @returns {string} Multiformat encoded key string
* @throws {ArgumentError} If inputs are invalid
*
* @example
* ```javascript
* // Encode a public key
* const pubKey = Buffer.from([
* 0x04, // Uncompressed point
* ...new Array(63).fill(0) // X and Y coordinates
* ]);
* const encoded = encodeKey(MULTICODEC_SM2_PUB_HEADER, pubKey);
* console.log(encoded); // 'z8624...'
*
* // Encode a private key
* const privKey = Buffer.from(new Array(32).fill(0));
* const encodedPriv = encodeKey(MULTICODEC_SM2_PRIV_HEADER, privKey);
* console.log(encodedPriv); // 'z9026...'
* ```
*/
export function encodeKey(prefix, key) {
if (!isValidBinaryData(key)) {
throw new ArgumentError('Invalid key', {
code: ErrorCodes.ERR_ARGUMENT_INVALID,
details: 'Key must be a Buffer or Uint8Array'
});
}
// Convert key to Buffer for concatenation
const keyBuf = toBuffer(key);
const data = Buffer.concat([Buffer.from(prefix), keyBuf]);
return base58btc.encode(data);
}
/**
* Decode a multiformat encoded key string
*
* This function implements the multiformat decoding process, handling
* both multicodec and multibase aspects. It performs thorough validation
* to ensure key integrity and proper format compliance.
*
* Processing Steps:
* 1. Input validation
* 2. Multibase verification
* 3. Base58BTC decoding
* 4. Multicodec extraction
* 5. Key separation
*
* Security Considerations:
* - Format validation
* - Prefix verification
* - Buffer safety
* - Error handling
* - Type checking
*
* Performance Notes:
* - Minimal copying
* - Early validation
* - Efficient decoding
* - Buffer reuse
* - Type preservation
*
* Decoding Process:
* ```
* Input: "z8624k1k2k3..."
* Step 1: Validate multibase prefix ('z')
* Step 2: Base58BTC decode
* Step 3: Extract multicodec prefix
* Step 4: Validate prefix
* Step 5: Extract key data
* Output: {
* prefix: [0x86,0x24],
* key: [k1,k2,k3,...]
* }
* ```
*
* @param {string} encoded - Multiformat encoded key string
* @param {Buffer|Uint8Array} [outputType] - Optional type to match output format
* @returns {{key: Buffer|Uint8Array, prefix: Buffer}} Decoded key and prefix
* @throws {ArgumentError} If input is invalid
* @throws {KeyError} If key format is invalid
*
* @example
* ```javascript
* // Decode and validate a public key
* try {
* const { key, prefix } = decodeKey('z8624...');
*
* if (prefix.equals(MULTICODEC_SM2_PUB_HEADER)) {
* console.log('Valid SM2 public key');
* console.log('Length:', key.length); // 64 bytes
* }
* } catch (err) {
* if (err instanceof KeyError) {
* console.error('Invalid key format');
* }
* }
*
* // Decode with type matching
* const uint8 = new Uint8Array();
* const { key } = decodeKey('z8624...', uint8);
* console.log(key instanceof Uint8Array); // true
* ```
*/
export function decodeKey(encoded, outputType) {
if (!encoded || typeof encoded !== 'string') {
throw new ArgumentError('Invalid encoded key', {
code: ErrorCodes.ERR_ARGUMENT_INVALID,
details: 'Encoded key must be a non-empty string'
});
}
if (outputType && !isValidBinaryData(outputType)) {
throw new ArgumentError('Invalid output type', {
code: ErrorCodes.ERR_ARGUMENT_INVALID,
details: 'Output type must be a Buffer or Uint8Array'
});
}
if (!encoded.startsWith(MULTIBASE_BASE58BTC_HEADER)) {
throw new KeyError('Invalid SM2 key prefix', { code: ErrorCodes.ERR_KEY_FORMAT_NEW });
}
try {
const data = base58btc.decode(encoded);
if (data.length < 2) {
throw new KeyError('Invalid key length', { code: ErrorCodes.ERR_KEY_FORMAT_NEW });
}
const prefix = Buffer.from(data.subarray(0, 2));
const key = Buffer.from(data.subarray(2));
// Verify prefix
if (!prefix.equals(MULTICODEC_SM2_PUB_HEADER) &&
!prefix.equals(MULTICODEC_SM2_PRIV_HEADER)) {
throw new KeyError('Invalid SM2 key prefix', { code: ErrorCodes.ERR_KEY_FORMAT_NEW });
}
// Match output type if specified
return {
key: outputType ? matchBinaryType(outputType, key) : key,
prefix
};
} catch (error) {
if (error instanceof SM2Error) {
throw error;
}
throw new KeyError('Invalid SM2 public key prefix', {
code: ErrorCodes.ERR_KEY_FORMAT_NEW,
cause: error
});
}
}