Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

- Return go-core-compatible 56-byte X448 shared secrets from `SigningKey.computeSharedSecret`, replacing the previous 57-byte Edwards encoding.
- Import go-core PBKDF2-SHA3-256 keystores while preserving MAC-validated access to historical CoreBC SHA2 keystores.
- Add independent shared-secret and keystore compatibility vectors for Node and browser builds.

## 1.1.0

- Add the German community mnemonic wordlist from dys2p/wordlists-de as `wordlists.de` and `LangDe` in the extra browser bundle.
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ with the caller. Core uses 57-byte Ed448 private and public keys and
network-prefixed addresses; Ethereum secp256k1 keys are not interchangeable.
Keep private keys and recovery phrases out of logs and source control.

`SigningKey.computeSharedSecret` returns the 56-byte X448 shared secret used
by go-core, accepting the peer's 57-byte Ed448 public key. This changes the
57-byte Edwards-point result returned by version 1.1.0. Applications using
the previous result as key material must account for that change.

PBKDF2 keystore imports recognize go-core's SHA3-256 interpretation of
`hmac-sha256`, with MAC-checked fallback for historical CoreBC SHA2-256
keystores. SHA2-512 keystores remain supported. This does not change the
public `pbkdf2` API, mnemonic derivation, or scrypt keystore exports.

