Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f23d0a609
|
||
|
|
49f3442e5c
|
||
|
|
b39241c840
|
||
|
|
74e1e0cc6a
|
||
|
|
e55352aa4e
|
||
|
|
f7dc72e32c
|
||
|
|
506b77351c
|
||
|
|
2bffbc62bc
|
||
|
|
c616a0d949
|
||
|
|
6ed8b1ea6d
|
||
|
|
9281daa14c | ||
|
|
c7ed69fe55 | ||
|
|
22a1246672 | ||
|
|
4a5b6acc78 |
@@ -13,3 +13,12 @@ logFilters:
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.9.1.cjs
|
||||
|
||||
# @quantus packages come from the Gitea registry. Safe as a whole-scope route
|
||||
# because we own every name in it — unlike @polkadot, where routing the scope
|
||||
# would send yarn looking for dozens of unforked packages that are not there.
|
||||
#
|
||||
# Reads are anonymous; publishing needs `pass gitea/package-publisher-token`.
|
||||
npmScopes:
|
||||
quantus:
|
||||
npmRegistryServer: "https://git.lair.cafe/api/packages/quantus/npm/"
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 14.0.3 Mar 23, 2026
|
||||
|
||||
Changes:
|
||||
|
||||
- Fix(hw-ledger): reset transport on operation errors and add explicit disconnect API ([#2024](https://github.com/polkadot-js/common/pull/2024))
|
||||
|
||||
|
||||
## 14.0.2 Mar 16, 2026
|
||||
|
||||
Changes:
|
||||
|
||||
+2
-1
@@ -1,5 +1,5 @@
|
||||
1674 Jaco Bump deps (#1912)
|
||||
46 Tarik Gul 14.0.2 (#2023)
|
||||
47 Tarik Gul 14.0.3 (#2025)
|
||||
13 Carlo Sala feat(networks): add zeitgeist Ledger support (#1840)
|
||||
11 Valentin Fernandez 13.5.7 (#2008)
|
||||
9 Antoine Estienne Add eth uri keyring test (#1291)
|
||||
@@ -32,6 +32,7 @@
|
||||
1 Chris Get rid of warnings for dual esm and cjs packages of the same version (#1890)
|
||||
1 Dmitrii Novikov Add `Vara` support to Generic Ledger App (#1958)
|
||||
1 Eliott Teissonniere add nodle ledger specs (#1089)
|
||||
1 Francis O'Brien Fix(hw-ledger): reset transport on operation errors and add explicit disconnect API (#2024)
|
||||
1 Guilane DENGUIR feat(VTB): Added support for VTB blockchain (#1651)
|
||||
1 h4x3rotab Throws an error when signing with a locked keypair (#578)
|
||||
1 Harry Liu Update README.md (#446)
|
||||
|
||||
+3
-3
@@ -14,10 +14,10 @@
|
||||
},
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"version": "14.0.2",
|
||||
"version": "14.0.3",
|
||||
"versions": {
|
||||
"git": "14.0.2",
|
||||
"npm": "14.0.2"
|
||||
"git": "14.0.3",
|
||||
"npm": "14.0.3"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
},
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"version": "14.0.2",
|
||||
"version": "14.0.3",
|
||||
"browser": "browser.js",
|
||||
"main": "node.js",
|
||||
"react-native": "react-native.js",
|
||||
@@ -22,7 +22,7 @@
|
||||
"@ledgerhq/hw-transport": "^6.31.4",
|
||||
"@ledgerhq/hw-transport-webhid": "^6.29.4",
|
||||
"@ledgerhq/hw-transport-webusb": "^6.29.4",
|
||||
"@polkadot/util": "14.0.2",
|
||||
"@polkadot/util": "14.0.3",
|
||||
"tslib": "^2.8.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
|
||||
// Do not edit, auto-generated by @polkadot/dev
|
||||
|
||||
export const packageInfo = { name: '@polkadot/hw-ledger-transports', path: 'auto', type: 'auto', version: '14.0.2' };
|
||||
export const packageInfo = { name: '@polkadot/hw-ledger-transports', path: 'auto', type: 'auto', version: '14.0.3' };
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
"./packageDetect.cjs"
|
||||
],
|
||||
"type": "module",
|
||||
"version": "14.0.2",
|
||||
"version": "14.0.3",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@polkadot/hw-ledger-transports": "14.0.2",
|
||||
"@polkadot/util": "14.0.2",
|
||||
"@polkadot/hw-ledger-transports": "14.0.3",
|
||||
"@polkadot/util": "14.0.3",
|
||||
"@zondax/ledger-substrate": "1.1.1",
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { SubstrateApp } from '@zondax/ledger-substrate';
|
||||
import type { TransportDef, TransportType } from '@polkadot/hw-ledger-transports/types';
|
||||
import type { Transport, TransportDef, TransportType } from '@polkadot/hw-ledger-transports/types';
|
||||
import type { AccountOptions, LedgerAddress, LedgerSignature, LedgerVersion } from './types.js';
|
||||
|
||||
import { newSubstrateApp } from '@zondax/ledger-substrate';
|
||||
@@ -30,6 +30,17 @@ async function wrapError <T extends WrappedResult> (promise: Promise<T>): Promis
|
||||
return result;
|
||||
}
|
||||
|
||||
/** @internal Best-effort transport cleanup after failed operations */
|
||||
async function closeTransport (transport: Transport | null): Promise<void> {
|
||||
if (transport) {
|
||||
try {
|
||||
await transport.close();
|
||||
} catch {
|
||||
// Ignore cleanup errors so the original failure is preserved.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal Wraps a sign/signRaw call and returns the associated signature */
|
||||
function sign (method: 'sign' | 'signRaw', message: Uint8Array, accountOffset = 0, addressOffset = 0, { account = LEDGER_DEFAULT_ACCOUNT, addressIndex = LEDGER_DEFAULT_INDEX, change = LEDGER_DEFAULT_CHANGE }: Partial<AccountOptions> = {}): (app: SubstrateApp) => Promise<LedgerSignature> {
|
||||
return async (app: SubstrateApp): Promise<LedgerSignature> => {
|
||||
@@ -114,16 +125,29 @@ export class Ledger {
|
||||
return this.withApp(sign('signRaw', u8aWrapBytes(message), accountOffset, addressOffset, options));
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes any active transport connection
|
||||
*/
|
||||
public async disconnect (): Promise<void> {
|
||||
const app = this.#app;
|
||||
|
||||
this.#app = null;
|
||||
|
||||
await closeTransport(app?.transport || null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* Returns a created SubstrateApp to perform operations against. Generally
|
||||
* this is only used internally, to ensure consistent bahavior.
|
||||
* this is only used internally, to ensure consistent behavior.
|
||||
*/
|
||||
async withApp <T> (fn: (app: SubstrateApp) => Promise<T>): Promise<T> {
|
||||
let transport: Transport | null = null;
|
||||
|
||||
try {
|
||||
if (!this.#app) {
|
||||
const transport = await this.#transportDef.create();
|
||||
transport = await this.#transportDef.create();
|
||||
|
||||
// We need this override for the actual type passing - the Deno environment
|
||||
// is quite a bit stricter and it yields invalids between the two (specifically
|
||||
@@ -136,6 +160,7 @@ export class Ledger {
|
||||
|
||||
return await fn(this.#app);
|
||||
} catch (error) {
|
||||
await closeTransport(this.#app?.transport || transport);
|
||||
this.#app = null;
|
||||
|
||||
throw error;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2017-2026 @polkadot/hw-ledger authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { TransportDef, TransportType } from '@polkadot/hw-ledger-transports/types';
|
||||
import type { Transport, TransportDef, TransportType } from '@polkadot/hw-ledger-transports/types';
|
||||
import type { AccountOptionsGeneric, LedgerAddress, LedgerSignature, LedgerVersion } from './types.js';
|
||||
|
||||
import { PolkadotGenericApp } from '@zondax/ledger-substrate';
|
||||
@@ -33,7 +33,7 @@ async function wrapError <T extends WrappedResult> (promise: Promise<T>): Promis
|
||||
try {
|
||||
result = await promise;
|
||||
} catch (e: unknown) {
|
||||
// We check to see if the propogated error is the newer ResponseError type.
|
||||
// We check to see if the propagated error is the newer ResponseError type.
|
||||
// The response code use to be part of the result, but with the latest breaking changes from 0.42.x
|
||||
// the interface and it's types have completely changed.
|
||||
if ((e as ResponseError).returnCode) {
|
||||
@@ -46,6 +46,17 @@ async function wrapError <T extends WrappedResult> (promise: Promise<T>): Promis
|
||||
return result;
|
||||
}
|
||||
|
||||
/** @internal Best-effort transport cleanup after failed operations */
|
||||
async function closeTransport (transport: Transport | null): Promise<void> {
|
||||
if (transport) {
|
||||
try {
|
||||
await transport.close();
|
||||
} catch {
|
||||
// Ignore cleanup errors so the original failure is preserved.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal Wraps a signEd25519/signRawEd25519 call and returns the associated signature */
|
||||
function sign (method: 'signEd25519' | 'signRawEd25519', message: Uint8Array, slip44: number, accountIndex = 0, addressOffset = 0): (app: PolkadotGenericApp) => Promise<LedgerSignature> {
|
||||
const bip42Path = `m/44'/${slip44}'/${accountIndex}'/${0}'/${addressOffset}'`;
|
||||
@@ -245,6 +256,17 @@ export class LedgerGeneric {
|
||||
return this.withApp(signWithMetadataEcdsa(message, this.#slip44, accountIndex, addressOffset, options));
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes any active transport connection
|
||||
*/
|
||||
public async disconnect (): Promise<void> {
|
||||
const app = this.#app;
|
||||
|
||||
this.#app = null;
|
||||
|
||||
await closeTransport(app?.transport || null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
@@ -252,9 +274,11 @@ export class LedgerGeneric {
|
||||
* this is only used internally, to ensure consistent bahavior.
|
||||
*/
|
||||
async withApp <T> (fn: (app: PolkadotGenericApp) => Promise<T>): Promise<T> {
|
||||
let transport: Transport | null = null;
|
||||
|
||||
try {
|
||||
if (!this.#app) {
|
||||
const transport = await this.#transportDef.create();
|
||||
transport = await this.#transportDef.create();
|
||||
|
||||
// We need this override for the actual type passing - the Deno environment
|
||||
// is quite a bit stricter and it yields invalids between the two (specifically
|
||||
@@ -267,6 +291,7 @@ export class LedgerGeneric {
|
||||
|
||||
return await fn(this.#app);
|
||||
} catch (error) {
|
||||
await closeTransport(this.#app?.transport || transport);
|
||||
this.#app = null;
|
||||
|
||||
throw error;
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
|
||||
// Do not edit, auto-generated by @polkadot/dev
|
||||
|
||||
export const packageInfo = { name: '@polkadot/hw-ledger', path: 'auto', type: 'auto', version: '14.0.2' };
|
||||
export const packageInfo = { name: '@polkadot/hw-ledger', path: 'auto', type: 'auto', version: '14.0.3' };
|
||||
|
||||
@@ -18,15 +18,16 @@
|
||||
"./packageDetect.cjs"
|
||||
],
|
||||
"type": "module",
|
||||
"version": "14.0.2",
|
||||
"version": "14.0.3-quantus.2",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@polkadot/util": "14.0.2",
|
||||
"@polkadot/util-crypto": "14.0.2",
|
||||
"@polkadot/util": "14.0.3",
|
||||
"@polkadot/util-crypto": "14.0.3-quantus.2",
|
||||
"@quantus/crypto": "^0.1.0",
|
||||
"tslib": "^2.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@polkadot/util": "14.0.2",
|
||||
"@polkadot/util-crypto": "14.0.2"
|
||||
"@polkadot/util": "14.0.3",
|
||||
"@polkadot/util-crypto": "14.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,25 @@ import type { EncryptedJsonEncoding, Keypair, KeypairType } from '@polkadot/util
|
||||
import type { KeyringInstance, KeyringOptions, KeyringPair, KeyringPair$Json, KeyringPair$Meta } from './types.js';
|
||||
|
||||
import { hexToU8a, isHex, stringToU8a } from '@polkadot/util';
|
||||
import { base64Decode, decodeAddress, ed25519PairFromSeed as ed25519FromSeed, encodeAddress, ethereumEncode, hdEthereum, keyExtractSuri, keyFromPath, mnemonicToLegacySeed, mnemonicToMiniSecret, secp256k1PairFromSeed as secp256k1FromSeed, sr25519PairFromSeed as sr25519FromSeed } from '@polkadot/util-crypto';
|
||||
import { base64Decode, decodeAddress, dilithiumPairFromMnemonic, dilithiumPairFromSeed, dilithiumPathFromSuri, ed25519PairFromSeed as ed25519FromSeed, encodeAddress, ethereumEncode, hdEthereum, isDilithium, keyExtractSuri, keyFromPath, mnemonicToLegacySeed, mnemonicToMiniSecret, secp256k1PairFromSeed as secp256k1FromSeed, sr25519PairFromSeed as sr25519FromSeed } from '@polkadot/util-crypto';
|
||||
|
||||
import { createPair } from './pair/index.js';
|
||||
import { DEV_PHRASE } from './defaults.js';
|
||||
import { Pairs } from './pairs.js';
|
||||
|
||||
/**
|
||||
* Every keypair type this keyring will construct.
|
||||
*
|
||||
* Was two separate inline arrays, both of which a widened `KeypairType` silently
|
||||
* walked past — the union is compile-time and these checks are not. Named once so
|
||||
* the next change to the union has one place to look, and so the error message
|
||||
* cannot drift from the check.
|
||||
*/
|
||||
const KEYPAIR_TYPES: KeypairType[] = ['dilithium65', 'dilithium87', 'ecdsa', 'ed25519', 'ethereum', 'sr25519'];
|
||||
|
||||
const PairFromSeed = {
|
||||
dilithium65: (seed: Uint8Array): Keypair => dilithiumPairFromSeed(seed, 'dilithium65'),
|
||||
dilithium87: (seed: Uint8Array): Keypair => dilithiumPairFromSeed(seed, 'dilithium87'),
|
||||
ecdsa: (seed: Uint8Array): Keypair => secp256k1FromSeed(seed),
|
||||
ed25519: (seed: Uint8Array): Keypair => ed25519FromSeed(seed),
|
||||
ethereum: (seed: Uint8Array): Keypair => secp256k1FromSeed(seed),
|
||||
@@ -50,8 +62,11 @@ export class Keyring implements KeyringInstance {
|
||||
constructor (options: KeyringOptions = {}) {
|
||||
options.type = options.type || 'ed25519';
|
||||
|
||||
if (!['ecdsa', 'ethereum', 'ed25519', 'sr25519'].includes(options.type || 'undefined')) {
|
||||
throw new Error(`Expected a keyring type of either 'ed25519', 'sr25519', 'ethereum' or 'ecdsa', found '${options.type || 'unknown'}`);
|
||||
// A runtime whitelist behind the KeypairType union, so widening the type
|
||||
// alone was not enough — the compiler cannot see this one, and it fails at
|
||||
// construction rather than at use.
|
||||
if (!KEYPAIR_TYPES.includes(options.type || ('undefined' as KeypairType))) {
|
||||
throw new Error(`Expected a keyring type of one of ${KEYPAIR_TYPES.map((t) => `'${t}'`).join(', ')}, found '${options.type || 'unknown'}'`);
|
||||
}
|
||||
|
||||
this.#pairs = new Pairs();
|
||||
@@ -175,19 +190,35 @@ export class Keyring implements KeyringInstance {
|
||||
? [type]
|
||||
: type;
|
||||
|
||||
if (!['ed25519', 'sr25519', 'ecdsa', 'ethereum'].includes(cryptoType)) {
|
||||
// Same list, second copy — this one guards JSON coming in from a file the
|
||||
// user chose, so it is the guard that will matter most once quantus/common#6
|
||||
// narrows the union and imports of quantum-unsafe keys have to be refused
|
||||
// with a message explaining why rather than "unknown crypto type".
|
||||
if (!KEYPAIR_TYPES.includes(cryptoType as KeypairType)) {
|
||||
throw new Error(`Unknown crypto type ${cryptoType}`);
|
||||
}
|
||||
|
||||
// Here the address and publicKey are 32 bytes and isomorphic. This is why the address field needs to be the public key for ethereum type pairs
|
||||
const publicKey = isHex(address)
|
||||
// For the curve schemes the address and publicKey are 32 bytes and
|
||||
// isomorphic, which is why the address field holds the public key for
|
||||
// ethereum pairs and why this works at all.
|
||||
const raw = isHex(address)
|
||||
? hexToU8a(address)
|
||||
: this.decodeAddress(address, ignoreChecksum);
|
||||
const decoded = isHex(encoded)
|
||||
? hexToU8a(encoded)
|
||||
: base64Decode(encoded);
|
||||
|
||||
return createPair({ toSS58: this.encodeAddress, type: cryptoType as KeypairType }, { publicKey, secretKey: new Uint8Array() }, meta, decoded, encType);
|
||||
// ML-DSA breaks the isomorphism: the account id is a one-way Poseidon2 hash
|
||||
// and the 1952/2592-byte public key is inside `decoded`, which is encrypted
|
||||
// and stays that way until someone supplies a password. So pass the account
|
||||
// id as an account id rather than pretending it is a key — createPair uses
|
||||
// it for the address while locked, and checks it against the real public key
|
||||
// the moment `decodePkcs8` produces one.
|
||||
const info = isDilithium(cryptoType as KeypairType)
|
||||
? { accountId: raw, publicKey: new Uint8Array(), secretKey: new Uint8Array() }
|
||||
: { publicKey: raw, secretKey: new Uint8Array() };
|
||||
|
||||
return createPair({ toSS58: this.encodeAddress, type: cryptoType as KeypairType }, info, meta, decoded, encType);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -212,6 +243,33 @@ export class Keyring implements KeyringInstance {
|
||||
let seed: Uint8Array;
|
||||
const isPhraseHex = isHex(phrase, 256);
|
||||
|
||||
// ML-DSA derives from the mnemonic itself along a hardened BIP44 path, not
|
||||
// from a seed along a junction chain — lattice keys have no public
|
||||
// derivability, so there is no soft junction to emulate and the chain's own
|
||||
// Pair::derive refuses for the same reason. Handled before the seeding below
|
||||
// because that seeding (mnemonicToMiniSecret) is the wrong one for us and
|
||||
// would produce a valid key for an account nobody owns.
|
||||
if (isDilithium(type)) {
|
||||
if (isPhraseHex) {
|
||||
// A raw 32-byte seed goes straight into keygen, which is how the
|
||||
// dev-genesis accounts are defined. Combining one with a derivation path
|
||||
// is ambiguous — is the seed the master, or already derived? — so refuse
|
||||
// rather than pick.
|
||||
if (derivePath) {
|
||||
throw new Error('A derivation path cannot be combined with a raw seed for post-quantum pairs');
|
||||
}
|
||||
|
||||
return createPair({ toSS58: this.encodeAddress, type }, PairFromSeed[type](hexToU8a(phrase)), meta, null);
|
||||
}
|
||||
|
||||
return createPair(
|
||||
{ toSS58: this.encodeAddress, type },
|
||||
dilithiumPairFromMnemonic(phrase, password || '', dilithiumPathFromSuri(type, derivePath), type),
|
||||
meta,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
if (isPhraseHex) {
|
||||
seed = hexToU8a(phrase);
|
||||
} else {
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
|
||||
// Do not edit, auto-generated by @polkadot/dev
|
||||
|
||||
export const packageInfo = { name: '@polkadot/keyring', path: 'auto', type: 'auto', version: '14.0.2' };
|
||||
export const packageInfo = { name: '@polkadot/keyring', path: 'auto', type: 'auto', version: '14.0.3-quantus.2' };
|
||||
|
||||
@@ -16,8 +16,20 @@ const SEED_OFFSET = PAIR_HDR.length;
|
||||
* For divisor/headers, don't rely on the magic being static. These will
|
||||
* change between generations, aka with the long-awaited 4th generation
|
||||
* of the format. The external decode interface is the only way to use and decode these.
|
||||
*
|
||||
* `secretLength` exists for keys that are not 32 or 64 bytes. The body is
|
||||
* `HDR ‖ secretKey ‖ DIV ‖ publicKey`, and upstream located the divider by
|
||||
* trying the two lengths every curve scheme uses. An ML-DSA secret is 4032 or
|
||||
* 4896 bytes, so neither offset matches and the decode throws "Invalid encoding
|
||||
* divider found in body" — a stored account that cannot be read back.
|
||||
*
|
||||
* The caller passes the length rather than this function searching for the
|
||||
* divider. Searching would work almost always and fail catastrophically when it
|
||||
* did not: PAIR_DIV is five bytes, so a 4032-byte secret contains a false match
|
||||
* about once in 270 million keys, and the result would be a silently wrong key
|
||||
* rather than an error. The caller knows the type, so it knows the length.
|
||||
**/
|
||||
export function decodePair (passphrase?: string, encrypted?: Uint8Array | null, _encType?: EncryptedJsonEncoding | EncryptedJsonEncoding[]): { publicKey: Uint8Array; secretKey: Uint8Array } {
|
||||
export function decodePair (passphrase?: string, encrypted?: Uint8Array | null, _encType?: EncryptedJsonEncoding | EncryptedJsonEncoding[], secretLength?: number): { publicKey: Uint8Array; secretKey: Uint8Array } {
|
||||
const encType = Array.isArray(_encType) || _encType === undefined
|
||||
? _encType
|
||||
: [_encType];
|
||||
@@ -29,6 +41,23 @@ export function decodePair (passphrase?: string, encrypted?: Uint8Array | null,
|
||||
throw new Error('Invalid encoding header found in body');
|
||||
}
|
||||
|
||||
// an explicitly-sized secret (ML-DSA); there is one offset and it either
|
||||
// matches or the blob is not what the caller said it was
|
||||
if (secretLength) {
|
||||
const divOffset = SEED_OFFSET + secretLength;
|
||||
|
||||
if (!u8aEq(decrypted.subarray(divOffset, divOffset + PAIR_DIV.length), PAIR_DIV)) {
|
||||
throw new Error('Invalid encoding divider found in body');
|
||||
}
|
||||
|
||||
return {
|
||||
// the public key is the remainder: its length varies by scheme and the
|
||||
// body ends here, so there is nothing to read past it
|
||||
publicKey: decrypted.subarray(divOffset + PAIR_DIV.length),
|
||||
secretKey: decrypted.subarray(SEED_OFFSET, divOffset)
|
||||
};
|
||||
}
|
||||
|
||||
// setup for generation 3 format
|
||||
let secretKey = decrypted.subarray(SEED_OFFSET, SEED_OFFSET + SEC_LENGTH);
|
||||
let divOffset = SEED_OFFSET + SEC_LENGTH;
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
// Copyright 2017-2026 @polkadot/keyring authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/// <reference types="@polkadot/dev-test/globals.d.ts" />
|
||||
|
||||
import { contextForSpec, Scheme, sizes } from '@quantus/crypto';
|
||||
|
||||
import { dilithiumVerify } from '@polkadot/util-crypto';
|
||||
|
||||
import { Keyring } from '../keyring.js';
|
||||
|
||||
// crystal_alice, from the chain's dev genesis: a raw 32-byte seed straight into
|
||||
// ML-DSA-87 keygen, no HD derivation. The expected address is what
|
||||
// `quantus developer create-test-wallets` prints — an independent implementation,
|
||||
// not this one.
|
||||
const ALICE_ADDRESS = 'qzk1Nxai3dZD9Cn5kwGcgL6mKxsfxwqdis7kDQJ52aJS2vSn7';
|
||||
|
||||
const MESSAGE = new Uint8Array([0x74, 0x68, 0x65, 0x20, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64]);
|
||||
|
||||
describe('dilithium pairs', (): void => {
|
||||
const keyring = new Keyring({ ss58Format: 189, type: 'dilithium87' });
|
||||
const alice = keyring.addFromSeed(new Uint8Array(32), { name: 'crystal_alice' }, 'dilithium87');
|
||||
const context = contextForSpec(148);
|
||||
const s87 = sizes(Scheme.MlDsa87);
|
||||
const s65 = sizes(Scheme.MlDsa65);
|
||||
|
||||
it('derives the address quantus-cli derives', (): void => {
|
||||
expect(alice.address).toEqual(ALICE_ADDRESS);
|
||||
});
|
||||
|
||||
// The assumption this whole fork exists to break. Everywhere else in this
|
||||
// package `addressRaw` and `publicKey` are the same 32 bytes.
|
||||
it('has an addressRaw that is not the publicKey', (): void => {
|
||||
expect(alice.addressRaw.length).toEqual(32);
|
||||
expect(alice.publicKey.length).toEqual(s87.publicKey);
|
||||
});
|
||||
|
||||
it('signs to the wire form the runtime reads', (): void => {
|
||||
const signature = alice.sign(MESSAGE, { context });
|
||||
|
||||
expect(signature.length).toEqual(s87.signatureWithPublicKey);
|
||||
});
|
||||
|
||||
// `withType` prepends the signature enum's variant index. For the curve types
|
||||
// that enum is Substrate's MultiSignature; here it is the runtime's
|
||||
// DilithiumSignatureScheme, reached by the same mechanism.
|
||||
it('prepends the chain variant byte with withType', (): void => {
|
||||
const signed87 = alice.sign(MESSAGE, { context, withType: true });
|
||||
const k65 = keyring.addFromSeed(new Uint8Array(32).fill(9), {}, 'dilithium65');
|
||||
const signed65 = k65.sign(MESSAGE, { context, withType: true });
|
||||
|
||||
expect(signed87[0]).toEqual(0);
|
||||
expect(signed87.length).toEqual(s87.signatureWithPublicKey + 1);
|
||||
expect(signed65[0]).toEqual(1);
|
||||
expect(signed65.length).toEqual(s65.signatureWithPublicKey + 1);
|
||||
});
|
||||
|
||||
it('verifies against the account id', (): void => {
|
||||
const signature = alice.sign(MESSAGE, { context });
|
||||
|
||||
expect(dilithiumVerify(MESSAGE, signature, alice.addressRaw, 'dilithium87', context)).toEqual(true);
|
||||
});
|
||||
|
||||
// The spec-148 boundary. A signature made under the wrong context is
|
||||
// cryptographically valid and rejected by the chain, so this is the only place
|
||||
// it can be caught locally.
|
||||
it('does not verify under the wrong context', (): void => {
|
||||
const signature = alice.sign(MESSAGE, { context });
|
||||
|
||||
expect(dilithiumVerify(MESSAGE, signature, alice.addressRaw, 'dilithium87', contextForSpec(147))).toEqual(false);
|
||||
});
|
||||
|
||||
// Verification checks the embedded public key hashes to the account being
|
||||
// verified against, not merely that the signature is valid for *some* key.
|
||||
it('does not verify another account signature', (): void => {
|
||||
const bob = keyring.addFromSeed(new Uint8Array(32).fill(1), {}, 'dilithium87');
|
||||
const signature = bob.sign(MESSAGE, { context });
|
||||
|
||||
expect(dilithiumVerify(MESSAGE, signature, alice.addressRaw, 'dilithium87', context)).toEqual(false);
|
||||
});
|
||||
|
||||
it('refuses to sign without a context', (): void => {
|
||||
expect(() => alice.sign(MESSAGE)).toThrow(/signing context is required/);
|
||||
});
|
||||
|
||||
// There is no ML-DSA VRF, and faking one from an ordinary signature would
|
||||
// produce output that looks like a VRF proof and cannot be verified as one.
|
||||
it('refuses to VRF sign', (): void => {
|
||||
expect(() => alice.vrfSign(MESSAGE)).toThrow(/not available for dilithium87/);
|
||||
});
|
||||
|
||||
// The classical paths must be untouched — this is what a careless rebase
|
||||
// breaks, and it would break silently.
|
||||
it('leaves sr25519 unchanged', (): void => {
|
||||
const sr = new Keyring({ type: 'sr25519' }).addFromUri('//Alice');
|
||||
|
||||
expect(sr.address).toEqual('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY');
|
||||
expect(sr.sign(MESSAGE).length).toEqual(64);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright 2017-2026 @polkadot/keyring authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/// <reference types="@polkadot/dev-test/globals.d.ts" />
|
||||
|
||||
import { dilithiumPath } from '@polkadot/util-crypto';
|
||||
|
||||
import { Keyring } from '../keyring.js';
|
||||
|
||||
// The well-known Substrate development phrase — public by design, so pinning it
|
||||
// commits no secret. Any account it derives is assumed compromised.
|
||||
const DEV_PHRASE = 'bottom drive obey lake curtain smoke basket hold race lonely fit walk';
|
||||
|
||||
// What `quantus wallet import --mnemonic-file <DEV_PHRASE> --scheme <s>` prints.
|
||||
// An independent implementation, not this one.
|
||||
const EXPECT_65 = 'qzq29m9WvneDAeXbtgueKCREtNe1rVVs6bXSMLmjr6shqvwq6';
|
||||
const EXPECT_87 = 'qzjrYTUnnE5NduTZKxe9dESCMTZg7nTueKM3bwhnkRdD1iYV4';
|
||||
|
||||
// crystal_alice — a raw 32-byte seed straight into keygen, no derivation.
|
||||
const ALICE = 'qzk1Nxai3dZD9Cn5kwGcgL6mKxsfxwqdis7kDQJ52aJS2vSn7';
|
||||
|
||||
describe('dilithium derivation', (): void => {
|
||||
const keyring = new Keyring({ ss58Format: 189, type: 'dilithium65' });
|
||||
|
||||
it('builds the paths quantus-cli uses', (): void => {
|
||||
expect(dilithiumPath('dilithium65')).toEqual("m/44'/189189'/0'/0'/1'");
|
||||
expect(dilithiumPath('dilithium87')).toEqual("m/44'/189189'/0'/0'/0'");
|
||||
expect(dilithiumPath('dilithium65', 3)).toEqual("m/44'/189189'/3'/0'/1'");
|
||||
});
|
||||
|
||||
// The whole derivation chain at once: BIP39 to a 64-byte seed (not Substrate's
|
||||
// mnemonicToMiniSecret), the HMAC-SHA512 walk keyed with "Dilithium seed", the
|
||||
// trailing hardened index carrying the scheme, and the Poseidon2 account hash.
|
||||
it('derives what quantus-cli derives, both schemes', (): void => {
|
||||
expect(keyring.createFromUri(DEV_PHRASE, {}, 'dilithium65').address).toEqual(EXPECT_65);
|
||||
expect(keyring.createFromUri(DEV_PHRASE, {}, 'dilithium87').address).toEqual(EXPECT_87);
|
||||
});
|
||||
|
||||
it('defaults to account index 0', (): void => {
|
||||
expect(keyring.createFromUri(`${DEV_PHRASE}//0`, {}, 'dilithium65').address).toEqual(EXPECT_65);
|
||||
});
|
||||
|
||||
it('derives distinct accounts per index', (): void => {
|
||||
const zero = keyring.createFromUri(`${DEV_PHRASE}//0`, {}, 'dilithium65').address;
|
||||
const one = keyring.createFromUri(`${DEV_PHRASE}//1`, {}, 'dilithium65').address;
|
||||
|
||||
expect(zero).not.toEqual(one);
|
||||
});
|
||||
|
||||
it('accepts an explicit hardened path', (): void => {
|
||||
const byIndex = keyring.createFromUri(`${DEV_PHRASE}//2`, {}, 'dilithium65').address;
|
||||
const byPath = keyring.createFromUri(`${DEV_PHRASE}//m/44'/189189'/2'/0'/1'`, {}, 'dilithium65').address;
|
||||
|
||||
expect(byPath).toEqual(byIndex);
|
||||
});
|
||||
|
||||
// The suri syntax was built for curve junctions, where `/foo` is a soft
|
||||
// derivation over arbitrary bytes. Neither form exists for ML-DSA, and quietly
|
||||
// reinterpreting one would hand back an address no other tool derives.
|
||||
it('refuses soft derivation', (): void => {
|
||||
expect(() => keyring.createFromUri(`${DEV_PHRASE}/0`, {}, 'dilithium65')).toThrow(/Soft derivation is not possible/);
|
||||
});
|
||||
|
||||
it('refuses a named junction', (): void => {
|
||||
expect(() => keyring.createFromUri(`${DEV_PHRASE}//Alice`, {}, 'dilithium65')).toThrow(/Unsupported derivation path/);
|
||||
});
|
||||
|
||||
it('refuses an unhardened level in an explicit path', (): void => {
|
||||
expect(() => keyring.createFromUri(`${DEV_PHRASE}//m/44'/189189'/0'/0'/1`, {}, 'dilithium65')).toThrow(/Unhardened derivation/);
|
||||
});
|
||||
|
||||
// A raw seed is already key material. Whether it is the master or something
|
||||
// already derived is unanswerable, so combining it with a path is refused
|
||||
// rather than guessed.
|
||||
it('takes a raw hex seed underived, and refuses to derive from one', (): void => {
|
||||
const seed = `0x${'00'.repeat(32)}`;
|
||||
|
||||
expect(keyring.createFromUri(seed, {}, 'dilithium87').address).toEqual(ALICE);
|
||||
expect(() => keyring.createFromUri(`${seed}//1`, {}, 'dilithium87')).toThrow(/cannot be combined with a raw seed/);
|
||||
});
|
||||
|
||||
it('honours a BIP39 passphrase', (): void => {
|
||||
const plain = keyring.createFromUri(DEV_PHRASE, {}, 'dilithium65').address;
|
||||
const withPass = keyring.createFromUri(`${DEV_PHRASE}///hunter2`, {}, 'dilithium65').address;
|
||||
|
||||
expect(plain).not.toEqual(withPass);
|
||||
});
|
||||
|
||||
// Deriving a child *from a pair* is not merely unimplemented here, it is
|
||||
// impossible: ML-DSA keys are not derivable from one another, and the Quantus
|
||||
// tree derives each account from the mnemonic independently.
|
||||
it('refuses to derive from an existing pair', (): void => {
|
||||
const pair = keyring.createFromUri(DEV_PHRASE, {}, 'dilithium65');
|
||||
|
||||
expect(() => pair.derive('//1')).toThrow(/derive from the mnemonic with createFromUri/);
|
||||
});
|
||||
|
||||
it('leaves sr25519 junction derivation unchanged', (): void => {
|
||||
const sr = new Keyring({ type: 'sr25519' });
|
||||
|
||||
expect(sr.createFromUri('//Alice').address).toEqual('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY');
|
||||
expect(sr.createFromUri('//Bob').address).toEqual('5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
// Copyright 2017-2026 @polkadot/keyring authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/// <reference types="@polkadot/dev-test/globals.d.ts" />
|
||||
|
||||
import { contextForSpec } from '@quantus/crypto';
|
||||
|
||||
import { dilithiumVerify } from '@polkadot/util-crypto';
|
||||
|
||||
import { Keyring } from '../keyring.js';
|
||||
|
||||
// The address `quantus developer create-test-wallets` prints for crystal_alice.
|
||||
const ALICE_ADDRESS = 'qzk1Nxai3dZD9Cn5kwGcgL6mKxsfxwqdis7kDQJ52aJS2vSn7';
|
||||
const PASSWORD = 'not a good password';
|
||||
const MESSAGE = new Uint8Array([0x6a, 0x73, 0x6f, 0x6e]);
|
||||
|
||||
describe('dilithium account JSON', (): void => {
|
||||
const keyring = new Keyring({ ss58Format: 189, type: 'dilithium87' });
|
||||
const context = contextForSpec(148);
|
||||
|
||||
const backup = (type: 'dilithium65' | 'dilithium87', fill = 0) => {
|
||||
const pair = keyring.addFromSeed(new Uint8Array(32).fill(fill), { name: 'backed up' }, type);
|
||||
|
||||
return { json: pair.toJson(PASSWORD), original: pair };
|
||||
};
|
||||
|
||||
// The whole point. Before this worked, an exported Quantus account could not be
|
||||
// imported by anything — decodePair looked for its divider at one of two fixed
|
||||
// offsets and an ML-DSA secret is at neither, so restore threw "Invalid
|
||||
// encoding divider found in body". That is a data-durability bug, not a
|
||||
// convenience one: it had to land before any build let a user create an account.
|
||||
it('round-trips an ML-DSA-87 account', (): void => {
|
||||
const { json, original } = backup('dilithium87');
|
||||
const restored = keyring.createFromJson(json);
|
||||
|
||||
restored.decodePkcs8(PASSWORD);
|
||||
|
||||
expect(restored.address).toEqual(original.address);
|
||||
expect(restored.publicKey).toEqual(original.publicKey);
|
||||
expect(restored.isLocked).toEqual(false);
|
||||
});
|
||||
|
||||
it('round-trips an ML-DSA-65 account', (): void => {
|
||||
const { json, original } = backup('dilithium65', 5);
|
||||
const restored = keyring.createFromJson(json);
|
||||
|
||||
restored.decodePkcs8(PASSWORD);
|
||||
|
||||
expect(restored.address).toEqual(original.address);
|
||||
expect(restored.publicKey).toEqual(original.publicKey);
|
||||
});
|
||||
|
||||
// A restored pair is locked, and callers read `pair.address` off it long before
|
||||
// any password appears. For every other scheme that works because the address
|
||||
// *is* the public key; here the key is still encrypted, so the account id has
|
||||
// to be carried as data until decodePkcs8 supplies the real one.
|
||||
it('reports the same address before and after unlock', (): void => {
|
||||
const { json } = backup('dilithium87');
|
||||
const restored = keyring.createFromJson(json);
|
||||
|
||||
expect(restored.isLocked).toEqual(true);
|
||||
expect(restored.address).toEqual(ALICE_ADDRESS);
|
||||
|
||||
restored.decodePkcs8(PASSWORD);
|
||||
|
||||
expect(restored.address).toEqual(ALICE_ADDRESS);
|
||||
});
|
||||
|
||||
it('restores a pair that can still sign', (): void => {
|
||||
const { json, original } = backup('dilithium87');
|
||||
const restored = keyring.createFromJson(json);
|
||||
|
||||
restored.decodePkcs8(PASSWORD);
|
||||
|
||||
const signature = restored.sign(MESSAGE, { context });
|
||||
|
||||
expect(dilithiumVerify(MESSAGE, signature, original.addressRaw, 'dilithium87', context)).toEqual(true);
|
||||
});
|
||||
|
||||
it('fails cleanly on a wrong password', (): void => {
|
||||
const { json } = backup('dilithium87');
|
||||
const restored = keyring.createFromJson(json);
|
||||
|
||||
expect(() => restored.decodePkcs8('wrong')).toThrow();
|
||||
// and must not have half-applied anything
|
||||
expect(restored.isLocked).toEqual(true);
|
||||
expect(restored.address).toEqual(ALICE_ADDRESS);
|
||||
});
|
||||
|
||||
// For every other scheme a tampered `address` field cannot decode at all,
|
||||
// because the address is the public key. Here it decodes perfectly and yields a
|
||||
// pair reporting an address its key does not control — a user would see someone
|
||||
// else's address in their own wallet and believe they held it.
|
||||
it('rejects JSON whose address does not match its key', (): void => {
|
||||
const { json } = backup('dilithium87');
|
||||
const other = keyring.addFromSeed(new Uint8Array(32).fill(2), {}, 'dilithium87');
|
||||
const tampered = { ...json, address: other.address };
|
||||
const restored = keyring.createFromJson(tampered);
|
||||
|
||||
expect(restored.address).toEqual(other.address);
|
||||
expect(() => restored.decodePkcs8(PASSWORD)).toThrow(/does not match the address/);
|
||||
});
|
||||
|
||||
it('leaves sr25519 JSON round-tripping unchanged', (): void => {
|
||||
const sr = new Keyring({ type: 'sr25519' });
|
||||
const pair = sr.addFromUri('//Alice');
|
||||
const restored = sr.createFromJson(pair.toJson(PASSWORD));
|
||||
|
||||
restored.decodePkcs8(PASSWORD);
|
||||
|
||||
expect(restored.address).toEqual(pair.address);
|
||||
expect(restored.publicKey).toEqual(pair.publicKey);
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,7 @@ import type { KeyringPair, KeyringPair$Json, KeyringPair$Meta, SignOptions } fro
|
||||
import type { PairInfo } from './types.js';
|
||||
|
||||
import { objectSpread, u8aConcat, u8aEmpty, u8aEq, u8aToHex, u8aToU8a } from '@polkadot/util';
|
||||
import { blake2AsU8a, ed25519PairFromSeed as ed25519FromSeed, ed25519Sign, ethereumEncode, keccakAsU8a, keyExtractPath, keyFromPath, secp256k1Compress, secp256k1Expand, secp256k1PairFromSeed as secp256k1FromSeed, secp256k1Sign, signatureVerify, sr25519PairFromSeed as sr25519FromSeed, sr25519Sign, sr25519VrfSign, sr25519VrfVerify } from '@polkadot/util-crypto';
|
||||
import { blake2AsU8a, dilithiumAccountFromPublic, dilithiumPairFromSeed, dilithiumSign, dilithiumSizes, ed25519PairFromSeed as ed25519FromSeed, ed25519Sign, ethereumEncode, isDilithium, keccakAsU8a, keyExtractPath, keyFromPath, secp256k1Compress, secp256k1Expand, secp256k1PairFromSeed as secp256k1FromSeed, secp256k1Sign, signatureVerify, sr25519PairFromSeed as sr25519FromSeed, sr25519Sign, sr25519VrfSign, sr25519VrfVerify } from '@polkadot/util-crypto';
|
||||
|
||||
import { decodePair } from './decode.js';
|
||||
import { encodePair } from './encode.js';
|
||||
@@ -20,27 +20,70 @@ interface Setup {
|
||||
const SIG_TYPE_NONE = new Uint8Array();
|
||||
|
||||
const TYPE_FROM_SEED = {
|
||||
dilithium65: (seed: Uint8Array) => dilithiumPairFromSeed(seed, 'dilithium65'),
|
||||
dilithium87: (seed: Uint8Array) => dilithiumPairFromSeed(seed, 'dilithium87'),
|
||||
ecdsa: secp256k1FromSeed,
|
||||
ed25519: ed25519FromSeed,
|
||||
ethereum: secp256k1FromSeed,
|
||||
sr25519: sr25519FromSeed
|
||||
};
|
||||
|
||||
// For the curve types these index Substrate's `MultiSignature`. For the ML-DSA
|
||||
// types they index the Quantus runtime's `DilithiumSignatureScheme`, where
|
||||
// `Dilithium87` is variant 0 and `Dilithium65` is variant 1 — a different enum
|
||||
// that happens to be reached by the same `withType` mechanism, so no special
|
||||
// casing is needed anywhere upstream of here.
|
||||
const TYPE_PREFIX = {
|
||||
dilithium65: new Uint8Array([1]),
|
||||
dilithium87: new Uint8Array([0]),
|
||||
ecdsa: new Uint8Array([2]),
|
||||
ed25519: new Uint8Array([0]),
|
||||
ethereum: new Uint8Array([2]),
|
||||
sr25519: new Uint8Array([1])
|
||||
};
|
||||
|
||||
const TYPE_SIGNATURE = {
|
||||
ecdsa: (m: Uint8Array, p: Partial<Keypair>) => secp256k1Sign(m, p, 'blake2'),
|
||||
ed25519: ed25519Sign,
|
||||
ethereum: (m: Uint8Array, p: Partial<Keypair>) => secp256k1Sign(m, p, 'keccak'),
|
||||
sr25519: sr25519Sign
|
||||
// The ML-DSA arms take a context; the curve arms ignore the extra argument. The
|
||||
// signature is written out because TypeScript would otherwise infer the narrowest
|
||||
// common shape — the two-argument curve arms — and reject passing a context at all.
|
||||
type SignFn = (message: Uint8Array, pair: Partial<Keypair>, context?: Uint8Array) => Uint8Array;
|
||||
|
||||
const TYPE_SIGNATURE: Record<KeypairType, SignFn> = {
|
||||
dilithium65: (m, p, ctx) => dilithiumSign(m, p, 'dilithium65', requireContext(ctx)),
|
||||
dilithium87: (m, p, ctx) => dilithiumSign(m, p, 'dilithium87', requireContext(ctx)),
|
||||
ecdsa: (m, p) => secp256k1Sign(m, p, 'blake2'),
|
||||
// Wrapped rather than passed directly: ed25519Sign and sr25519Sign take a
|
||||
// third argument of their own (`onlyJs`), and letting it line up with the
|
||||
// context slot would silently reinterpret a Uint8Array as a boolean.
|
||||
ed25519: (m, p) => ed25519Sign(m, p),
|
||||
ethereum: (m, p) => secp256k1Sign(m, p, 'keccak'),
|
||||
sr25519: (m, p) => sr25519Sign(m, p)
|
||||
};
|
||||
|
||||
/**
|
||||
* A post-quantum pair will not sign without being told which context to use.
|
||||
*
|
||||
* See `SignOptions.context`. The alternative — defaulting to the extrinsic
|
||||
* context — produces a valid signature that a spec-147 chain rejects, with
|
||||
* nothing local able to explain why.
|
||||
*/
|
||||
function requireContext (context?: Uint8Array): Uint8Array {
|
||||
if (!context) {
|
||||
throw new Error('A signing context is required for post-quantum keypairs; pass options.context (see contextForSpec)');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
// Every curve arm here is identity or a cheap re-encoding, because on Substrate
|
||||
// the address *is* the public key. The ML-DSA arms are the exception that the
|
||||
// rest of this file has to be read in light of: 1952 or 2592 bytes in, 32 out,
|
||||
// and no way back. `ecdsa` already hints at the shape — it hashes when given an
|
||||
// uncompressed key — but it stays reversible in the sense that matters, since
|
||||
// the signature carries a recoverable key. ML-DSA does not, which is why its
|
||||
// signatures carry the public key explicitly.
|
||||
const TYPE_ADDRESS = {
|
||||
dilithium65: dilithiumAccountFromPublic,
|
||||
dilithium87: dilithiumAccountFromPublic,
|
||||
ecdsa: (p: Uint8Array) => p.length > 32 ? blake2AsU8a(p) : p,
|
||||
ed25519: (p: Uint8Array) => p,
|
||||
ethereum: (p: Uint8Array) => p.length === 20 ? p : keccakAsU8a(secp256k1Expand(p)),
|
||||
@@ -86,11 +129,21 @@ function vrfHash (proof: Uint8Array, context?: string | Uint8Array, extra?: stri
|
||||
* an `encoded` property that is assigned with the encoded public key in hex format, and an `encoding`
|
||||
* property that indicates whether the public key value of the `encoded` property is encoded or not.
|
||||
*/
|
||||
export function createPair ({ toSS58, type }: Setup, { publicKey, secretKey }: PairInfo, meta: KeyringPair$Meta = {}, encoded: Uint8Array | null = null, encTypes?: EncryptedJsonEncoding[]): KeyringPair {
|
||||
export function createPair ({ toSS58, type }: Setup, { accountId, publicKey, secretKey }: PairInfo, meta: KeyringPair$Meta = {}, encoded: Uint8Array | null = null, encTypes?: EncryptedJsonEncoding[]): KeyringPair {
|
||||
const decodePkcs8 = (passphrase?: string, userEncoded?: Uint8Array | null): void => {
|
||||
const decoded = decodePair(passphrase, userEncoded || encoded, encTypes);
|
||||
// ML-DSA secrets are 4032/4896 bytes, which is neither of the two lengths
|
||||
// decodePair would otherwise try. It has to be told.
|
||||
const decoded = decodePair(passphrase, userEncoded || encoded, encTypes, isDilithium(type)
|
||||
? dilithiumSizes(type).secretKey
|
||||
: undefined
|
||||
);
|
||||
|
||||
if (decoded.secretKey.length === 64) {
|
||||
// Upstream decides "is this a secret key or a seed?" by length, because for
|
||||
// the curve schemes a 64-byte blob is a secret and a 32-byte one is a seed.
|
||||
// An ML-DSA secret is neither, and would take the seed branch and be fed to
|
||||
// keygen as entropy — producing a valid, wrong key in silence. The type
|
||||
// knows the answer, so ask it.
|
||||
if (isDilithium(type) || decoded.secretKey.length === 64) {
|
||||
publicKey = decoded.publicKey;
|
||||
secretKey = decoded.secretKey;
|
||||
} else {
|
||||
@@ -99,6 +152,25 @@ export function createPair ({ toSS58, type }: Setup, { publicKey, secretKey }: P
|
||||
publicKey = pair.publicKey;
|
||||
secretKey = pair.secretKey;
|
||||
}
|
||||
|
||||
// The public key has only now arrived. If the pair was constructed from JSON
|
||||
// it has been reporting an address carried as data since then, so check the
|
||||
// two agree.
|
||||
//
|
||||
// This is not a paranoid check. For every other scheme the address *is* the
|
||||
// public key, so a JSON file whose `address` field has been edited cannot
|
||||
// decode at all. Here it decodes perfectly and yields a pair that reports an
|
||||
// address its key does not control — a user would see the attacker's address
|
||||
// in their own wallet and believe they held it.
|
||||
if (accountId) {
|
||||
const derived = TYPE_ADDRESS[type](publicKey);
|
||||
|
||||
if (!u8aEq(derived, accountId)) {
|
||||
throw new Error('Decoded public key does not match the address in the account JSON');
|
||||
}
|
||||
|
||||
accountId = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const recode = (passphrase?: string): Uint8Array => {
|
||||
@@ -110,8 +182,15 @@ export function createPair ({ toSS58, type }: Setup, { publicKey, secretKey }: P
|
||||
return encoded;
|
||||
};
|
||||
|
||||
// While an ML-DSA pair restored from JSON is locked, its public key is still
|
||||
// inside the encrypted blob and the account id cannot be computed — so use the
|
||||
// one carried alongside. `decodePkcs8` clears it once the real key arrives and
|
||||
// has been checked against it.
|
||||
const addressRawOf = (): Uint8Array =>
|
||||
accountId ?? TYPE_ADDRESS[type](publicKey);
|
||||
|
||||
const encodeAddress = (): string => {
|
||||
const raw = TYPE_ADDRESS[type](publicKey);
|
||||
const raw = addressRawOf();
|
||||
|
||||
return type === 'ethereum'
|
||||
? ethereumEncode(raw)
|
||||
@@ -123,7 +202,7 @@ export function createPair ({ toSS58, type }: Setup, { publicKey, secretKey }: P
|
||||
return encodeAddress();
|
||||
},
|
||||
get addressRaw (): Uint8Array {
|
||||
const raw = TYPE_ADDRESS[type](publicKey);
|
||||
const raw = addressRawOf();
|
||||
|
||||
return type === 'ethereum'
|
||||
? raw.slice(-20)
|
||||
@@ -146,6 +225,13 @@ export function createPair ({ toSS58, type }: Setup, { publicKey, secretKey }: P
|
||||
derive: (suri: string, meta?: KeyringPair$Meta): KeyringPair => {
|
||||
if (type === 'ethereum') {
|
||||
throw new Error('Unable to derive on this keypair');
|
||||
} else if (isDilithium(type)) {
|
||||
// Not "not implemented" — not possible. A child here would have to come
|
||||
// from this pair's key material, and ML-DSA keys are not derivable from
|
||||
// one another at all; the Quantus tree derives every account from the
|
||||
// mnemonic independently. So the caller needs the mnemonic, not this
|
||||
// pair, and saying that is more use than a generic refusal.
|
||||
throw new Error(`Unable to derive from an existing ${type} pair; derive from the mnemonic with createFromUri instead`);
|
||||
} else if (isLocked(secretKey)) {
|
||||
throw new Error('Cannot derive on a locked keypair');
|
||||
}
|
||||
@@ -173,7 +259,7 @@ export function createPair ({ toSS58, type }: Setup, { publicKey, secretKey }: P
|
||||
options.withType
|
||||
? TYPE_PREFIX[type]
|
||||
: SIG_TYPE_NONE,
|
||||
TYPE_SIGNATURE[type](u8aToU8a(message), { publicKey, secretKey })
|
||||
TYPE_SIGNATURE[type](u8aToU8a(message), { publicKey, secretKey }, options.context)
|
||||
);
|
||||
},
|
||||
toJson: (passphrase?: string): KeyringPair$Json => {
|
||||
@@ -203,6 +289,15 @@ export function createPair ({ toSS58, type }: Setup, { publicKey, secretKey }: P
|
||||
return sr25519VrfSign(message, { secretKey }, context, extra);
|
||||
}
|
||||
|
||||
// There is no ML-DSA VRF. The construction below fakes one out of an
|
||||
// ordinary signature, which is sound only because the underlying schemes
|
||||
// are deterministic and unique — properties ML-DSA's signature does not
|
||||
// have in the form this needs. Refuse rather than produce something that
|
||||
// looks like a VRF output and is not verifiable as one.
|
||||
if (isDilithium(type)) {
|
||||
throw new Error(`VRF signing is not available for ${type}`);
|
||||
}
|
||||
|
||||
const proof = TYPE_SIGNATURE[type](u8aToU8a(message), { publicKey, secretKey });
|
||||
|
||||
return u8aConcat(vrfHash(proof, context, extra), proof);
|
||||
|
||||
@@ -2,6 +2,21 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export interface PairInfo {
|
||||
/**
|
||||
* The 32-byte account id, when it cannot be derived from `publicKey`.
|
||||
*
|
||||
* Only ML-DSA pairs need this, and only while locked. Everywhere else the
|
||||
* address is a cheap function of the public key, so a pair restored from JSON
|
||||
* can report its address before anyone supplies a password. A Quantus account
|
||||
* id is a one-way Poseidon2 hash, and the public key lives inside the
|
||||
* encrypted blob — so between `createFromJson` and `decodePkcs8` there is
|
||||
* nothing to compute it from, and callers read `pair.address` in exactly that
|
||||
* window.
|
||||
*
|
||||
* Carried as data rather than derived, and checked against the real public key
|
||||
* once that arrives.
|
||||
*/
|
||||
accountId?: Uint8Array | undefined;
|
||||
publicKey: Uint8Array;
|
||||
secretKey?: Uint8Array | undefined;
|
||||
seed?: Uint8Array | null;
|
||||
|
||||
@@ -72,6 +72,17 @@ export interface KeyringPair$Json extends EncryptedJson {
|
||||
}
|
||||
|
||||
export interface SignOptions {
|
||||
/**
|
||||
* The FIPS 204 domain-separation context, for post-quantum pairs.
|
||||
*
|
||||
* Mandatory for `dilithium65`/`dilithium87` and ignored by every other type.
|
||||
* There is deliberately no default: Quantus extrinsics on spec >= 148 are
|
||||
* verified under `QUANTUS_EXTRINSIC` and earlier ones under the empty context,
|
||||
* and a signature made under the wrong one is cryptographically valid, rejected
|
||||
* by the chain, and locally indistinguishable from a correct one. Only the
|
||||
* caller knows the spec version, so only the caller can answer.
|
||||
*/
|
||||
context?: Uint8Array;
|
||||
/** Create a MultiSignature-compatible output with an indicator type */
|
||||
withType?: boolean;
|
||||
}
|
||||
|
||||
@@ -15,14 +15,14 @@
|
||||
},
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"version": "14.0.2",
|
||||
"version": "14.0.3-quantus.2",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@polkadot/util": "14.0.2",
|
||||
"@polkadot/util": "14.0.3",
|
||||
"@substrate/ss58-registry": "^1.51.0",
|
||||
"tslib": "^2.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@polkadot/hw-ledger": "14.0.2"
|
||||
"@polkadot/hw-ledger": "14.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,10 +22,15 @@ describe('defaults', (): void => {
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('has no entries for testnets', (): void => {
|
||||
// Upstream asserted the opposite: that no testnet appears in knownGenesis.
|
||||
// This fork carries Quantus testnets deliberately — a wallet for a chain
|
||||
// under development has to reach its testnet — so the assertion becomes that
|
||||
// any testnet listed here is one we meant to list. See defaults/genesis.ts.
|
||||
it('has genesis entries only for testnets we deliberately support', (): void => {
|
||||
expect(
|
||||
genesisKeys.filter((network) =>
|
||||
knownTestnet[network]
|
||||
knownTestnet[network] &&
|
||||
!network.startsWith('quantus')
|
||||
)
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -8,9 +8,20 @@ import type { KnownGenesis } from '../types.js';
|
||||
// oldest goes last. This make lookups for the current a simple genesisHash[0]
|
||||
// where the latest ios always the first entry (See Kusama as an example)
|
||||
//
|
||||
// IMPORTANT: Apart from the test relays, this list is limited to live parachains
|
||||
// and live production networks. It does not and should not contain any testnets,
|
||||
// either stand-alone or connected to test relays such as Westend/Rococo
|
||||
// UPSTREAM SAID: "Apart from the test relays, this list is limited to live
|
||||
// parachains and live production networks. It does not and should not contain
|
||||
// any testnets, either stand-alone or connected to test relays such as
|
||||
// Westend/Rococo"
|
||||
//
|
||||
// THIS FORK DOES NOT. That rule keeps testnets out of polkadot-js/apps' chain
|
||||
// picker, which is a sensible product decision for a production explorer and the
|
||||
// wrong one for us. Quantus is a chain under active development, and a wallet
|
||||
// that cannot talk to its testnet is a wallet that cannot be tested against
|
||||
// anything but real funds. Testnets are first-class here.
|
||||
//
|
||||
// `isTestnet` is still set honestly, so a consumer that wants to hide them can;
|
||||
// what changed is that we no longer make that choice on the consumer's behalf.
|
||||
// See `toExpanded` in ../interfaces.ts.
|
||||
export const knownGenesis: KnownGenesis = {
|
||||
acala: [
|
||||
'0xfc41b9bd8ef8fe53d58c7ea67c794c7ec9a73daf05e6d54b14ff6342c99ba64c'
|
||||
@@ -153,6 +164,18 @@ export const knownGenesis: KnownGenesis = {
|
||||
polymesh: [
|
||||
'0x6fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f4063'
|
||||
],
|
||||
// Quantus mainnet, read from the node with `chain_getBlockHash(0)` on
|
||||
// 2026-09-09, the day it launched. Not the staging chain, which is
|
||||
// `0xb0e90ed9f291a9194f79…` and which telemetry confusingly calls
|
||||
// "Quantus Staging Mainnet".
|
||||
quantus: [
|
||||
'0xfb5487c0be6ae4ade2d41d16e50465129861636c2b8d61fa94d7a19631626fba'
|
||||
],
|
||||
// Heisenberg, the public Quantus testnet. A testnet carrying a genesis hash is
|
||||
// a deliberate departure from upstream — see the note above.
|
||||
'quantus-heisenberg': [
|
||||
'0xa5aa9e5c84d4a3722c152295e7973c9af522f2fb1ef7db5afaa3d5f4dc8d3b4f'
|
||||
],
|
||||
quartz: [
|
||||
'0xcd4d732201ebe5d6b014edda071c4203e16867305332301dc8d092044b28e554'
|
||||
],
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
export { knownGenesis } from './genesis.js';
|
||||
export { knownIcon } from './icons.js';
|
||||
export { knownLedger } from './ledger.js';
|
||||
export { knownTestnet } from './testnets.js';
|
||||
export { knownQuantus } from './quantus.js';
|
||||
export { exposedTestnet, knownTestnet } from './testnets.js';
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2017-2026 @polkadot/networks authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { KnownSubstrate } from '../types.js';
|
||||
|
||||
// Quantus networks, which `@substrate/ss58-registry` does not carry.
|
||||
//
|
||||
// Prefix 189 is unclaimed in that registry — nothing occupies 185..195 at all —
|
||||
// so this is an addition rather than an override. It lives in this fork because
|
||||
// the registry is Parity-curated and post-quantum support has not landed
|
||||
// upstream; see the standing convention in quantus/extension#1.
|
||||
//
|
||||
// `standardAccount: 'ml-dsa'` is a value upstream's type does not allow, which
|
||||
// is why `KnownSubstrate` is widened in ../types.ts. It is load-bearing: a null
|
||||
// or missing `standardAccount` makes `toExpanded` mark the network `isIgnored`
|
||||
// and it disappears from `availableNetworks`.
|
||||
export const knownQuantus: KnownSubstrate[] = [
|
||||
{
|
||||
decimals: [12],
|
||||
displayName: 'Quantus',
|
||||
network: 'quantus',
|
||||
prefix: 189,
|
||||
standardAccount: 'ml-dsa',
|
||||
symbols: ['QTC'],
|
||||
website: 'https://quantus.com'
|
||||
},
|
||||
{
|
||||
decimals: [12],
|
||||
displayName: 'Quantus Heisenberg',
|
||||
network: 'quantus-heisenberg',
|
||||
prefix: 189,
|
||||
standardAccount: 'ml-dsa',
|
||||
// HEI, not QTC — confirmed from the chain itself via system_properties on
|
||||
// a1-heisenberg.quantus.cat. A testnet token that displayed as the mainnet
|
||||
// symbol would be a good way to confuse play money for real.
|
||||
symbols: ['HEI'],
|
||||
website: 'https://quantus.com'
|
||||
}
|
||||
];
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
import type { KnownTestnet } from '../types.js';
|
||||
|
||||
// testnets should not allow selection
|
||||
// Marks a network as a testnet. Purely descriptive — see `exposedTestnet` below
|
||||
// for which of them this fork actually offers.
|
||||
export const knownTestnet: KnownTestnet = {
|
||||
'': true, // this is the default non-network entry
|
||||
'cess-testnet': true,
|
||||
@@ -11,6 +12,24 @@ export const knownTestnet: KnownTestnet = {
|
||||
jupiter: true,
|
||||
'mathchain-testnet': true,
|
||||
p3dt: true,
|
||||
'quantus-heisenberg': true,
|
||||
subspace_testnet: true,
|
||||
'zero-alphaville': true
|
||||
};
|
||||
|
||||
// Testnets this fork exposes anyway, despite upstream's rule that a testnet is
|
||||
// never offered.
|
||||
//
|
||||
// That rule keeps testnets out of polkadot-js/apps' chain picker, which is right
|
||||
// for a production explorer and wrong for us: Quantus is under active
|
||||
// development, and a wallet that cannot reach its testnet can only be tested
|
||||
// against real funds.
|
||||
//
|
||||
// An explicit allowlist rather than dropping the rule wholesale. Dropping it
|
||||
// exposed all fifteen of upstream's testnets too, which is a change we have no
|
||||
// reason to make and no way to have thought about — it moved the tail of the
|
||||
// sorted list from 'ZERO' to 'ZERO Alphaville' and would have surprised anyone
|
||||
// reading `availableNetworks`. Opting in one network at a time says what we mean.
|
||||
export const exposedTestnet: KnownTestnet = {
|
||||
'quantus-heisenberg': true
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import type { SubstrateNetwork } from './types.js';
|
||||
|
||||
import { knownGenesis, knownIcon, knownLedger, knownTestnet } from './defaults/index.js';
|
||||
import { allNetworks, availableNetworks, selectableNetworks } from './index.js';
|
||||
import { allNetworks, availableNetworks, isQuantumSafe, selectableNetworks } from './index.js';
|
||||
|
||||
describe('availableNetworks', (): void => {
|
||||
it('has the correct starting order', (): void => {
|
||||
@@ -51,16 +51,40 @@ describe('availableNetworks', (): void => {
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('has no testnets exposed', (): void => {
|
||||
// Upstream asserted that no testnet reaches availableNetworks at all. This
|
||||
// fork exposes the Quantus ones deliberately — see exposedTestnet — so what is
|
||||
// worth pinning is that an exposed testnet is still *labelled* one. Losing the
|
||||
// label is the failure that matters: a user cannot tell play money from real
|
||||
// money by looking at an address.
|
||||
it('labels every exposed testnet as a testnet', (): void => {
|
||||
expect(
|
||||
Object.keys(knownTestnet).filter((network) =>
|
||||
availableNetworks.some((a) =>
|
||||
a.network === network
|
||||
)
|
||||
availableNetworks.some((a) => a.network === network && !a.isTestnet)
|
||||
)
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('exposes no testnets except the ones we opted into', (): void => {
|
||||
expect(
|
||||
availableNetworks
|
||||
.filter((a) => a.isTestnet && !a.network.startsWith('quantus'))
|
||||
.map((a) => a.network)
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
// These lists stay complete on purpose. An earlier version of this fork
|
||||
// filtered them to post-quantum chains, which broke @polkadot/types-known — it
|
||||
// looks up every chain it holds upgrade history for and throws when one is
|
||||
// missing, so @polkadot/api failed at import. Gating what a wallet offers is
|
||||
// the wallet's job; isQuantumSafe is exported for it and the extension applies
|
||||
// it in its own chain list.
|
||||
it('identifies which chains are post-quantum', (): void => {
|
||||
expect(isQuantumSafe('ml-dsa')).toEqual(true);
|
||||
expect(isQuantumSafe('Sr25519')).toEqual(false);
|
||||
expect(isQuantumSafe(null)).toEqual(false);
|
||||
expect(availableNetworks.filter((n) => isQuantumSafe(n.standardAccount)).map((n) => n.network)).toEqual(['quantus', 'quantus-heisenberg']);
|
||||
});
|
||||
|
||||
it('has allNetworks icons, except for overrides', (): void => {
|
||||
expect(
|
||||
availableNetworks.filter(({ icon, network }) =>
|
||||
@@ -92,14 +116,20 @@ describe('availableNetworks', (): void => {
|
||||
});
|
||||
|
||||
describe('allNetworks', (): void => {
|
||||
it('has no ss58 duplicates', (): void => {
|
||||
// Upstream asserted one network per SS58 prefix. That stops holding the moment
|
||||
// a chain has a testnet: Quantus mainnet and Heisenberg are both 189, because
|
||||
// they are the same chain at two deployments and an address is valid on
|
||||
// either. Consumers key on genesisHash, so sharing a prefix costs nothing.
|
||||
it('has no ss58 duplicates, except deployments of the same chain', (): void => {
|
||||
const dupes: SubstrateNetwork[] = [];
|
||||
const uniques: SubstrateNetwork[] = [];
|
||||
|
||||
allNetworks.forEach((a): void => {
|
||||
if (uniques.some((u) => u.prefix === a.prefix)) {
|
||||
const clash = uniques.find((u) => u.prefix === a.prefix);
|
||||
|
||||
if (clash && clash.network.split('-')[0] !== a.network.split('-')[0]) {
|
||||
dupes.push(a);
|
||||
} else {
|
||||
} else if (!clash) {
|
||||
uniques.push(a);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7,3 +7,4 @@
|
||||
|
||||
export * from './interfaces.js';
|
||||
export { packageInfo } from './packageInfo.js';
|
||||
export { isQuantumSafe } from './quantumSafe.js';
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { KnownSubstrate, Network, SubstrateNetwork } from './types.js';
|
||||
|
||||
import knownSubstrate from '@substrate/ss58-registry';
|
||||
|
||||
import { knownGenesis, knownIcon, knownLedger, knownTestnet } from './defaults/index.js';
|
||||
import { exposedTestnet, knownGenesis, knownIcon, knownLedger, knownQuantus, knownTestnet } from './defaults/index.js';
|
||||
|
||||
// These are known prefixes that are not sorted
|
||||
const UNSORTED = [0, 2, 42];
|
||||
@@ -26,7 +26,16 @@ function toExpanded (o: KnownSubstrate): SubstrateNetwork {
|
||||
|
||||
// filtering
|
||||
n.isTestnet = !!knownTestnet[network] || TESTNETS.includes(nameParts[nameParts.length - 1]);
|
||||
n.isIgnored = n.isTestnet || (
|
||||
|
||||
// Upstream had a bare `isTestnet ||` here, so a testnet was dropped no matter
|
||||
// how complete its entry was. That suits a production chain picker and does
|
||||
// not suit us — Quantus development happens on Heisenberg, and tools that
|
||||
// cannot reach a testnet can only be tested against real funds.
|
||||
//
|
||||
// The exception is an allowlist rather than a blanket removal: see
|
||||
// `exposedTestnet`. `isTestnet` is still reported honestly either way, so a
|
||||
// consumer can label or filter — we simply no longer make that call for them.
|
||||
n.isIgnored = (n.isTestnet && !exposedTestnet[network]) || (
|
||||
!(
|
||||
o.standardAccount &&
|
||||
o.decimals?.length &&
|
||||
@@ -59,11 +68,31 @@ function sortNetworks (a: Network, b: Network): number {
|
||||
: 1;
|
||||
}
|
||||
|
||||
// This is all the Substrate networks with our additional information
|
||||
export const allNetworks = knownSubstrate.map(toExpanded);
|
||||
// This is all the Substrate networks with our additional information.
|
||||
//
|
||||
// None of these three lists is filtered to post-quantum chains, and an earlier
|
||||
// version of this fork filtering `availableNetworks` was a mistake worth
|
||||
// recording. These are *library* lists, not a product's offer: other packages
|
||||
// consume them expecting the chains Substrate knows about to be present.
|
||||
// `@polkadot/types-known` looks up every chain it has upgrade history for and
|
||||
// **throws** when one is missing, so removing Polkadot and Kusama here made
|
||||
// `@polkadot/api` fail at import — nothing to do with Quantus, and impossible to
|
||||
// diagnose from the error.
|
||||
//
|
||||
// Gating what a wallet *offers* is the wallet's decision. `isQuantumSafe` is
|
||||
// exported for exactly that, and the extension applies it in its own chain list.
|
||||
// Keeping these complete also means a pasted Polkadot address still decodes and
|
||||
// renders as what it is — recognisable, and therefore refusable.
|
||||
//
|
||||
// Quantus is appended rather than merged: `@substrate/ss58-registry` does not
|
||||
// carry it and prefix 189 is unclaimed there, so there is nothing to override.
|
||||
// Appending also means a registry bump cannot silently drop it.
|
||||
export const allNetworks = knownSubstrate
|
||||
.map((o) => toExpanded(o as KnownSubstrate))
|
||||
.concat(knownQuantus.map(toExpanded));
|
||||
|
||||
// The list of available/claimed prefixes
|
||||
// - no testnets
|
||||
// - testnets only where explicitly exposed (see exposedTestnet)
|
||||
// - we only include those where we have a standardAccount
|
||||
// - sort by name, however we keep 0, 2, 42 first in the list
|
||||
export const availableNetworks = allNetworks.filter(filterAvailable).sort(sortNetworks);
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
|
||||
// Do not edit, auto-generated by @polkadot/dev
|
||||
|
||||
export const packageInfo = { name: '@polkadot/networks', path: 'auto', type: 'auto', version: '14.0.2' };
|
||||
export const packageInfo = { name: '@polkadot/networks', path: 'auto', type: 'auto', version: '14.0.3-quantus.2' };
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2017-2026 @polkadot/networks authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { KnownSubstrate } from './types.js';
|
||||
|
||||
/**
|
||||
* The `standardAccount` values that survive a cryptographically relevant quantum
|
||||
* computer.
|
||||
*
|
||||
* Everything else in the registry — `*25519`, `Ed25519`, `Sr25519`, `secp256k1` —
|
||||
* is a discrete-log scheme, and Shor's algorithm breaks all of them at once. A
|
||||
* chain using one of those has no quantum-safe account type to offer, whatever
|
||||
* else is true of it.
|
||||
*/
|
||||
const QUANTUM_SAFE_ACCOUNTS: KnownSubstrate['standardAccount'][] = ['ml-dsa'];
|
||||
|
||||
/**
|
||||
* Whether a network's accounts are post-quantum.
|
||||
*
|
||||
* This gates what the tooling built on this fork will *offer*. It deliberately
|
||||
* does not gate address encoding or decoding: pasting a Polkadot address into a
|
||||
* Quantus wallet should render as a Polkadot address so it can be recognised and
|
||||
* refused, not fail to decode and look like a typo.
|
||||
*
|
||||
* A `null` standardAccount — which the registry uses for chains that never said —
|
||||
* is treated as unsafe. Unknown is not the same as safe, and the cost of being
|
||||
* wrong in that direction is a user holding funds under a key Shor's algorithm
|
||||
* recovers.
|
||||
*/
|
||||
export function isQuantumSafe (standardAccount: KnownSubstrate['standardAccount']): boolean {
|
||||
return QUANTUM_SAFE_ACCOUNTS.includes(standardAccount);
|
||||
}
|
||||
@@ -12,7 +12,19 @@ export type KnownLedger = Record<string, number>;
|
||||
|
||||
export type KnownGenesis = Record<string, HexString[]>;
|
||||
|
||||
export type KnownSubstrate = RegistryEntry;
|
||||
/**
|
||||
* A registry entry, widened for schemes the upstream registry does not know about.
|
||||
*
|
||||
* `@substrate/ss58-registry` types `standardAccount` as a closed union of the
|
||||
* curves Substrate ships with. Quantus signs with ML-DSA, which is not one of
|
||||
* them and — given the registry is a Parity-curated list and post-quantum
|
||||
* support has not landed upstream — is not going to become one. Widening here
|
||||
* keeps the Quantus entry in `defaults/quantus.ts` type-checked rather than
|
||||
* cast, and leaves every upstream entry exactly as the registry declares it.
|
||||
*/
|
||||
export type KnownSubstrate = Omit<RegistryEntry, 'standardAccount'> & {
|
||||
standardAccount: RegistryEntry['standardAccount'] | 'ml-dsa';
|
||||
};
|
||||
|
||||
export type KnownTestnet = Record<string, true>;
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"./packageDetect.cjs"
|
||||
],
|
||||
"type": "module",
|
||||
"version": "14.0.2",
|
||||
"version": "14.0.3-quantus.2",
|
||||
"browser": {
|
||||
"crypto": false,
|
||||
"stream": false
|
||||
@@ -29,17 +29,18 @@
|
||||
"dependencies": {
|
||||
"@noble/curves": "^1.3.0",
|
||||
"@noble/hashes": "^1.3.3",
|
||||
"@polkadot/networks": "14.0.2",
|
||||
"@polkadot/util": "14.0.2",
|
||||
"@polkadot/networks": "14.0.3-quantus.2",
|
||||
"@polkadot/util": "14.0.3",
|
||||
"@polkadot/wasm-crypto": "^7.5.3",
|
||||
"@polkadot/wasm-util": "^7.5.3",
|
||||
"@polkadot/x-bigint": "14.0.2",
|
||||
"@polkadot/x-randomvalues": "14.0.2",
|
||||
"@polkadot/x-bigint": "14.0.3",
|
||||
"@polkadot/x-randomvalues": "14.0.3",
|
||||
"@quantus/crypto": "^0.1.0",
|
||||
"@scure/base": "^1.1.7",
|
||||
"@scure/sr25519": "^0.2.0",
|
||||
"tslib": "^2.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@polkadot/util": "14.0.2"
|
||||
"@polkadot/util": "14.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ export * from './base58/index.js';
|
||||
export * from './base64/index.js';
|
||||
export * from './blake2/index.js';
|
||||
export * from './crypto.js';
|
||||
export * from './dilithium.js';
|
||||
export * from './dilithium/index.js';
|
||||
export * from './ed25519/index.js';
|
||||
export * from './ethereum/index.js';
|
||||
export * from './hd/index.js';
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { KeypairType } from './types.js';
|
||||
|
||||
/** The Quantus post-quantum keypair types. */
|
||||
export const DILITHIUM_TYPES = ['dilithium65', 'dilithium87'] as const;
|
||||
|
||||
/** A Quantus post-quantum keypair type. */
|
||||
export type DilithiumType = (typeof DILITHIUM_TYPES)[number];
|
||||
|
||||
/** The keypair types that predate Quantus, i.e. everything upstream supports. */
|
||||
export type SubstrateKeypairType = Exclude<KeypairType, DilithiumType>;
|
||||
|
||||
/** Whether `type` is one of the Quantus post-quantum schemes. */
|
||||
export function isDilithium (type: KeypairType): type is DilithiumType {
|
||||
return type === 'dilithium65' || type === 'dilithium87';
|
||||
}
|
||||
|
||||
/**
|
||||
* Narrow a `KeypairType` to the ones the curve-based code paths understand,
|
||||
* throwing for the Quantus schemes.
|
||||
*
|
||||
* Opening up `KeypairType` turns every lookup table keyed by it into a type
|
||||
* error — which is the point, since it enumerates precisely the places that need
|
||||
* a post-quantum answer. Where that answer is not written yet, this is how the
|
||||
* gap is expressed: a named, typed refusal at the call site rather than a silent
|
||||
* `undefined` from an object index.
|
||||
*
|
||||
* It throws rather than degrading because every one of these paths produces key
|
||||
* material or an address. Returning a wrong-but-plausible value would mean funds
|
||||
* at an address nobody controls, discovered later and unrecoverably.
|
||||
*
|
||||
* Call sites disappear as the arms are implemented — see quantus/common#2 for
|
||||
* the pair maps and quantus/common#4 for derivation.
|
||||
*/
|
||||
export function assertSubstrateType (type: KeypairType, what: string): SubstrateKeypairType {
|
||||
if (isDilithium(type)) {
|
||||
throw new Error(`${what} is not implemented for ${type}`);
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { accountFromPublicKey } from '@quantus/crypto';
|
||||
|
||||
/**
|
||||
* The 32-byte account id for an ML-DSA public key — Poseidon2 over Goldilocks.
|
||||
*
|
||||
* This is the step with no Substrate equivalent. There an `AccountId32` *is* the
|
||||
* public key; here it is a one-way hash of one, which is why an ML-DSA signature
|
||||
* has to carry its public key inside itself and why nothing can recover a key
|
||||
* from an address. Every "the address and the publicKey are isomorphic"
|
||||
* assumption in this codebase is false for these types.
|
||||
*/
|
||||
export function dilithiumAccountFromPublic (publicKey: Uint8Array): Uint8Array {
|
||||
return accountFromPublicKey(publicKey);
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/// <reference types="@polkadot/dev-test/globals.d.ts" />
|
||||
|
||||
import type { QuantusCliWallet } from './cliWallet.js';
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { u8aToHex } from '@polkadot/util';
|
||||
|
||||
import { encodeAddress } from '../address/encode.js';
|
||||
import { cliWalletAddressMatches, dilithiumFromCliWallet, dilithiumToCliWallet } from './cliWallet.js';
|
||||
import { dilithiumPairFromMnemonic } from './pair.js';
|
||||
|
||||
const FIXTURES = path.join(process.cwd(), 'packages/util-crypto/src/dilithium/fixtures');
|
||||
|
||||
// The public Substrate dev phrase — what both fixtures were created from, and
|
||||
// published everywhere. No secret is committed.
|
||||
const DEV_PHRASE = 'bottom drive obey lake curtain smoke basket hold race lonely fit walk';
|
||||
|
||||
function load (name: string): QuantusCliWallet {
|
||||
return JSON.parse(fs.readFileSync(path.join(FIXTURES, name), 'utf-8')) as QuantusCliWallet;
|
||||
}
|
||||
|
||||
interface Case {
|
||||
file: string;
|
||||
path: string;
|
||||
type: 'dilithium65' | 'dilithium87';
|
||||
}
|
||||
|
||||
const CASE_65: Case = { file: 'cli-dev_ml_dsa_65.json', path: "m/44'/189189'/0'/0'/1'", type: 'dilithium65' };
|
||||
const CASE_87: Case = { file: 'cli-dev_ml_dsa_87.json', path: "m/44'/189189'/0'/0'/0'", type: 'dilithium87' };
|
||||
|
||||
// Both parameter sets get the identical battery. Written as a function called
|
||||
// twice rather than a loop over a `describe`, because a computed describe title
|
||||
// is invisible to static analysis and the linter rightly objects.
|
||||
function testScheme (c: Case): void {
|
||||
const wallet = load(c.file);
|
||||
|
||||
it('imports and reproduces the address the file claims', async (): Promise<void> => {
|
||||
const imported = await dilithiumFromCliWallet(wallet);
|
||||
|
||||
expect(imported.type).toEqual(c.type);
|
||||
expect(encodeAddress(imported.accountId, 189)).toEqual(wallet.address);
|
||||
});
|
||||
|
||||
// The same account reached two entirely different ways: through the CLI's
|
||||
// encrypted container, and by deriving from the mnemonic ourselves. If either
|
||||
// side were wrong these would not meet.
|
||||
it('yields the key our own derivation yields', async (): Promise<void> => {
|
||||
const imported = await dilithiumFromCliWallet(wallet);
|
||||
const derived = dilithiumPairFromMnemonic(DEV_PHRASE, '', c.path, c.type);
|
||||
|
||||
expect(u8aToHex(imported.keypair.publicKey)).toEqual(u8aToHex(derived.publicKey));
|
||||
expect(u8aToHex(imported.keypair.secretKey)).toEqual(u8aToHex(derived.secretKey));
|
||||
});
|
||||
|
||||
it('reports the derivation path the CLI recorded', async (): Promise<void> => {
|
||||
const imported = await dilithiumFromCliWallet(wallet);
|
||||
|
||||
expect(imported.derivationPath).toEqual(c.path);
|
||||
});
|
||||
|
||||
// The CLI keeps a mnemonic in every HD wallet. We do not return it — the
|
||||
// extension's posture is that a storage password unlocks signing and nothing on
|
||||
// disk regenerates the tree. The flag exists so a caller can tell the user
|
||||
// where their backup lives without the phrase passing through.
|
||||
it('reports that a mnemonic was present without returning it', async (): Promise<void> => {
|
||||
const imported = await dilithiumFromCliWallet(wallet);
|
||||
|
||||
expect(imported.hadMnemonic).toEqual(true);
|
||||
expect(Object.keys(imported).includes('mnemonic')).toEqual(false);
|
||||
});
|
||||
}
|
||||
|
||||
describe('quantus-cli wallet import', (): void => {
|
||||
describe('dilithium65', (): void => {
|
||||
testScheme(CASE_65);
|
||||
});
|
||||
|
||||
describe('dilithium87', (): void => {
|
||||
testScheme(CASE_87);
|
||||
});
|
||||
|
||||
it('fails clearly on a wrong password', async (): Promise<void> => {
|
||||
await expect(dilithiumFromCliWallet(load(CASE_65.file), 'wrong')).rejects.toThrow(/password may be incorrect/);
|
||||
});
|
||||
|
||||
// An ML-KEM envelope mode that is planned or optional. Ignoring these fields
|
||||
// and decrypting by the path we know would fail later and more confusingly,
|
||||
// most likely as a bad-password error on a password that was correct.
|
||||
it('refuses a wallet using ML-KEM envelope encryption', async (): Promise<void> => {
|
||||
const wallet = { ...load(CASE_65.file), kyber_ciphertext: [1, 2, 3] };
|
||||
|
||||
await expect(dilithiumFromCliWallet(wallet)).rejects.toThrow(/ML-KEM envelope encryption/);
|
||||
});
|
||||
|
||||
it('refuses an unknown container version', async (): Promise<void> => {
|
||||
const wallet = { ...load(CASE_65.file), encryption_version: 3 };
|
||||
|
||||
await expect(dilithiumFromCliWallet(wallet)).rejects.toThrow(/encryption version 3/);
|
||||
});
|
||||
|
||||
// The Argon2 parameters live in the file because they are expected to change.
|
||||
// A build that assumed today's values would reject a valid password on an
|
||||
// older or newer wallet — the least useful thing it could say.
|
||||
it('reads the Argon2 parameters rather than assuming them', async (): Promise<void> => {
|
||||
const wallet = { ...load(CASE_65.file), argon2_params: '$argon2i$v=19$m=19456,t=2,p=1$x' };
|
||||
|
||||
await expect(dilithiumFromCliWallet(wallet)).rejects.toThrow(/Unsupported Argon2 parameters/);
|
||||
});
|
||||
|
||||
// The address sits outside the encrypted blob, so it is the one field an
|
||||
// attacker can edit without the password. Unchecked, a tampered file imports
|
||||
// cleanly and produces an account displaying an address it cannot sign for.
|
||||
it('detects an address that disagrees with the key', async (): Promise<void> => {
|
||||
const a = await dilithiumFromCliWallet(load(CASE_65.file));
|
||||
const b = await dilithiumFromCliWallet(load(CASE_87.file));
|
||||
|
||||
expect(cliWalletAddressMatches(a.accountId, a.accountId)).toEqual(true);
|
||||
expect(cliWalletAddressMatches(a.accountId, b.accountId)).toEqual(false);
|
||||
});
|
||||
|
||||
// Export is what stops import being a one-way door. The mnemonic is always
|
||||
// null, which is a shape the CLI writes itself for raw-seed wallets and opens
|
||||
// normally — verified against quantus-cli 2.2.2 by hand, since CI has no CLI.
|
||||
describe('export', (): void => {
|
||||
it('round-trips a keypair back through import', async (): Promise<void> => {
|
||||
const source = await dilithiumFromCliWallet(load(CASE_65.file));
|
||||
const exported = await dilithiumToCliWallet({
|
||||
derivationPath: CASE_65.path,
|
||||
keypair: source.keypair,
|
||||
name: 'round tripped',
|
||||
type: 'dilithium65'
|
||||
}, 'a password');
|
||||
const reimported = await dilithiumFromCliWallet(exported, 'a password');
|
||||
|
||||
expect(u8aToHex(reimported.keypair.publicKey)).toEqual(u8aToHex(source.keypair.publicKey));
|
||||
expect(u8aToHex(reimported.keypair.secretKey)).toEqual(u8aToHex(source.keypair.secretKey));
|
||||
expect(reimported.type).toEqual('dilithium65');
|
||||
expect(reimported.derivationPath).toEqual(CASE_65.path);
|
||||
});
|
||||
|
||||
it('writes the address the key actually hashes to', async (): Promise<void> => {
|
||||
const source = await dilithiumFromCliWallet(load(CASE_87.file));
|
||||
const exported = await dilithiumToCliWallet({
|
||||
keypair: source.keypair, name: 'addressed', type: 'dilithium87'
|
||||
});
|
||||
|
||||
expect(exported.address).toEqual(load(CASE_87.file).address);
|
||||
});
|
||||
|
||||
it('carries no mnemonic', async (): Promise<void> => {
|
||||
const source = await dilithiumFromCliWallet(load(CASE_65.file));
|
||||
const exported = await dilithiumToCliWallet({ keypair: source.keypair, name: 'x', type: 'dilithium65' });
|
||||
const reimported = await dilithiumFromCliWallet(exported);
|
||||
|
||||
expect(source.hadMnemonic).toEqual(true);
|
||||
expect(reimported.hadMnemonic).toEqual(false);
|
||||
});
|
||||
|
||||
// A raw-seed account has no path, and the CLI records `m/` for that rather
|
||||
// than omitting the field.
|
||||
it('records m/ for an account with no derivation path', async (): Promise<void> => {
|
||||
const source = await dilithiumFromCliWallet(load(CASE_65.file));
|
||||
const exported = await dilithiumToCliWallet({ keypair: source.keypair, name: 'raw', type: 'dilithium65' });
|
||||
const reimported = await dilithiumFromCliWallet(exported);
|
||||
|
||||
expect(reimported.derivationPath).toEqual('m/');
|
||||
});
|
||||
|
||||
it('uses a fresh salt and nonce each time', async (): Promise<void> => {
|
||||
const source = await dilithiumFromCliWallet(load(CASE_65.file));
|
||||
const input = { keypair: source.keypair, name: 'x', type: 'dilithium65' as const };
|
||||
const a = await dilithiumToCliWallet(input);
|
||||
const b = await dilithiumToCliWallet(input);
|
||||
|
||||
expect(a.argon2_salt).not.toEqual(b.argon2_salt);
|
||||
expect(a.aes_nonce).not.toEqual(b.aes_nonce);
|
||||
expect(a.encrypted_data).not.toEqual(b.encrypted_data);
|
||||
});
|
||||
|
||||
it('writes a container this build declares it supports', async (): Promise<void> => {
|
||||
const source = await dilithiumFromCliWallet(load(CASE_65.file));
|
||||
const exported = await dilithiumToCliWallet({ keypair: source.keypair, name: 'x', type: 'dilithium65' });
|
||||
|
||||
expect(exported.encryption_version).toEqual(2);
|
||||
expect(exported.kyber_ciphertext).toEqual([]);
|
||||
expect(exported.kyber_public_key).toEqual([]);
|
||||
expect(exported.argon2_params.startsWith('$argon2id$v=19$m=')).toEqual(true);
|
||||
expect(exported.argon2_salt.length).toEqual(16);
|
||||
expect(exported.aes_nonce.length).toEqual(12);
|
||||
});
|
||||
|
||||
it('refuses key material that does not match the scheme', async (): Promise<void> => {
|
||||
const source = await dilithiumFromCliWallet(load(CASE_65.file));
|
||||
|
||||
await expect(dilithiumToCliWallet({ keypair: source.keypair, name: 'x', type: 'dilithium87' })).rejects.toThrow(/Key lengths do not match/);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,304 @@
|
||||
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DilithiumType } from '../dilithium.js';
|
||||
import type { Keypair } from '../types.js';
|
||||
|
||||
import { argon2id } from '@noble/hashes/argon2';
|
||||
|
||||
import { u8aEq } from '@polkadot/util';
|
||||
|
||||
import { encodeAddress } from '../address/encode.js';
|
||||
import { base64Encode } from '../base64/index.js';
|
||||
import { dilithiumAccountFromPublic } from './account.js';
|
||||
import { dilithiumSizes } from './scheme.js';
|
||||
|
||||
/** The wallet file `quantus-cli` writes to ~/.quantus/wallets/<name>.json */
|
||||
export interface QuantusCliWallet {
|
||||
address: string;
|
||||
aes_nonce: number[];
|
||||
argon2_params: string;
|
||||
argon2_salt: number[];
|
||||
created_at?: string;
|
||||
encrypted_data: number[];
|
||||
encryption_version: number;
|
||||
kyber_ciphertext?: number[];
|
||||
kyber_public_key?: number[];
|
||||
name?: string;
|
||||
wallet_type?: string;
|
||||
}
|
||||
|
||||
export interface QuantusCliImport {
|
||||
/** The 32-byte account id the file claims, verified against the key. */
|
||||
accountId: Uint8Array;
|
||||
/** The derivation path the CLI recorded, for display. `m/` for a raw-seed wallet. */
|
||||
derivationPath: string | null;
|
||||
keypair: Keypair;
|
||||
/** Whether the file carried a mnemonic. It is never returned — see below. */
|
||||
hadMnemonic: boolean;
|
||||
name: string | null;
|
||||
type: DilithiumType;
|
||||
}
|
||||
|
||||
/** The only container version this understands. */
|
||||
const SUPPORTED_VERSION = 2;
|
||||
|
||||
const SCHEME_TO_TYPE: Record<string, DilithiumType> = {
|
||||
'ml-dsa-65': 'dilithium65',
|
||||
'ml-dsa-87': 'dilithium87'
|
||||
};
|
||||
|
||||
const TYPE_TO_SCHEME: Record<DilithiumType, string> = {
|
||||
dilithium65: 'ml-dsa-65',
|
||||
dilithium87: 'ml-dsa-87'
|
||||
};
|
||||
|
||||
/**
|
||||
* Argon2 parameters written on export.
|
||||
*
|
||||
* Deliberately the same values `quantus-cli` 2.2.2 uses rather than something
|
||||
* stronger. The parameters travel in the file and the CLI reads them, so in
|
||||
* principle anything works — but "in principle" is doing load-bearing work
|
||||
* there, and a wallet that a slightly older CLI cannot open is a worse outcome
|
||||
* than one whose KDF is tuned conservatively. Raise them when there is a reason
|
||||
* to believe every CLI in circulation honours the PHC string.
|
||||
*/
|
||||
const EXPORT_ARGON2: Argon2Params = { m: 19456, p: 1, t: 2 };
|
||||
|
||||
/**
|
||||
* SS58 prefix for the `address` field of an exported wallet.
|
||||
*
|
||||
* Fixed rather than a parameter: this is the Quantus CLI's format, the CLI only
|
||||
* ever deals with Quantus, and an address at any other prefix would be one it
|
||||
* could not match to the key inside.
|
||||
*/
|
||||
const QUANTUS_PREFIX = 189;
|
||||
|
||||
/** What the CLI records for a wallet with no derivation — a raw seed. */
|
||||
const NO_DERIVATION = 'm/';
|
||||
|
||||
interface Argon2Params {
|
||||
m: number;
|
||||
p: number;
|
||||
t: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the PHC string the CLI stores, e.g.
|
||||
* `$argon2id$v=19$m=19456,t=2,p=1$<salt>`.
|
||||
*
|
||||
* Read rather than hardcoded. The parameters are in the file because they are
|
||||
* expected to change, and a build that assumed today's values would fail to open
|
||||
* an older or newer wallet with a *wrong password* error — which is the least
|
||||
* useful thing it could say, since the password would be right.
|
||||
*/
|
||||
function parseArgon2Params (phc: string): Argon2Params {
|
||||
const match = /^\$argon2id\$v=19\$m=(\d+),t=(\d+),p=(\d+)/.exec(phc);
|
||||
|
||||
if (!match) {
|
||||
throw new Error(`Unsupported Argon2 parameters in wallet: ${phc}`);
|
||||
}
|
||||
|
||||
return {
|
||||
m: parseInt(match[1], 10),
|
||||
p: parseInt(match[3], 10),
|
||||
t: parseInt(match[2], 10)
|
||||
};
|
||||
}
|
||||
|
||||
function assertSupported (wallet: QuantusCliWallet): void {
|
||||
if (wallet.encryption_version !== SUPPORTED_VERSION) {
|
||||
throw new Error(`Unsupported quantus-cli wallet encryption version ${wallet.encryption_version}; this build understands version ${SUPPORTED_VERSION}`);
|
||||
}
|
||||
|
||||
// An ML-KEM envelope mode, either planned or optional — the fields exist and
|
||||
// are empty in every wallet seen so far. Refuse rather than ignore them and
|
||||
// decrypt by the path we do know: a wallet encrypted to an ML-KEM key is not
|
||||
// one we can open, and treating it as if it were would fail later and more
|
||||
// confusingly, most likely as a bad-password error.
|
||||
if (wallet.kyber_ciphertext?.length || wallet.kyber_public_key?.length) {
|
||||
throw new Error('This wallet uses ML-KEM envelope encryption, which is not supported');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt and validate a `quantus-cli` wallet file.
|
||||
*
|
||||
* The result is exactly what importing the same account by mnemonic would
|
||||
* produce: a keypair, and nothing that can regenerate the rest of the tree. The
|
||||
* CLI stores a mnemonic inside every HD wallet and this **deliberately does not
|
||||
* return it** — the extension's posture is that a storage password unlocks
|
||||
* signing and nothing on disk regenerates the wallet. `hadMnemonic` says one was
|
||||
* present, so a caller can tell the user where their backup lives, without the
|
||||
* phrase itself passing through.
|
||||
*
|
||||
* Async because AES-GCM is WebCrypto, which has no synchronous form. That is
|
||||
* also why this is an edge conversion rather than something `createFromJson`
|
||||
* learns: that path is synchronous and should stay so, and the keyring should
|
||||
* keep exactly one on-disk format.
|
||||
*/
|
||||
export async function dilithiumFromCliWallet (wallet: QuantusCliWallet, password = ''): Promise<QuantusCliImport> {
|
||||
assertSupported(wallet);
|
||||
|
||||
const { m, p, t } = parseArgon2Params(wallet.argon2_params);
|
||||
const key = argon2id(
|
||||
new TextEncoder().encode(password),
|
||||
new Uint8Array(wallet.argon2_salt),
|
||||
{ dkLen: 32, m, p, t }
|
||||
);
|
||||
|
||||
let plaintext: Uint8Array;
|
||||
|
||||
try {
|
||||
const aesKey = await crypto.subtle.importKey('raw', key, 'AES-GCM', false, ['decrypt']);
|
||||
|
||||
plaintext = new Uint8Array(
|
||||
await crypto.subtle.decrypt(
|
||||
{ iv: new Uint8Array(wallet.aes_nonce), name: 'AES-GCM' },
|
||||
aesKey,
|
||||
new Uint8Array(wallet.encrypted_data)
|
||||
)
|
||||
);
|
||||
} catch {
|
||||
// AES-GCM authenticates, so this is a wrong password or a corrupted file and
|
||||
// there is no way to tell which from here.
|
||||
throw new Error('Unable to decrypt the wallet; the password may be incorrect');
|
||||
}
|
||||
|
||||
const decoded = JSON.parse(new TextDecoder().decode(plaintext)) as {
|
||||
keypair?: { private_key?: number[]; public_key?: number[]; scheme?: string };
|
||||
derivation_path?: string;
|
||||
mnemonic?: string | null;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
const scheme = decoded.keypair?.scheme;
|
||||
const type = scheme ? SCHEME_TO_TYPE[scheme] : undefined;
|
||||
|
||||
if (!type) {
|
||||
throw new Error(`Unsupported signature scheme in wallet: ${scheme ?? 'none'}`);
|
||||
}
|
||||
|
||||
if (!decoded.keypair?.public_key || !decoded.keypair.private_key) {
|
||||
throw new Error('Wallet contains no keypair');
|
||||
}
|
||||
|
||||
const publicKey = new Uint8Array(decoded.keypair.public_key);
|
||||
const secretKey = new Uint8Array(decoded.keypair.private_key);
|
||||
const sizes = dilithiumSizes(type);
|
||||
|
||||
if (publicKey.length !== sizes.publicKey || secretKey.length !== sizes.secretKey) {
|
||||
throw new Error(`Wallet key lengths do not match ${scheme}: expected ${sizes.publicKey}/${sizes.secretKey}, found ${publicKey.length}/${secretKey.length}`);
|
||||
}
|
||||
|
||||
// The address travels outside the encrypted blob, so it is the one field an
|
||||
// attacker can edit without knowing the password. Left unchecked, a tampered
|
||||
// file would import cleanly and produce an account displaying an address it
|
||||
// cannot sign for — the same failure the account-JSON check in the keyring
|
||||
// exists to prevent.
|
||||
const accountId = dilithiumAccountFromPublic(publicKey);
|
||||
|
||||
return {
|
||||
accountId,
|
||||
derivationPath: decoded.derivation_path ?? null,
|
||||
hadMnemonic: !!decoded.mnemonic,
|
||||
keypair: { publicKey, secretKey },
|
||||
name: decoded.name ?? wallet.name ?? null,
|
||||
type
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a decrypted wallet's key matches the address its file claims.
|
||||
*
|
||||
* Separate from the decrypt so the caller can render the mismatch rather than
|
||||
* only catch it — "this file's address does not match its key" is a different
|
||||
* message from "wrong password", and a user can act on the difference.
|
||||
*/
|
||||
export function cliWalletAddressMatches (accountId: Uint8Array, decodedAddress: Uint8Array): boolean {
|
||||
return u8aEq(accountId, decodedAddress);
|
||||
}
|
||||
|
||||
export interface QuantusCliExportInput {
|
||||
/** The derivation path, where known. Omit for a raw-seed account. */
|
||||
derivationPath?: string | null;
|
||||
keypair: Keypair;
|
||||
name: string;
|
||||
type: DilithiumType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a `quantus-cli` wallet file.
|
||||
*
|
||||
* The mnemonic is **always null**. That is not a limitation worked around: the
|
||||
* CLI's format has a null-mnemonic shape and writes one itself for every
|
||||
* raw-seed wallet, and a wallet in that shape lists, opens and spends normally.
|
||||
* What it cannot do is `export --format mnemonic` or derive further accounts —
|
||||
* the honest consequence of the extension never storing a phrase it showed once.
|
||||
*
|
||||
* Without this, importing a CLI wallet (see `dilithiumFromCliWallet`) is a
|
||||
* one-way door, and an extension backup is readable only by polkadot-js-format
|
||||
* tooling.
|
||||
*/
|
||||
export async function dilithiumToCliWallet ({ derivationPath, keypair, name, type }: QuantusCliExportInput, password = ''): Promise<QuantusCliWallet> {
|
||||
const sizes = dilithiumSizes(type);
|
||||
|
||||
if (keypair.publicKey.length !== sizes.publicKey || keypair.secretKey.length !== sizes.secretKey) {
|
||||
throw new Error(`Key lengths do not match ${type}: expected ${sizes.publicKey}/${sizes.secretKey}, found ${keypair.publicKey.length}/${keypair.secretKey.length}`);
|
||||
}
|
||||
|
||||
const payload = JSON.stringify({
|
||||
derivation_path: derivationPath || NO_DERIVATION,
|
||||
keypair: {
|
||||
private_key: Array.from(keypair.secretKey),
|
||||
public_key: Array.from(keypair.publicKey),
|
||||
scheme: TYPE_TO_SCHEME[type]
|
||||
},
|
||||
metadata: {
|
||||
algorithm: TYPE_TO_SCHEME[type].toUpperCase(),
|
||||
// So a CLI maintainer reading a bug report can tell where the file came
|
||||
// from without having to guess from its shape.
|
||||
exported_by: 'quantus-extension',
|
||||
version: '1.0.0'
|
||||
},
|
||||
mnemonic: null,
|
||||
name
|
||||
});
|
||||
|
||||
const salt = crypto.getRandomValues(new Uint8Array(16));
|
||||
const nonce = crypto.getRandomValues(new Uint8Array(12));
|
||||
const key = argon2id(
|
||||
new TextEncoder().encode(password),
|
||||
salt,
|
||||
{ dkLen: 32, m: EXPORT_ARGON2.m, p: EXPORT_ARGON2.p, t: EXPORT_ARGON2.t }
|
||||
);
|
||||
const aesKey = await crypto.subtle.importKey('raw', key, 'AES-GCM', false, ['encrypt']);
|
||||
const encrypted = new Uint8Array(
|
||||
await crypto.subtle.encrypt(
|
||||
{ iv: nonce, name: 'AES-GCM' },
|
||||
aesKey,
|
||||
new TextEncoder().encode(payload)
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
address: encodeAddress(dilithiumAccountFromPublic(keypair.publicKey), QUANTUS_PREFIX),
|
||||
aes_nonce: Array.from(nonce),
|
||||
// The salt goes into the PHC string as well as its own field, because that
|
||||
// is what the CLI writes and a reader may take it from either.
|
||||
argon2_params: `$argon2id$v=19$m=${EXPORT_ARGON2.m},t=${EXPORT_ARGON2.t},p=${EXPORT_ARGON2.p}$${base64Unpadded(salt)}`,
|
||||
argon2_salt: Array.from(salt),
|
||||
created_at: new Date().toISOString(),
|
||||
encrypted_data: Array.from(encrypted),
|
||||
encryption_version: SUPPORTED_VERSION,
|
||||
kyber_ciphertext: [],
|
||||
kyber_public_key: [],
|
||||
name,
|
||||
wallet_type: 'hot'
|
||||
};
|
||||
}
|
||||
|
||||
/** Unpadded standard base64, as the PHC string format requires. */
|
||||
function base64Unpadded (data: Uint8Array): string {
|
||||
return base64Encode(data).replace(/=+$/, '');
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# quantus-cli wallet fixtures
|
||||
|
||||
Genuine output of `quantus-cli` 2.2.2, not files this repo constructed. Testing a
|
||||
decoder against a blob the same code encrypted would prove only that it is
|
||||
self-consistent; these prove it agrees with the tool whose wallets users actually
|
||||
have.
|
||||
|
||||
Created with:
|
||||
|
||||
```sh
|
||||
quantus wallet import --name dev_ml_dsa_65 --scheme ml-dsa-65 \
|
||||
--mnemonic-file <phrase> --allow-empty-password
|
||||
```
|
||||
|
||||
**They contain no secret.** The mnemonic inside each is the well-known Substrate
|
||||
development phrase — `bottom drive obey lake curtain smoke basket hold race
|
||||
lonely fit walk` — which is published in polkadot-sdk, in polkadot-js and in every
|
||||
tutorial. The password is empty. Any account they derive is assumed compromised
|
||||
and must never hold value.
|
||||
|
||||
That is the only reason it is acceptable to commit a decryptable wallet file. Do
|
||||
not add a fixture created from a generated mnemonic, however throwaway it seems.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export { dilithiumAccountFromPublic } from './account.js';
|
||||
export type { QuantusCliExportInput, QuantusCliImport, QuantusCliWallet } from './cliWallet.js';
|
||||
export { cliWalletAddressMatches, dilithiumFromCliWallet, dilithiumToCliWallet } from './cliWallet.js';
|
||||
export { dilithiumPairFromMnemonic, dilithiumPairFromSeed } from './pair.js';
|
||||
export { dilithiumPath, dilithiumPathFromSuri } from './path.js';
|
||||
export { dilithiumSchemeFor, dilithiumSizes } from './scheme.js';
|
||||
export { dilithiumSign } from './sign.js';
|
||||
export { dilithiumVerify } from './verify.js';
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DilithiumType } from '../dilithium.js';
|
||||
import type { Keypair } from '../types.js';
|
||||
|
||||
import { keypairFromMnemonic, keypairFromSeed } from '@quantus/crypto';
|
||||
|
||||
import { dilithiumSchemeFor } from './scheme.js';
|
||||
|
||||
/**
|
||||
* Create an ML-DSA keypair from 32 bytes of entropy.
|
||||
*
|
||||
* FIPS 204 `ML-DSA.KeyGen_internal` with no Quantus-specific step. The parameter
|
||||
* set is absorbed into the seed expansion, so the same 32 bytes give independent
|
||||
* keys per scheme — which is why the dev-genesis accounts (`[0u8; 32]` and
|
||||
* friends) and HD-derived accounts share this one entry point.
|
||||
*/
|
||||
export function dilithiumPairFromSeed (seed: Uint8Array, type: DilithiumType): Keypair {
|
||||
return keypairFromSeed(seed, dilithiumSchemeFor(type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an ML-DSA keypair from a BIP39 mnemonic at a hardened path.
|
||||
*
|
||||
* The seeding is the part that silently goes wrong. This runs mnemonic → the
|
||||
* 64-byte BIP39 seed → an HMAC-SHA512 chain keyed with the literal string
|
||||
* "Dilithium seed". Substrate's own `mnemonicToMiniSecret` is a different
|
||||
* derivation entirely and is what the rest of this package reaches for by
|
||||
* default; using it here yields a perfectly well-formed key for an account
|
||||
* nobody owns.
|
||||
*/
|
||||
export function dilithiumPairFromMnemonic (mnemonic: string, password: string, path: string, type: DilithiumType): Keypair {
|
||||
return keypairFromMnemonic(mnemonic, password, path, dilithiumSchemeFor(type));
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DilithiumType } from '../dilithium.js';
|
||||
|
||||
/** Quantus BIP44 coin type. */
|
||||
const COIN_TYPE = 189189;
|
||||
|
||||
/**
|
||||
* The derivation path for an account index.
|
||||
*
|
||||
* ```text
|
||||
* m/44'/189189'/<account>'/0'/<0 for ML-DSA-87 | 1 for ML-DSA-65>'
|
||||
* ```
|
||||
*
|
||||
* Two things about this are unusual and both are deliberate: the account index
|
||||
* sits at the third level rather than the last, and the trailing index carries
|
||||
* the *scheme* rather than an address index. It is what `quantus-cli` and the
|
||||
* mobile wallet already derive, so anything else produces addresses no other
|
||||
* Quantus tool can find. Do not improve it.
|
||||
*
|
||||
* Every level is hardened. Lattice keys are not publicly derivable, so an
|
||||
* unhardened level cannot mean what BIP-32 implies, and the hdwallet crate
|
||||
* rejects one outright.
|
||||
*/
|
||||
export function dilithiumPath (type: DilithiumType, account = 0): string {
|
||||
if (!Number.isInteger(account) || account < 0) {
|
||||
throw new Error(`Invalid account index: ${account}`);
|
||||
}
|
||||
|
||||
return `m/44'/${COIN_TYPE}'/${account}'/0'/${type === 'dilithium65' ? 1 : 0}'`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the derivation part of a suri to a Quantus path.
|
||||
*
|
||||
* Accepted:
|
||||
* - nothing → account 0, the default `quantus-cli` uses
|
||||
* - `//<n>` → account index n
|
||||
* - `//m/44'/189189'/…` → that path verbatim, for anything the index form
|
||||
* cannot express (the wormhole tree, say)
|
||||
*
|
||||
* Everything else throws, and that matters more than it looks. The suri syntax
|
||||
* these strings come from was built for curve junctions, where `/foo` is a soft
|
||||
* derivation and `//foo` a hard one over arbitrary bytes. Neither exists for
|
||||
* ML-DSA. Quietly reinterpreting `//Alice` as *something* would hand the user an
|
||||
* address no other tool derives and no seed phrase obviously recovers.
|
||||
*/
|
||||
export function dilithiumPathFromSuri (type: DilithiumType, derivePath: string): string {
|
||||
if (!derivePath) {
|
||||
return dilithiumPath(type);
|
||||
}
|
||||
|
||||
if (!derivePath.startsWith('//')) {
|
||||
throw new Error(`Soft derivation is not possible for ${type}; use //<index> or a full //m/44'/${COIN_TYPE}'/… path`);
|
||||
}
|
||||
|
||||
const rest = derivePath.substring(2);
|
||||
|
||||
if (/^\d+$/.test(rest)) {
|
||||
return dilithiumPath(type, parseInt(rest, 10));
|
||||
}
|
||||
|
||||
if (rest.startsWith('m/')) {
|
||||
if (/\d(?!')(?:\/|$)/.test(rest)) {
|
||||
throw new Error(`Unhardened derivation is not possible for ${type}: ${rest}`);
|
||||
}
|
||||
|
||||
return rest;
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported derivation path for ${type}: ${derivePath}`);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Sizes } from '@quantus/crypto';
|
||||
import type { DilithiumType } from '../dilithium.js';
|
||||
|
||||
import { Scheme, sizes } from '@quantus/crypto';
|
||||
|
||||
/**
|
||||
* The `@quantus/crypto` scheme selector for a keyring type.
|
||||
*
|
||||
* The mapping is the identity on the chain's signature-enum variant index:
|
||||
* `Dilithium87` is variant 0 and `Dilithium65` is variant 1, and that byte is
|
||||
* what a signed extrinsic carries. Nothing here invents a numbering.
|
||||
*/
|
||||
export function dilithiumSchemeFor (type: DilithiumType): Scheme {
|
||||
return type === 'dilithium65'
|
||||
? Scheme.MlDsa65
|
||||
: Scheme.MlDsa87;
|
||||
}
|
||||
|
||||
/**
|
||||
* Key and signature lengths for a keyring type, read from the crate.
|
||||
*
|
||||
* Not written down here on purpose. These are consensus-critical — the runtime
|
||||
* decodes a fixed-size array with no compact length prefix — so a constant that
|
||||
* drifted from the crate would re-frame every byte after the signature while
|
||||
* looking entirely healthy.
|
||||
*/
|
||||
export function dilithiumSizes (type: DilithiumType): Sizes {
|
||||
return sizes(dilithiumSchemeFor(type));
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DilithiumType } from '../dilithium.js';
|
||||
import type { Keypair } from '../types.js';
|
||||
|
||||
import { sign, signatureWithPublicKey } from '@quantus/crypto';
|
||||
|
||||
import { u8aToU8a } from '@polkadot/util';
|
||||
|
||||
import { dilithiumSchemeFor } from './scheme.js';
|
||||
|
||||
/**
|
||||
* Sign a message with an ML-DSA keypair under a FIPS 204 context.
|
||||
*
|
||||
* Returns `signature ‖ publicKey`, which is what the runtime's
|
||||
* `DilithiumSignatureScheme` carries — as a fixed-size array with **no** compact
|
||||
* length prefix. The public key travels inside the signature because the account
|
||||
* id is a one-way hash of it, so a verifier holding only an address cannot
|
||||
* recover the key to check anything.
|
||||
*
|
||||
* `context` is mandatory and has no default. Extrinsics on spec >= 148 are
|
||||
* verified under `QUANTUS_EXTRINSIC` and earlier ones under the empty context;
|
||||
* a signature made under the wrong one is cryptographically valid, rejected by
|
||||
* the chain, and indistinguishable from a correct one without asking a node.
|
||||
* Defaulting would turn that into a silent failure at the worst possible moment,
|
||||
* so the caller — which is the only party that knows the spec version — states it.
|
||||
*/
|
||||
export function dilithiumSign (message: string | Uint8Array, { publicKey, secretKey }: Partial<Keypair>, type: DilithiumType, context: Uint8Array): Uint8Array {
|
||||
if (!publicKey || !secretKey) {
|
||||
throw new Error('Expected a valid keypair to sign with');
|
||||
}
|
||||
|
||||
const scheme = dilithiumSchemeFor(type);
|
||||
const signature = sign(u8aToU8a(message), { publicKey, secretKey }, context, scheme);
|
||||
|
||||
return signatureWithPublicKey(signature, publicKey);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DilithiumType } from '../dilithium.js';
|
||||
|
||||
import { sizes, verify } from '@quantus/crypto';
|
||||
|
||||
import { u8aToU8a } from '@polkadot/util';
|
||||
|
||||
import { dilithiumAccountFromPublic } from './account.js';
|
||||
import { dilithiumSchemeFor } from './scheme.js';
|
||||
|
||||
/**
|
||||
* Verify a `signature ‖ publicKey` blob against an account id.
|
||||
*
|
||||
* This mirrors the runtime's `Verify::verify` rather than inventing a scheme:
|
||||
* split the blob, hash the embedded public key with Poseidon2, check it equals
|
||||
* the account being verified against, and only then check the signature. Both
|
||||
* halves matter. Skipping the account check would accept a valid signature from
|
||||
* *some* key rather than *the* key, which is the whole of the security property.
|
||||
*
|
||||
* `account` is the 32-byte account id — not a public key. For every other scheme
|
||||
* in this package those are the same bytes; here they are not.
|
||||
*/
|
||||
export function dilithiumVerify (message: string | Uint8Array, sigWithPublic: Uint8Array, account: Uint8Array, type: DilithiumType, context: Uint8Array): boolean {
|
||||
const scheme = dilithiumSchemeFor(type);
|
||||
const { publicKey: publicKeyLen, signature: signatureLen } = sizes(scheme);
|
||||
|
||||
if (sigWithPublic.length !== signatureLen + publicKeyLen) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const signature = sigWithPublic.subarray(0, signatureLen);
|
||||
const publicKey = sigWithPublic.subarray(signatureLen);
|
||||
const derived = dilithiumAccountFromPublic(publicKey);
|
||||
|
||||
if (derived.length !== account.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < derived.length; i++) {
|
||||
if (derived[i] !== account[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return verify(u8aToU8a(message), signature, publicKey, context, scheme);
|
||||
}
|
||||
@@ -17,7 +17,21 @@ const generators = {
|
||||
};
|
||||
|
||||
export function keyFromPath (pair: Keypair, path: DeriveJunction[], type: KeypairType): Keypair {
|
||||
const keyHdkd = generators[type];
|
||||
const keyHdkd = generators[type as keyof typeof generators] as typeof keyHdkdEd25519 | undefined;
|
||||
|
||||
// ML-DSA has no junction derivation to fall back on. Lattice keys are not
|
||||
// publicly derivable, so a soft junction cannot mean what BIP-32 implies, and
|
||||
// the chain's own `Pair::derive` refuses for the same reason. Quantus keys
|
||||
// derive from a hardened BIP44 path instead — see quantus/common#4 — which is
|
||||
// a different entry point, not a different generator in this map.
|
||||
//
|
||||
// This throws rather than returning the pair unchanged: silently ignoring a
|
||||
// derivation path would hand the caller the *parent* key under a child's
|
||||
// name, and fund an address nobody asked for.
|
||||
if (!keyHdkd) {
|
||||
throw new Error(`Unable to derive on this keypair type: ${type}`);
|
||||
}
|
||||
|
||||
let result = pair;
|
||||
|
||||
for (const junction of path) {
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
|
||||
// Do not edit, auto-generated by @polkadot/dev
|
||||
|
||||
export const packageInfo = { name: '@polkadot/util-crypto', path: 'auto', type: 'auto', version: '14.0.2' };
|
||||
export const packageInfo = { name: '@polkadot/util-crypto', path: 'auto', type: 'auto', version: '14.0.3-quantus.2' };
|
||||
|
||||
@@ -18,8 +18,19 @@ export interface Seedpair {
|
||||
seed: Uint8Array;
|
||||
}
|
||||
|
||||
/** The supported types of pairs */
|
||||
export type KeypairType = 'ed25519' | 'sr25519' | 'ecdsa' | 'ethereum';
|
||||
/**
|
||||
* The supported types of pairs
|
||||
*
|
||||
* `dilithium65` and `dilithium87` are the Quantus post-quantum schemes — ML-DSA-65
|
||||
* and ML-DSA-87 respectively. They are two entries rather than one `dilithium`
|
||||
* because the parameter set is not an implementation detail: it selects the
|
||||
* signature enum variant byte the chain reads off the wire, it selects the
|
||||
* trailing hardened index of the derivation path, and it fixes the key and
|
||||
* signature lengths. New Quantus accounts use `dilithium65`; `dilithium87` is
|
||||
* legacy, used by accounts created before the scheme was recorded and by the
|
||||
* dev-genesis accounts, so it must be supported but never chosen.
|
||||
*/
|
||||
export type KeypairType = 'ed25519' | 'sr25519' | 'ecdsa' | 'ethereum' | 'dilithium65' | 'dilithium87';
|
||||
|
||||
export interface VerifyResult {
|
||||
/** The detected crypto interface, or 'none' if not detected */
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"./packageDetect.cjs"
|
||||
],
|
||||
"type": "module",
|
||||
"version": "14.0.2",
|
||||
"version": "14.0.3",
|
||||
"main": "index.js",
|
||||
"exports": {
|
||||
"./hex/toU8a": {
|
||||
@@ -35,15 +35,15 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@polkadot/x-bigint": "14.0.2",
|
||||
"@polkadot/x-global": "14.0.2",
|
||||
"@polkadot/x-textdecoder": "14.0.2",
|
||||
"@polkadot/x-textencoder": "14.0.2",
|
||||
"@polkadot/x-bigint": "14.0.3",
|
||||
"@polkadot/x-global": "14.0.3",
|
||||
"@polkadot/x-textdecoder": "14.0.3",
|
||||
"@polkadot/x-textencoder": "14.0.3",
|
||||
"@types/bn.js": "^5.1.6",
|
||||
"bn.js": "^5.2.1",
|
||||
"tslib": "^2.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@polkadot/x-randomvalues": "14.0.2"
|
||||
"@polkadot/x-randomvalues": "14.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
|
||||
// Do not edit, auto-generated by @polkadot/dev
|
||||
|
||||
export const packageInfo = { name: '@polkadot/util', path: 'auto', type: 'auto', version: '14.0.2' };
|
||||
export const packageInfo = { name: '@polkadot/util', path: 'auto', type: 'auto', version: '14.0.3' };
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
"./shim.cjs"
|
||||
],
|
||||
"type": "module",
|
||||
"version": "14.0.2",
|
||||
"version": "14.0.3",
|
||||
"dependencies": {
|
||||
"@polkadot/x-global": "14.0.2",
|
||||
"@polkadot/x-global": "14.0.3",
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
|
||||
// Do not edit, auto-generated by @polkadot/dev
|
||||
|
||||
export const packageInfo = { name: '@polkadot/x-bigint', path: 'auto', type: 'auto', version: '14.0.2' };
|
||||
export const packageInfo = { name: '@polkadot/x-bigint', path: 'auto', type: 'auto', version: '14.0.3' };
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
},
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"version": "14.0.2",
|
||||
"version": "14.0.3",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@polkadot/util": "14.0.2",
|
||||
"@polkadot/util": "14.0.3",
|
||||
"buffer-es6": "^4.9.3",
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
|
||||
// Do not edit, auto-generated by @polkadot/dev
|
||||
|
||||
export const packageInfo = { name: '@polkadot/x-bundle', path: 'auto', type: 'auto', version: '14.0.2' };
|
||||
export const packageInfo = { name: '@polkadot/x-bundle', path: 'auto', type: 'auto', version: '14.0.3' };
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
},
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"version": "14.0.2",
|
||||
"version": "14.0.3",
|
||||
"browser": "browser.js",
|
||||
"main": "node.js",
|
||||
"react-native": "react-native.js",
|
||||
"dependencies": {
|
||||
"@polkadot/x-global": "14.0.2",
|
||||
"@polkadot/x-global": "14.0.3",
|
||||
"node-fetch": "^3.3.2",
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
|
||||
// Do not edit, auto-generated by @polkadot/dev
|
||||
|
||||
export const packageInfo = { name: '@polkadot/x-fetch', path: 'auto', type: 'auto', version: '14.0.2' };
|
||||
export const packageInfo = { name: '@polkadot/x-fetch', path: 'auto', type: 'auto', version: '14.0.3' };
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
},
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"version": "14.0.2",
|
||||
"version": "14.0.3",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
|
||||
// Do not edit, auto-generated by @polkadot/dev
|
||||
|
||||
export const packageInfo = { name: '@polkadot/x-global', path: 'auto', type: 'auto', version: '14.0.2' };
|
||||
export const packageInfo = { name: '@polkadot/x-global', path: 'auto', type: 'auto', version: '14.0.3' };
|
||||
|
||||
@@ -15,21 +15,21 @@
|
||||
},
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"version": "14.0.2",
|
||||
"version": "14.0.3",
|
||||
"browser": "browser.js",
|
||||
"main": "node.js",
|
||||
"react-native": "react-native.js",
|
||||
"dependencies": {
|
||||
"@polkadot/x-global": "14.0.2",
|
||||
"@polkadot/x-global": "14.0.3",
|
||||
"tslib": "^2.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@polkadot/util": "14.0.2",
|
||||
"@polkadot/util": "14.0.3",
|
||||
"@polkadot/wasm-util": "^7.5.3",
|
||||
"@types/react-native": "^0.73.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@polkadot/util": "14.0.2",
|
||||
"@polkadot/util": "14.0.3",
|
||||
"@polkadot/wasm-util": "*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
|
||||
// Do not edit, auto-generated by @polkadot/dev
|
||||
|
||||
export const packageInfo = { name: '@polkadot/x-randomvalues', path: 'auto', type: 'auto', version: '14.0.2' };
|
||||
export const packageInfo = { name: '@polkadot/x-randomvalues', path: 'auto', type: 'auto', version: '14.0.3' };
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
},
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"version": "14.0.2",
|
||||
"version": "14.0.3",
|
||||
"browser": "browser.js",
|
||||
"main": "node.js",
|
||||
"react-native": "react-native.js",
|
||||
"dependencies": {
|
||||
"@polkadot/x-global": "14.0.2",
|
||||
"@polkadot/x-global": "14.0.3",
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
|
||||
// Do not edit, auto-generated by @polkadot/dev
|
||||
|
||||
export const packageInfo = { name: '@polkadot/x-textdecoder', path: 'auto', type: 'auto', version: '14.0.2' };
|
||||
export const packageInfo = { name: '@polkadot/x-textdecoder', path: 'auto', type: 'auto', version: '14.0.3' };
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
},
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"version": "14.0.2",
|
||||
"version": "14.0.3",
|
||||
"browser": "browser.js",
|
||||
"main": "node.js",
|
||||
"react-native": "react-native.js",
|
||||
"dependencies": {
|
||||
"@polkadot/x-global": "14.0.2",
|
||||
"@polkadot/x-global": "14.0.3",
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
|
||||
// Do not edit, auto-generated by @polkadot/dev
|
||||
|
||||
export const packageInfo = { name: '@polkadot/x-textencoder', path: 'auto', type: 'auto', version: '14.0.2' };
|
||||
export const packageInfo = { name: '@polkadot/x-textencoder', path: 'auto', type: 'auto', version: '14.0.3' };
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
},
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"version": "14.0.2",
|
||||
"version": "14.0.3",
|
||||
"browser": "browser.js",
|
||||
"main": "node.js",
|
||||
"react-native": "react-native.js",
|
||||
"dependencies": {
|
||||
"@polkadot/x-global": "14.0.2",
|
||||
"@polkadot/x-global": "14.0.3",
|
||||
"tslib": "^2.8.0",
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
|
||||
// Do not edit, auto-generated by @polkadot/dev
|
||||
|
||||
export const packageInfo = { name: '@polkadot/x-ws', path: 'auto', type: 'auto', version: '14.0.2' };
|
||||
export const packageInfo = { name: '@polkadot/x-ws', path: 'auto', type: 'auto', version: '14.0.3' };
|
||||
|
||||
@@ -1590,7 +1590,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@polkadot/hw-ledger-transports@npm:14.0.2, @polkadot/hw-ledger-transports@workspace:packages/hw-ledger-transports":
|
||||
"@polkadot/hw-ledger-transports@npm:14.0.3, @polkadot/hw-ledger-transports@workspace:packages/hw-ledger-transports":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@polkadot/hw-ledger-transports@workspace:packages/hw-ledger-transports"
|
||||
dependencies:
|
||||
@@ -1598,7 +1598,7 @@ __metadata:
|
||||
"@ledgerhq/hw-transport-node-hid-singleton": "npm:^6.31.5"
|
||||
"@ledgerhq/hw-transport-webhid": "npm:^6.29.4"
|
||||
"@ledgerhq/hw-transport-webusb": "npm:^6.29.4"
|
||||
"@polkadot/util": "npm:14.0.2"
|
||||
"@polkadot/util": "npm:14.0.3"
|
||||
tslib: "npm:^2.8.0"
|
||||
dependenciesMeta:
|
||||
"@ledgerhq/hw-transport-node-hid-singleton":
|
||||
@@ -1606,12 +1606,12 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@polkadot/hw-ledger@npm:14.0.2, @polkadot/hw-ledger@workspace:packages/hw-ledger":
|
||||
"@polkadot/hw-ledger@npm:14.0.3, @polkadot/hw-ledger@workspace:packages/hw-ledger":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@polkadot/hw-ledger@workspace:packages/hw-ledger"
|
||||
dependencies:
|
||||
"@polkadot/hw-ledger-transports": "npm:14.0.2"
|
||||
"@polkadot/util": "npm:14.0.2"
|
||||
"@polkadot/hw-ledger-transports": "npm:14.0.3"
|
||||
"@polkadot/util": "npm:14.0.3"
|
||||
"@zondax/ledger-substrate": "npm:1.1.1"
|
||||
tslib: "npm:^2.8.0"
|
||||
languageName: unknown
|
||||
@@ -1621,55 +1621,57 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@polkadot/keyring@workspace:packages/keyring"
|
||||
dependencies:
|
||||
"@polkadot/util": "npm:14.0.2"
|
||||
"@polkadot/util-crypto": "npm:14.0.2"
|
||||
"@polkadot/util": "npm:14.0.3"
|
||||
"@polkadot/util-crypto": "npm:14.0.3"
|
||||
"@quantus/crypto": "npm:^0.1.0"
|
||||
tslib: "npm:^2.8.0"
|
||||
peerDependencies:
|
||||
"@polkadot/util": 14.0.2
|
||||
"@polkadot/util-crypto": 14.0.2
|
||||
"@polkadot/util": 14.0.3
|
||||
"@polkadot/util-crypto": 14.0.3
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@polkadot/networks@npm:14.0.2, @polkadot/networks@workspace:packages/networks":
|
||||
"@polkadot/networks@npm:14.0.3, @polkadot/networks@workspace:packages/networks":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@polkadot/networks@workspace:packages/networks"
|
||||
dependencies:
|
||||
"@polkadot/hw-ledger": "npm:14.0.2"
|
||||
"@polkadot/util": "npm:14.0.2"
|
||||
"@polkadot/hw-ledger": "npm:14.0.3"
|
||||
"@polkadot/util": "npm:14.0.3"
|
||||
"@substrate/ss58-registry": "npm:^1.51.0"
|
||||
tslib: "npm:^2.8.0"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@polkadot/util-crypto@npm:14.0.2, @polkadot/util-crypto@workspace:packages/util-crypto":
|
||||
"@polkadot/util-crypto@npm:14.0.3, @polkadot/util-crypto@workspace:packages/util-crypto":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@polkadot/util-crypto@workspace:packages/util-crypto"
|
||||
dependencies:
|
||||
"@noble/curves": "npm:^1.3.0"
|
||||
"@noble/hashes": "npm:^1.3.3"
|
||||
"@polkadot/networks": "npm:14.0.2"
|
||||
"@polkadot/util": "npm:14.0.2"
|
||||
"@polkadot/networks": "npm:14.0.3"
|
||||
"@polkadot/util": "npm:14.0.3"
|
||||
"@polkadot/wasm-crypto": "npm:^7.5.3"
|
||||
"@polkadot/wasm-util": "npm:^7.5.3"
|
||||
"@polkadot/x-bigint": "npm:14.0.2"
|
||||
"@polkadot/x-randomvalues": "npm:14.0.2"
|
||||
"@polkadot/x-bigint": "npm:14.0.3"
|
||||
"@polkadot/x-randomvalues": "npm:14.0.3"
|
||||
"@quantus/crypto": "npm:^0.1.0"
|
||||
"@scure/base": "npm:^1.1.7"
|
||||
"@scure/sr25519": "npm:^0.2.0"
|
||||
tslib: "npm:^2.8.0"
|
||||
peerDependencies:
|
||||
"@polkadot/util": 14.0.2
|
||||
"@polkadot/util": 14.0.3
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@polkadot/util@npm:14.0.2, @polkadot/util@workspace:packages/util":
|
||||
"@polkadot/util@npm:14.0.3, @polkadot/util@workspace:packages/util":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@polkadot/util@workspace:packages/util"
|
||||
dependencies:
|
||||
"@polkadot/x-bigint": "npm:14.0.2"
|
||||
"@polkadot/x-global": "npm:14.0.2"
|
||||
"@polkadot/x-randomvalues": "npm:14.0.2"
|
||||
"@polkadot/x-textdecoder": "npm:14.0.2"
|
||||
"@polkadot/x-textencoder": "npm:14.0.2"
|
||||
"@polkadot/x-bigint": "npm:14.0.3"
|
||||
"@polkadot/x-global": "npm:14.0.3"
|
||||
"@polkadot/x-randomvalues": "npm:14.0.3"
|
||||
"@polkadot/x-textdecoder": "npm:14.0.3"
|
||||
"@polkadot/x-textencoder": "npm:14.0.3"
|
||||
"@types/bn.js": "npm:^5.1.6"
|
||||
bn.js: "npm:^5.2.1"
|
||||
tslib: "npm:^2.8.0"
|
||||
@@ -1756,11 +1758,11 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@polkadot/x-bigint@npm:14.0.2, @polkadot/x-bigint@workspace:packages/x-bigint":
|
||||
"@polkadot/x-bigint@npm:14.0.3, @polkadot/x-bigint@workspace:packages/x-bigint":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@polkadot/x-bigint@workspace:packages/x-bigint"
|
||||
dependencies:
|
||||
"@polkadot/x-global": "npm:14.0.2"
|
||||
"@polkadot/x-global": "npm:14.0.3"
|
||||
tslib: "npm:^2.8.0"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
@@ -1769,7 +1771,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@polkadot/x-bundle@workspace:packages/x-bundle"
|
||||
dependencies:
|
||||
"@polkadot/util": "npm:14.0.2"
|
||||
"@polkadot/util": "npm:14.0.3"
|
||||
buffer-es6: "npm:^4.9.3"
|
||||
tslib: "npm:^2.8.0"
|
||||
languageName: unknown
|
||||
@@ -1779,13 +1781,13 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@polkadot/x-fetch@workspace:packages/x-fetch"
|
||||
dependencies:
|
||||
"@polkadot/x-global": "npm:14.0.2"
|
||||
"@polkadot/x-global": "npm:14.0.3"
|
||||
node-fetch: "npm:^3.3.2"
|
||||
tslib: "npm:^2.8.0"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@polkadot/x-global@npm:14.0.2, @polkadot/x-global@workspace:packages/x-global":
|
||||
"@polkadot/x-global@npm:14.0.3, @polkadot/x-global@workspace:packages/x-global":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@polkadot/x-global@workspace:packages/x-global"
|
||||
dependencies:
|
||||
@@ -1793,35 +1795,35 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@polkadot/x-randomvalues@npm:14.0.2, @polkadot/x-randomvalues@workspace:packages/x-randomvalues":
|
||||
"@polkadot/x-randomvalues@npm:14.0.3, @polkadot/x-randomvalues@workspace:packages/x-randomvalues":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@polkadot/x-randomvalues@workspace:packages/x-randomvalues"
|
||||
dependencies:
|
||||
"@polkadot/util": "npm:14.0.2"
|
||||
"@polkadot/util": "npm:14.0.3"
|
||||
"@polkadot/wasm-util": "npm:^7.5.3"
|
||||
"@polkadot/x-global": "npm:14.0.2"
|
||||
"@polkadot/x-global": "npm:14.0.3"
|
||||
"@types/react-native": "npm:^0.73.0"
|
||||
tslib: "npm:^2.8.0"
|
||||
peerDependencies:
|
||||
"@polkadot/util": 14.0.2
|
||||
"@polkadot/util": 14.0.3
|
||||
"@polkadot/wasm-util": "*"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@polkadot/x-textdecoder@npm:14.0.2, @polkadot/x-textdecoder@workspace:packages/x-textdecoder":
|
||||
"@polkadot/x-textdecoder@npm:14.0.3, @polkadot/x-textdecoder@workspace:packages/x-textdecoder":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@polkadot/x-textdecoder@workspace:packages/x-textdecoder"
|
||||
dependencies:
|
||||
"@polkadot/x-global": "npm:14.0.2"
|
||||
"@polkadot/x-global": "npm:14.0.3"
|
||||
tslib: "npm:^2.8.0"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@polkadot/x-textencoder@npm:14.0.2, @polkadot/x-textencoder@workspace:packages/x-textencoder":
|
||||
"@polkadot/x-textencoder@npm:14.0.3, @polkadot/x-textencoder@workspace:packages/x-textencoder":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@polkadot/x-textencoder@workspace:packages/x-textencoder"
|
||||
dependencies:
|
||||
"@polkadot/x-global": "npm:14.0.2"
|
||||
"@polkadot/x-global": "npm:14.0.3"
|
||||
tslib: "npm:^2.8.0"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
@@ -1830,13 +1832,23 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@polkadot/x-ws@workspace:packages/x-ws"
|
||||
dependencies:
|
||||
"@polkadot/x-global": "npm:14.0.2"
|
||||
"@polkadot/x-global": "npm:14.0.3"
|
||||
"@types/ws": "npm:^8.5.12"
|
||||
tslib: "npm:^2.8.0"
|
||||
ws: "npm:^8.18.0"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@quantus/crypto@npm:^0.1.0":
|
||||
version: 0.1.0
|
||||
resolution: "@quantus/crypto@npm:0.1.0::__archiveUrl=https%3A%2F%2Fgit.lair.cafe%2Fapi%2Fpackages%2Fquantus%2Fnpm%2F%2540quantus%252Fcrypto%2F-%2F0.1.0%2Fcrypto-0.1.0.tgz"
|
||||
dependencies:
|
||||
fflate: "npm:^0.8.2"
|
||||
tslib: "npm:^2.7.0"
|
||||
checksum: 10/c6e531e0350d74d863cfa8acfbed0eeacaa2814c2f2628976b73b59a2ff4b6a382dd0d734e34c1c2442ba7e575f0ae12dd153b44fdbfec51b61f212c1ef41fbd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-native-community/cli-clean@npm:12.3.0":
|
||||
version: 12.3.0
|
||||
resolution: "@react-native-community/cli-clean@npm:12.3.0"
|
||||
@@ -6052,6 +6064,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fflate@npm:^0.8.2":
|
||||
version: 0.8.3
|
||||
resolution: "fflate@npm:0.8.3"
|
||||
checksum: 10/6ebf528dc9c56e78e715eac615b009b25dc33e15c1920b11ebba44e6d76181c647756a81a23e19247907496b93aa99928514c53090579a65109e026ac2824aa7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"figures@npm:^3.0.0":
|
||||
version: 3.2.0
|
||||
resolution: "figures@npm:3.2.0"
|
||||
|
||||
Reference in New Issue
Block a user