A CLI tool and JavaScript library for creating keys and
encoding strings. Exposes a command-line interface (crypt) and a
Typescript API.
Install globally:
npm i -g @substrate-system/cryptOr install locally, and run with npx:
npm i -D @substrate-system/crypt
npx crypt keysFor Ed25519 keys, this will print a JSON string with
{ publicKey, privateKey } to stdout. By default (format raw):
publicKeyis in multikey formatprivateKeyis the base64url-encoded seed
For X25519 keys (used for key exchange/ECDH), output is the same as Ed25519:
publicKeyis base64url-encodedprivateKeyis the base64url-encoded seed
For RSA keys, the private key is written to a file (requires -o option)
and the public key is printed to stdout in multikey format.
You can pass in a different encoding to use for the output with
the -f or --format option.
# Generate Ed25519 keypair (outputs both keys to stdout)
npx crypt keys
# Generate X25519 keypair for key exchange
npx crypt keys x25519
# Generate RSA keypair, write private key to file
npx crypt keys rsa -o private.pem
# Generate RSA keypair in JWK format (outputs both keys to stdout)
npx crypt keys rsa -f jwk
# Sign a message
npx crypt sign "my document" -k <private-key-seed>
# Sign from stdin
echo "my document" | npx crypt sign -k <private-key-seed>
# Convert between encoding formats via stdin
echo "Hello World" | npx crypt encode base64
# => SGVsbG8gV29ybGQ
echo "SGVsbG8gV29ybGQ" | npx crypt encode utf8 -i base64
# => Hello World
echo "SGVsbG8gV29ybGQ" | npx crypt encode hex -i base64
# => 48656c6c6f20576f726c64npx crypt --help
# Show help for a specific command
npx crypt keys --help
npx crypt encode --helpGenerate a new cryptographic keypair, by default ed25519.
Ed25519 keys (signing): Output is a JSON string of { publicKey, privateKey }
to stdout, by default (-f raw). The public key is in multikey format and
the private key is a base64url-encoded seed. Use -f jwk for JWK format.
If -o is specified, the private key is written to the file and only the
public key is printed to stdout.
X25519 keys (key exchange/ECDH): Similar to Ed25519, outputs
{ publicKey, privateKey } to stdout. By default (-f raw), both keys are
base64url-encoded. Use -f jwk for JWK format.
RSA keys (signing): Requires -o option to specify output file for the private key
(in PKCS#8 PEM format), unless using -f jwk which outputs both keys as
JSON to stdout.
keyType- The key type to use (default:ed25519)ed25519- Ed25519 elliptic curve (signing)x25519- X25519 elliptic curve (key exchange/ECDH)rsa- RSA-PSS 2048-bit (signing)
-
-f, --format- Output format (default:raw)raw- (default) For Ed25519: public key in multikey format, private key as base64url-encoded seed. For X25519: both keys as base64url-encoded strings. For RSA: requires-ooption.jwk- JSON Web Key format (outputs private key JWK, which includes public key inxfield for Ed25519 and X25519)
-
-o, --output- Output file for private key- Required for RSA (unless using
-f jwk) - Optional for Ed25519 and X25519 (if specified, private key goes to file instead of stdout)
- RSA private keys are written as PKCS#8 PEM format
- Ed25519 and X25519 private keys are written as base64url-encoded seed (default)
or JWK format (if using
-f jwk)
- Required for RSA (unless using
-
-u, --use- Key usage for RSA keys (default:sign)sign- (default) Generate RSA-PSS key for signingexchange- Generate RSA-OAEP key for encryption/key exchange- Only applies to RSA keys; ignored for Ed25519 and X25519
# Generate Ed25519 keypair (default: raw format)
npx crypt keys
# => {"publicKey":"z6Mk...", "privateKey":"mZ5t7zw8D..."}
# (publicKey is multikey format, privateKey is base64url-encoded seed)
# Generate Ed25519 keypair in JWK format
npx crypt keys -f jwk
# => {"key_ops":["sign"],"ext":true,"crv":"Ed25519","d":"...","x":"...","kty":"OKP","alg":"Ed25519"}
# (Returns private key JWK; public key is in the 'x' field)
# Generate Ed25519 keypair, save private key to file
npx crypt keys -o private.txt
# => {"publicKey":"z6Mk..."}
# (private.txt contains the base64url-encoded seed)
# Generate X25519 keypair for key exchange (default: raw format)
npx crypt keys x25519
# => {"publicKey":"B1UIJS3JEVkjr7uP1E1JWQ...","privateKey":"WCHnh8mcwxZ89Urp_i-F..."}
# (both keys are base64url-encoded)
# Generate X25519 keypair in JWK format
npx crypt keys x25519 -f jwk
# => {"key_ops":["deriveKey","deriveBits"],"ext":true,"crv":"X25519","d":"...","x":"...","kty":"OKP"}
# (Returns private key JWK; public key is in the 'x' field)
# Generate RSA signing keypair, save private key to file as PEM
npx crypt keys rsa -o private.pem
# => {"publicKey":"z5Tc..."}
# (private.pem contains PKCS#8 PEM format for RSA-PSS)
# Generate RSA encryption keypair with --use exchange
npx crypt keys rsa -f jwk -u exchange
# => {...JWK with private key...}
# (Returns private key JWK for RSA-OAEP)
# Generate RSA keypair in JWK format (signing by default)
npx crypt keys rsa -f jwk
# => {...JWK with private key...}
# (Returns private key JWK; public key components are included)Sign a message with an Ed25519 private key. The message can be provided as a positional argument or read from stdin.
message- The message to sign (optional if using stdin)- If omitted, the message will be read from stdin
- If provided as an argument, that takes precedence over stdin
-k, --key- (required) Private key seed (base64url-encoded)- The private key seed as output by
crypt keysin raw format - This is the 'd' field from an Ed25519 JWK
- The private key seed as output by
# Generate a keypair first
npx crypt keys
# => {"publicKey":"z6Mk...","privateKey":"k7s0h9nK5oqRd4ip..."}
# Sign a message using positional argument
npx crypt sign "my signed document" -k k7s0h9nK5oqRd4ip_K8ow2RXZ4p3B5Mp3hguz7G9CMI
# => jLYPhT1LAckU3WrcXlAPf4eaklfk8qTDyBf1otgqmr7Fx-YATTrZrLrlHYvNyl0EU5SF6URiKJtkyjeRGNe9AA
# Sign a message from stdin
echo "my signed document" | npx crypt sign -k k7s0h9nK5oqRd4ip_K8ow2RXZ4p3B5Mp3hguz7G9CMI
# => jLYPhT1LAckU3WrcXlAPf4eaklfk8qTDyBf1otgqmr7Fx-YATTrZrLrlHYvNyl0EU5SF6URiKJtkyjeRGNe9AA
# Sign a file
cat document.txt | npx crypt sign -k k7s0h9nK5oqRd4ip_K8ow2RXZ4p3B5Mp3hguz7G9CMI
# Complete workflow: generate keys and sign
PRIVATE_KEY=$(npx crypt keys | jq -r .privateKey)
echo "important message" | npx crypt sign -k $PRIVATE_KEYThe signature is output as a base64url-encoded string (Ed25519 signatures are 64 bytes, encoded as 86 characters).
Convert a string from one encoding format to another. Reads input from stdin.
output-format- The desired output format (default:base58btc)base58btc,base64,hex,base64url,utf8,ascii
-i, --input-format- The format of the input string (default:utf8)base64,hex,base64url,base58btc,utf8,ascii
# Encode UTF-8 text to base58btc (default)
echo "Hello World" | npx crypt encode
# Encode UTF-8 to base64
echo "Hello World" | npx crypt encode base64
# Convert base64 to hex
echo "SGVsbG8gV29ybGQ=" | npx crypt encode hex -i base64
# Convert hex to base58btc
echo "48656c6c6f" | npx crypt encode base58btc -i hex
# Pipe between commands (publicKey is in multikey format)
npx crypt keys | jq -r .publicKey | npx crypt encode hex -i multi
# convert "multikey" format to hex format
echo "z6MkiLr..." | npx crypt encode hex -i multiDecode a string from a given encoding format to UTF-8. Reads input from stdin.
input-format- The format of the input string (default:base64)base64,base64pad,hex,base64url,base58btc,ascii
# Decode base64 to UTF-8 (default)
echo "SGVsbG8gV29ybGQ=" | npx crypt decode
# Decode base64pad to UTF-8
echo "SGVsbG8gV29ybGQ=" | npx crypt decode base64pad
# => 'Hello World'
# Decode hex to UTF-8
echo "48656c6c6f20576f726c64" | npx crypt decode hex
# Decode base58btc to UTF-8
echo "JxF12TrwUP45BMd" | npx crypt decode base58btcThis package exposes a JavaScript API too.
npm i -S @substrate-system/cryptimport { keys, sign, encode, decode } from '@substrate-system/crypt'
// Or import everything
import * as crypt from '@substrate-system/crypt'Generate a new cryptographic keypair.
async function keys (args:{
keyType?:'ed25519'|'x25519'|'rsa',
format?:'raw'|'jwk',
use?:'sign'|'exchange'
} = {}):Promise<{
publicKey:string|object,
privateKey?:string|object,
privateKeyPem?:string
}>options(object, optional):keyType(ed25519,rsaorx25519; default:ed25519)format('raw' | 'jwk', default:'raw') - Output formatuse('sign' | 'exchange', default:'sign') - Key usage for RSA keys'sign'- Generate RSA-PSS key for signing'exchange'- Generate RSA-OAEP key for encryption/key exchange- Ignored for Ed25519 and X25519 keys
useMultibase(boolean, default:false) - Add multibase prefix (currently not functional for multikey format)
- For
rawformat Ed25519/X25519:{ publicKey: string, privateKey: string } - For
rawformat RSA:{ publicKey: string, privateKeyPem: string } - For
jwkformat: Returns the JWK object directly
Example:
import { keys } from '@substrate-system/crypt'
// Generate Ed25519 keypair (default)
const keypair = await keys()
console.log(keypair.publicKey) // z6Mk... (multikey format)
console.log(keypair.privateKey) // base64url-encoded seed
// Generate X25519 keypair
const x25519Keys = await keys({ keyType: 'x25519' })
// Generate RSA signing keypair in JWK format
const rsaJwk = await keys({ keyType: 'rsa', format: 'jwk' })
// Generate RSA encryption keypair
const rsaEncryptJwk = await keys({ keyType: 'rsa', format: 'jwk', use: 'exchange' })async function sign (
message:string,
options:{ key:string }
):Promise<string>Sign a message with a private key. Supports both Ed25519 and RSA keys.
message(string) - The message to signoptions(object):key(string, required) - Private key in one of these formats:- Ed25519: base64url-encoded seed
- RSA: PEM-encoded private key (PKCS#8 format)
Base64url-encoded signature.
import { keys, sign } from '@substrate-system/crypt'
// Sign with Ed25519 key
const ed25519Keypair = await keys()
const ed25519Sig = await sign('Hello World', { key: ed25519Keypair.privateKey })
console.log(ed25519Sig) // Base64url-encoded signature
// Sign with RSA key
const rsaKeypair = await keys({ keyType: 'rsa' })
const rsaSig = await sign('Hello World', { key: rsaKeypair.privateKeyPem })
console.log(rsaSig) // Base64url-encoded signatureConvert a string from one encoding format to another.
async function encode (
input:string,
options:{
inputFormat?:u.SupportedEncodings|'multi',
outputFormat:u.SupportedEncodings|'multi',
useMultibase?:boolean,
keyType?:'ed25519'|'rsa'
}
):Promise<string>input(string) - The input string to encodeoptions(object):inputFormat(u.SupportedEncodings|'multi', default:'utf8')outputFormat(u.SupportedEncodings|'multi', required) - Output formatuseMultibase(boolean, default:false) - Add multibase prefixkeyType('ed25519'|'rsa', optional) - Required if output is'multi'
Supported encodings: 'base64', 'hex', 'base64url', 'base58btc',
'utf8', 'ascii', 'multi'
The encoded string
import { encode } from '@substrate-system/crypt'
// Encode UTF-8 to base64
const encoded = await encode('Hello World', {
inputFormat: 'utf8',
outputFormat: 'base64'
})
// Convert hex to multikey format
const multikey = await encode('1234...', {
inputFormat: 'hex',
outputFormat: 'multi',
keyType: 'ed25519'
})Decode a string from a given format to UTF-8.
async function decode (
input:string,
options:{ inputFormat:u.SupportedEncodings }
):Promise<string>input(string) - The input string to decodeoptions(object):inputFormat(u.SupportedEncodings, required) - Input format
The decoded UTF-8 string
import { decode } from '@substrate-system/crypt'
const decoded = await decode('SGVsbG8gV29ybGQ=', {
inputFormat: 'base64'
})
console.log(decoded) // 'Hello World'This uses Multikey format strings. Multikey format, is a generic, self-describing, multicodec-based public key encoding.
import { base58btc } from 'multiformats/bases/base58'
import * as varint from "multiformats/src/varint"
// Suppose you have a raw public-key Buffer/Uint8Array
const rawKeyBytes = /* ... */
// --- helper: varint-encoded multicodec prefix for ed25519-pub ---
// multicodec code for ed25519-pub is 0xED (237).
// varint encoding for 237 is two bytes: 0xED 0x01
const ED25519_MULTICODEC_VARINT = Uint8Array.from([0xed, 0x01])
const out = new Uint8Array(ED25519_MULTICODEC_VARINT.length + rawPubKey.length)
out.set(ED25519_MULTICODEC_VARINT, 0)
out.set(rawKeyBytes, ED25519_MULTICODEC_VARINT.length)
// base58btc (multibase) encode
// multiformats' base58btc.encode typically returns a string that already
// uses the 'z'
let encoded = base58btc.encode(out)
encoded = encoded.startsWith('z') ? encoded : 'z' + encoded
// This yields something like "z6Mk…", same style as in the DID doc
console.log(encoded)
/**
* Decode a Multikey multibase string (ed25519-pub) back to raw key bytes.
* Returns an object { algCode, rawKey } where algCode is the multicodec
* numeric code.
*/
function decodeMultikey (multibaseStr):{ } {
// Accept with/without leading 'z' — multiformats will accept
// without the explicit 'z' only if decoder used directly.
const cleaned = (multibaseStr.startsWith('z') ?
multibaseStr :
'z' + multibaseStr)
const decoded = base58btc.decode(cleaned) // returns Uint8Array
// read varint (we know ed25519 varint is two bytes: 0xed 0x01)
// robust approach: parse varint; here we handle single or two-byte
// varints for small values
let i = 0
let code = 0
let shift = 0
while (i < decoded.length) {
const b = decoded[i++]
code |= (b & 0x7f) << shift
if ((b & 0x80) === 0) break
shift += 7
}
const rawKey = decoded.slice(i) // remainder is the raw key bytes
return { multicodec: code, rawKey }
}Private keys are either a "raw" 32 byte string, or JWK encoded, or PEM format for RSA.