`Contract` accepts JSON or human-readable ABIs and a provider for reads or
signer for writes. Transaction fields use `energyLimit`, `energyPrice`, and
`networkId`.
Expand Down
31 changes: 31 additions & 0 deletions scripts/test-browser.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { readFile } from "node:fs/promises";
import vm from "node:vm";
import { webcrypto } from "node:crypto";
import {
sharedSecretVectors,
pbkdf2KeystoreVectors,
} from "../lib.esm/_tests/crypto-compat-vectors.js";
for (const file of ["dist/corebc.umd.js", "dist/corebc.umd.min.js"]) {
const context = vm.createContext({
crypto: webcrypto,
Expand All @@ -12,6 +16,8 @@ for (const file of ["dist/corebc.umd.js", "dist/corebc.umd.min.js"]) {
AbortController,
fetch,
URL,
sharedSecretVectors,
pbkdf2KeystoreVectors,
});
vm.runInContext(
"globalThis.self = globalThis; globalThis.window = globalThis;",
Expand All @@ -33,6 +39,31 @@ for (const file of ["dist/corebc.umd.js", "dist/corebc.umd.min.js"]) {
`,
context,
);
await vm.runInContext(
`(async () => {
for (const vector of sharedSecretVectors) {
const a = new corebc.SigningKey(vector.privateA);
const b = new corebc.SigningKey(vector.privateB);
if (a.computeSharedSecret(b.publicKey) !== vector.secret || b.computeSharedSecret(a.publicKey) !== vector.secret) throw new Error("browser X448 vector mismatch: " + vector.name);
for (const peer of ["0x" + "ff".repeat(57), "0x" + "00".repeat(57), "0x01" + "00".repeat(56)]) {
let rejected = false;
try { a.computeSharedSecret(peer); } catch { rejected = true; }
if (!rejected) throw new Error("browser accepted an invalid X448 peer");
}
}
for (const vector of pbkdf2KeystoreVectors) {
const json = JSON.stringify(vector.keystore);
if (corebc.decryptKeystoreJsonSync(json, vector.password).privateKey !== vector.privateKey) throw new Error("browser PBKDF2 sync mismatch: " + vector.name);
if ((await corebc.decryptKeystoreJson(json, vector.password)).privateKey !== vector.privateKey) throw new Error("browser PBKDF2 async mismatch: " + vector.name);
for (const decrypt of [corebc.decryptKeystoreJsonSync, corebc.decryptKeystoreJson]) {
let rejected = false;
try { await decrypt(json, "wrong"); } catch (error) { rejected = error.code === "INVALID_ARGUMENT" && error.argument === "password"; }
if (!rejected) throw new Error("browser accepted an incorrect PBKDF2 password");
}
}
})()`,
context,
);
}
for (const file of ["wordlists-extra.js", "wordlists-extra.min.js"]) {
const { LangSk, LangDe } = await import(
Expand Down
131 changes: 131 additions & 0 deletions src.ts/_tests/crypto-compat-vectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Disposable compatibility vectors; no production keys.
// X448 expected outputs were calculated with Python cryptography/OpenSSL;
// the seed vector also matches go-goldilocks TestAdditionalX448Vectors.
// Public-key conversion and seed/scalar selection follow core-coin/ed448 v1.0.2.
// Keystores were generated independently with Node crypto PBKDF2, AES-128-CTR
// and SHA3-256 MACs. The sha3-256 case follows go-core a091fe2d3 getKDFKey;
// SHA2 cases preserve the historical CoreBC import formats.
export const sharedSecretVectors = [
{
name: "seed",
privateA:
"0x030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303",
publicA:
"0x18e2f925aef56fc05910474593ee84c932bc7ea4d352a3416f72d115e60eb58cd862f73ac50886fbaad6354a4eeb01fd9f740491fb11f59580",
privateB:
"0x040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404",
publicB:
"0x4e15975a78604b2d6575f2d3310ca4e4f122226189f5900f744c1ead49408bdf8247843c05ba24fbb26c9b5f81c287cbbe3d3e8a3ad0231d00",
secret:
"0xb5353e76df0e00c528f800f4659a7eab368f29374870979245eed3f2e9bda1351d70ca219ade1eb0b76fcea78af42268d46723ffdd9e36fb",
},
{
name: "mixed",
privateA:
"0x010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101",
publicA:
"0xe0758a33267939a394fb5ccb202ee851cebc2e89c91ac1289e2bfcddfd9ff9fc5694b0f569d7f7e9da16e1cde9301b29f48128b3cbd1168580",
privateB:
"0x020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020280",
publicB:
"0x7feb9357e8aceb83b713ad38f2e43e92050a13e317d58ff08195ff46247bc5dd3120fcc5346667a0bbdb54a66e96aed90b755b47793e849000",
secret:
"0xe34645238d104c7ae8fc8e54cbdcb937270f96648e1f60faee6c9d9121b54ae1179ba8158dea842cbc8ae31557c96b22f1fe8d7cc2cf9375",
},
{
name: "scalar",
privateA:
"0x010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010180",
publicA:
"0x9115fe1455b552b73a73ae0172dc39bb9deec3f91e0f4a9d5bc418ff0217dc50ddc8b982756a35b13597a48459e6b6f9eb24c4bdb05f6d6780",
privateB:
"0x0202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202ff",
publicB:
"0x7feb9357e8aceb83b713ad38f2e43e92050a13e317d58ff08195ff46247bc5dd3120fcc5346667a0bbdb54a66e96aed90b755b47793e849000",
secret:
"0xfabbfd711b429df0a179a9dc29a1d7e1284452d471caf6c80b9b151d60b4de30a560885926e884cb1056aac6ef9977c5e0394913d25cdd74",
},
];
export const pbkdf2KeystoreVectors = [
{
name: "sha3-256",
password: "compatibility-test",
privateKey:
"0x010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101",
address: "0xab41f5e89ba978317da861b7ad06ba3103d41c876098",
keystore: {
version: 3,
address: "ab41f5e89ba978317da861b7ad06ba3103d41c876098",
Crypto: {
cipher: "aes-128-ctr",
cipherparams: {
iv: "03030303030303030303030303030303",
},
ciphertext:
"6662d0dd06a9fce21fb06ad937e0d424794a9ee7638e978ab9015dbbf62ab24ba2f74ce6ddb9b6c0869c61f1f01b0521eee1486d4226adddd5",
mac: "879a98aa1cf740c375ce07358f619fc4dc9d4a8dbed0cf33efd44165ab91ad8d",
kdf: "pbkdf2",
kdfparams: {
c: 2048,
dklen: 32,
prf: "hmac-sha256",
salt: "0202020202020202020202020202020202020202020202020202020202020202",
},
},
},
},
{
name: "sha256",
password: "compatibility-test",
privateKey:
"0x010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101",
address: "0xab41f5e89ba978317da861b7ad06ba3103d41c876098",
keystore: {
version: 3,
address: "ab41f5e89ba978317da861b7ad06ba3103d41c876098",
Crypto: {
cipher: "aes-128-ctr",
cipherparams: {
iv: "03030303030303030303030303030303",
},
ciphertext:
"90adee35ec0cdd076d2e9078b7225af267fc7d075a77b9c079db6d83ac2d797347d8c5c9ba769a6897859cf0f4cb4d5c222d7573c97593d1a7",
mac: "521eabcd4b9407533c23ffe1f83737e8a87f503fe27fe2ca528cda6929c28dbf",
kdf: "pbkdf2",
kdfparams: {
c: 2048,
dklen: 32,
prf: "hmac-sha256",
salt: "0202020202020202020202020202020202020202020202020202020202020202",
},
},
},
},
{
name: "sha512",
password: "compatibility-test",
privateKey:
"0x010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101",
address: "0xab41f5e89ba978317da861b7ad06ba3103d41c876098",
keystore: {
version: 3,
address: "ab41f5e89ba978317da861b7ad06ba3103d41c876098",
Crypto: {
cipher: "aes-128-ctr",
cipherparams: {
iv: "03030303030303030303030303030303",
},
ciphertext:
"5f41211590deff955e6f4bbbd7d339e67234ec9e21ea1a69a4bf359029838e5eedd43746b7fffe11bbb04b1dd8e0f89fd4ca87c45b49c45dfc",
mac: "1e61cbe0b9f1b663fa256193e45327479a4db1a8048ae974b35ade0ad7c95b8c",
kdf: "pbkdf2",
kdfparams: {
c: 2048,
dklen: 32,
prf: "hmac-sha512",
salt: "0202020202020202020202020202020202020202020202020202020202020202",
},
},
},
},
];
24 changes: 23 additions & 1 deletion src.ts/_tests/test-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
keccak256,
} from "../index.js";
import { createHash } from "../crypto/crypto-browser.js";
import { sharedSecretVectors } from "./crypto-compat-vectors.js";

describe("updated crypto dependencies", () => {
it("matches standard hash and scrypt vectors", async () => {
Expand Down Expand Up @@ -40,7 +41,7 @@ describe("updated crypto dependencies", () => {
assert.throws(() => new SigningKey(new Uint8Array(length)));
assert.throws(() => key.sign("0x00"));
});
it("signs, rejects tampering and derives symmetric Ed448 secrets in both key modes", () => {
it("signs, rejects tampering and derives symmetric secrets in both key modes", () => {
const a = new SigningKey("01".repeat(57));
const b = new SigningKey("02".repeat(56) + "80");
const digest = sha256("0x1234");
Expand All @@ -59,4 +60,25 @@ describe("updated crypto dependencies", () => {
);
assert.throws(() => a.computeSharedSecret("0x00"));
});
for (const vector of sharedSecretVectors) {
it(`matches Core X448 shared secrets in ${vector.name} mode`, () => {
const a = new SigningKey(vector.privateA);
const b = new SigningKey(vector.privateB);
assert.equal(a.publicKey, vector.publicA);
assert.equal(b.publicKey, vector.publicB);
assert.equal(a.computeSharedSecret(b.publicKey), vector.secret);
assert.equal(b.computeSharedSecret(a.publicKey), vector.secret);
assert.equal(getBytes(vector.secret).length, 56);
assert.equal(a.privateKey, vector.privateA);
});
}
it("rejects invalid and small-order peers during shared-secret derivation", () => {
const key = new SigningKey(sharedSecretVectors[0].privateA);
for (const peer of [
"0x" + "ff".repeat(57),
"0x" + "00".repeat(57),
"0x01" + "00".repeat(56),
])
assert.throws(() => key.computeSharedSecret(peer));
});
});
67 changes: 67 additions & 0 deletions src.ts/_tests/test-keystore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,73 @@ import {
decryptKeystoreJsonSync,
decryptKeystoreJson,
} from "../index.js";
import { pbkdf2KeystoreVectors } from "./crypto-compat-vectors.js";

describe("PBKDF2 keystore interoperability", () => {
for (const vector of pbkdf2KeystoreVectors) {
it(`imports ${vector.name} wallets with both APIs`, async () => {
const json = JSON.stringify(vector.keystore);
const expected = {
address: vector.address,
privateKey: vector.privateKey,
};
assert.deepEqual(
decryptKeystoreJsonSync(json, vector.password),
expected,
);
const progress: number[] = [];
assert.deepEqual(
await decryptKeystoreJson(json, vector.password, (p) =>
progress.push(p),
),
expected,
);
assert.deepEqual(progress, [0, 1]);
assert.equal(
Wallet.fromEncryptedJsonSync(json, vector.password).address,
vector.address,
);
});
it(`rejects wrong passwords and tampering for ${vector.name}`, async () => {
const json = JSON.stringify(vector.keystore);
assert.throws(
() => decryptKeystoreJsonSync(json, "wrong"),
/incorrect password/,
);
await assert.rejects(
decryptKeystoreJson(json, "wrong"),
/incorrect password/,
);
for (const field of ["mac", "ciphertext"] as const) {
const changed = structuredClone(vector.keystore);
const bytes = changed.Crypto[field];
changed.Crypto[field] =
(bytes.startsWith("00") ? "01" : "00") + bytes.slice(2);
const modified = JSON.stringify(changed);
assert.throws(
() => decryptKeystoreJsonSync(modified, vector.password),
/incorrect password/,
);
await assert.rejects(
decryptKeystoreJson(modified, vector.password),
/incorrect password/,
);
}
});
}
it("retains address validation after authenticating a Core keystore", () => {
const vector = pbkdf2KeystoreVectors[0];
const changed = structuredClone(vector.keystore);
changed.address = Wallet.fromSeed({
seed: "02".repeat(64),
prefix: networkIdToPrefix(3),
}).address.slice(2);
assert.throws(
() => decryptKeystoreJsonSync(JSON.stringify(changed), vector.password),
/address mismatch/,
);
});
});

describe("AES keystores", () => {
const wallet = Wallet.fromSeed({
Expand Down
4 changes: 2 additions & 2 deletions src.ts/crypto/crypto-browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ export function pbkdf2Sync(
salt: Uint8Array,
iterations: number,
keylen: number,
_algo: "sha256" | "sha512",
_algo: "sha256" | "sha512" | "sha3-256",
): Uint8Array {
const algo = { sha256, sha512 }[_algo];
const algo = { sha256, sha512, "sha3-256": sha3_256 }[_algo];
assertArgument(algo != null, "invalid pbkdf2 algorithm", "algorithm", _algo);
return pbkdf2(algo, password, salt, { c: iterations, dkLen: keylen });
}
Expand Down
1 change: 1 addition & 0 deletions src.ts/crypto/ed448-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface Ed448Backend {
publicKeyCreate(key: Buffer): Buffer;
publicKeyFromScalar(scalar: Buffer): Buffer;
publicKeyVerify(key: Buffer): boolean;
publicKeyConvert(key: Buffer): Buffer;
publicKeyCombine(keys: Buffer[]): Buffer;
sign(message: Buffer, key: Buffer): Buffer;
signWithScalar(message: Buffer, scalar: Buffer, prefix: Buffer): Buffer;
Expand Down
10 changes: 7 additions & 3 deletions src.ts/crypto/signing-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export class SigningKey {
return hexConcat([hexlified, this.publicKey]);
}

/** Derives an Ed448 shared secret from a 57-byte public key. Hash it before use as a symmetric key. */
/** Derives the 56-byte X448 shared secret used by go-core from a 57-byte Ed448 public key. Hash it before use as a symmetric key. */
computeSharedSecret(other: BytesLike): string {
const pub = Buffer.from(getBytesCopy(other));
assertArgument(
Expand All @@ -114,9 +114,13 @@ export class SigningKey {
const scalar = key.subarray(0, 56);
scalar[0] &= 0xfc;
scalar[55] |= 0x80;
return hexlify(ed448.deriveWithScalar(pub, scalar));
return hexlify(
ed448.publicKeyConvert(ed448.deriveWithScalar(pub, scalar)),
);
}
return hexlify(ed448.derive(pub, key));
// The Edwards-to-Montgomery map commutes with scalar multiplication.
// Encode the shared point as X448, matching go-core's Ed448DeriveSecret.
return hexlify(ed448.publicKeyConvert(ed448.derive(pub, key)));
}

/** Computes an Ed448 public key from a 57-byte private key. The compression flag is ignored. */
Expand Down
Loading