From 58bcca00534f65a96682a07ac45e9f2644202eaa Mon Sep 17 00:00:00 2001 From: Pierre Seznec Date: Fri, 26 Apr 2024 18:23:38 +0200 Subject: [PATCH] [xp] refactor keys class --- .../massa-web3/src/experimental/account.ts | 21 +-- .../src/experimental/basicElements/keys.ts | 128 +++++++++++------- .../experimental/basicElements/signature.ts | 3 +- .../test/experimental/unit/account.spec.ts | 4 +- .../test/experimental/unit/key.spec.ts | 31 +---- .../unit/operationManager.spec.ts | 2 +- 6 files changed, 93 insertions(+), 96 deletions(-) diff --git a/packages/massa-web3/src/experimental/account.ts b/packages/massa-web3/src/experimental/account.ts index 516b1ead..51721fb8 100644 --- a/packages/massa-web3/src/experimental/account.ts +++ b/packages/massa-web3/src/experimental/account.ts @@ -1,6 +1,5 @@ import { Version } from './crypto/interfaces/versioner' import Sealer from './crypto/interfaces/sealer' -import VarintVersioner from './crypto/varintVersioner' import { PasswordSeal } from './crypto/passwordSeal' import { Address, PrivateKey, PublicKey, Signature } from './basicElements' import { readFileSync, existsSync } from 'fs' @@ -186,13 +185,9 @@ export class Account { keystore.Nonce ) const privateKeyBytes = await passwordSeal.unseal(keystore.CipheredData) - const privateKey = PrivateKey.fromBytes(privateKeyBytes, Version.V0) + const privateKey = PrivateKey.fromBytes(privateKeyBytes) const publicKey = PublicKey.fromBytes( - // TODO: The PublicKey class should be refactored to ensure consistency between the fromBytes and toBytes methods, - // similar to what was done for the address class. - // eslint-disable-next-line @typescript-eslint/no-magic-numbers - new Uint8Array(keystore.PublicKey).subarray(1), - Version.V0 + Uint8Array.from(keystore.PublicKey) ) const address = publicKey.getAddress() // TODO: add a consistency check with the address in the keystore @@ -210,18 +205,10 @@ export class Account { ) const privateKeyBytes = await passwordSeal.unseal(keystore.CipheredData) - const varintVersioner = new VarintVersioner() - const { data: bytes, version: numberVersion } = - varintVersioner.extract(privateKeyBytes) - const version = numberVersion as Version - const privateKey = PrivateKey.fromBytes(bytes, version) + const privateKey = PrivateKey.fromBytes(privateKeyBytes) const publicKey = PublicKey.fromBytes( - // TODO: The PublicKey class should be refactored to ensure consistency between the fromBytes and toBytes methods, - // similar to what was done for the address class. - // eslint-disable-next-line @typescript-eslint/no-magic-numbers - new Uint8Array(keystore.PublicKey).subarray(1), - Version.V0 + Uint8Array.from(keystore.PublicKey) ) const address = publicKey.getAddress() // TODO: add a consistency check with the address in the keystore diff --git a/packages/massa-web3/src/experimental/basicElements/keys.ts b/packages/massa-web3/src/experimental/basicElements/keys.ts index edef9f91..3ff2918e 100644 --- a/packages/massa-web3/src/experimental/basicElements/keys.ts +++ b/packages/massa-web3/src/experimental/basicElements/keys.ts @@ -8,11 +8,25 @@ import Serializer from '../crypto/interfaces/serializer' import Signer from '../crypto/interfaces/signer' import { Version, Versioner } from '../crypto/interfaces/versioner' import VarintVersioner from '../crypto/varintVersioner' -import { mustExtractPrefix, extractData } from './internal' const PRIVATE_KEY_PREFIX = 'S' const PUBLIC_KEY_PREFIX = 'P' +/** + * Get the version from string or bytes key. + * + * @remarks + * For now the function is common for private & public key but it might change in the futur. + * + * @returns the key version. + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function getVersion(data: string | Uint8Array): Version { + // When a new version will come, implement the logic to detect version here + // This should be done without serializer and versionner as they are potentially not known at this point + return Version.V0 +} + /** * A class representing a private key. * @@ -27,7 +41,9 @@ const PUBLIC_KEY_PREFIX = 'P' * - Voila! The code will automatically handle the new version. */ export class PrivateKey { - public bytes: Uint8Array + // The key in byte format. Version included. + private bytes: Uint8Array + private prefix = PRIVATE_KEY_PREFIX // eslint-disable-next-line max-params protected constructor( @@ -60,42 +76,46 @@ export class PrivateKey { } } + private checkPrefix(str: string): void { + if (!str.startsWith(this.prefix)) { + throw new Error( + `invalid private key prefix: ${this.prefix} was expected.` + ) + } + } + /** * Initializes a new private key object from a serialized string. * * @param str - The serialized private key string. - * @param version - The version of the private key. If not defined, the last version will be used. * * @returns A new private key instance. * * @throws If the private key prefix is invalid. */ - public static fromString(str: string, version?: Version): PrivateKey { - const privateKey = PrivateKey.initFromVersion(version) - + public static fromString(str: string): PrivateKey { try { - mustExtractPrefix(str, PRIVATE_KEY_PREFIX) - privateKey.bytes = extractData( - privateKey.serializer, - privateKey.versioner, - str.slice(PRIVATE_KEY_PREFIX.length), - privateKey.version + const version = getVersion(str) + const privateKey = PrivateKey.initFromVersion(version) + privateKey.checkPrefix(str) + privateKey.bytes = privateKey.serializer.deserialize( + str.slice(privateKey.prefix.length) ) + return privateKey } catch (e) { throw new Error(`invalid private key string: ${e.message}`) } - return privateKey } /** - * Initializes a new private key object from a raw byte array and a version. + * Initializes a new private key object from a byte array. * - * @param bytes - The raw bytes without any prefix version. - * @param version - The version of the private key. If not defined, the last version will be used. + * @param bytes - The private key in byte format. * * @returns A new private key instance. */ - public static fromBytes(bytes: Uint8Array, version?: Version): PrivateKey { + public static fromBytes(bytes: Uint8Array): PrivateKey { + const version = getVersion(bytes) const privateKey = PrivateKey.initFromVersion(version) privateKey.bytes = bytes return privateKey @@ -125,7 +145,8 @@ export class PrivateKey { */ public static generate(version?: Version): PrivateKey { const privateKey = PrivateKey.initFromVersion(version) - privateKey.bytes = privateKey.signer.generatePrivateKey() + const rawBytes = privateKey.signer.generatePrivateKey() + privateKey.bytes = privateKey.versioner.attach(privateKey.version, rawBytes) return privateKey } @@ -150,22 +171,18 @@ export class PrivateKey { * @returns The signature byte array. */ public async sign(message: Uint8Array): Promise { - const signatureRawBytes = await this.signer.sign( - this.bytes, - this.hasher.hash(message) - ) - return Signature.fromBytes( - this.versioner.attach(this.version, signatureRawBytes) - ) + const { data } = this.versioner.extract(this.bytes) + const signature = await this.signer.sign(data, this.hasher.hash(message)) + return Signature.fromBytes(this.versioner.attach(this.version, signature)) } /** - * Versions the private key bytes. + * Private key in bytes. * * @returns The versioned private key bytes. */ public toBytes(): Uint8Array { - return this.versioner.attach(this.version, this.bytes) + return this.bytes } /** @@ -179,7 +196,7 @@ export class PrivateKey { * @returns The serialized private key string. */ public toString(): string { - return `${PRIVATE_KEY_PREFIX}${this.serializer.serialize(this.toBytes())}` + return `${this.prefix}${this.serializer.serialize(this.bytes)}` } } @@ -198,7 +215,9 @@ export class PrivateKey { * - Voila! The code will automatically handle the new version. */ export class PublicKey { - public bytes: Uint8Array + // The key in byte format. Version included. + private bytes: Uint8Array + private prefix = PUBLIC_KEY_PREFIX // eslint-disable-next-line max-params protected constructor( @@ -233,43 +252,44 @@ export class PublicKey { } } + private checkPrefix(str: string): void { + if (!str.startsWith(this.prefix)) { + throw new Error(`invalid public key prefix: ${this.prefix} was expected.`) + } + } + /** * Initializes a new public key object from a serialized string. * * @param str - The serialized public key string. - * @param version - The version of the public key. If not defined, the last version will be used. * * @returns A new public key instance. * - * @throws If the public key string is invalid. + * @throws If the public key prefix is invalid. */ - public static fromString(str: string, version?: Version): PublicKey { - const publicKey = PublicKey.initFromVersion(version) - + public static fromString(str: string): PublicKey { try { - mustExtractPrefix(str, PUBLIC_KEY_PREFIX) - publicKey.bytes = extractData( - publicKey.serializer, - publicKey.versioner, - str.slice(PUBLIC_KEY_PREFIX.length), - publicKey.version + const version = getVersion(str) + const publicKey = PublicKey.initFromVersion(version) + publicKey.checkPrefix(str) + publicKey.bytes = publicKey.serializer.deserialize( + str.slice(publicKey.prefix.length) ) + return publicKey } catch (e) { throw new Error(`invalid public key string: ${e.message}`) } - - return publicKey } /** - * Initializes a new public key object from a raw byte array and a version. + * Initializes a new public key object from a byte array. * - * @param bytes - The raw bytes without any prefix version. - * @param version - The version of the public key. If not defined, the last version will be used. + * @param bytes - The public key in byte format. * * @returns A new public key instance. */ - public static fromBytes(bytes: Uint8Array, version?: Version): PublicKey { + public static fromBytes(bytes: Uint8Array): PublicKey { + const version = getVersion(bytes) const publicKey = PublicKey.initFromVersion(version) publicKey.bytes = bytes return publicKey @@ -286,7 +306,12 @@ export class PublicKey { privateKey: PrivateKey ): Promise { const publicKey = PublicKey.initFromVersion() - publicKey.bytes = await publicKey.signer.getPublicKey(privateKey.bytes) + const { data } = publicKey.versioner.extract(privateKey.toBytes()) + const publicKeyBytes = await publicKey.signer.getPublicKey(data) + publicKey.bytes = publicKey.versioner.attach( + publicKey.version, + publicKeyBytes + ) return publicKey } @@ -316,20 +341,21 @@ export class PublicKey { signature: Signature ): Promise { const { data: rawSignature } = this.versioner.extract(signature.toBytes()) + const { data: rawPublicKey } = this.versioner.extract(this.bytes) return await this.signer.verify( - this.bytes, + rawPublicKey, this.hasher.hash(data), rawSignature ) } /** - * Versions the public key bytes. + * Public key in bytes. * * @returns The versioned public key bytes. */ public toBytes(): Uint8Array { - return this.versioner.attach(this.version, this.bytes) + return this.bytes } /** @@ -343,6 +369,6 @@ export class PublicKey { * @returns The serialized public key string. */ public toString(): string { - return `${PUBLIC_KEY_PREFIX}${this.serializer.serialize(this.toBytes())}` + return `${this.prefix}${this.serializer.serialize(this.bytes)}` } } diff --git a/packages/massa-web3/src/experimental/basicElements/signature.ts b/packages/massa-web3/src/experimental/basicElements/signature.ts index 805963b1..9de13e10 100644 --- a/packages/massa-web3/src/experimental/basicElements/signature.ts +++ b/packages/massa-web3/src/experimental/basicElements/signature.ts @@ -91,8 +91,9 @@ export class Signature { const version = getVersion(bytes) const signature = Signature.initFromVersion(version) signature.bytes = bytes - const { version: extractedVersion } = signature.versioner.extract(bytes) + // safety check + const { version: extractedVersion } = signature.versioner.extract(bytes) if (extractedVersion !== version) { throw new Error( `invalid version: ${version}. ${signature.version} was expected.` diff --git a/packages/massa-web3/test/experimental/unit/account.spec.ts b/packages/massa-web3/test/experimental/unit/account.spec.ts index b38a1083..98ced2c5 100644 --- a/packages/massa-web3/test/experimental/unit/account.spec.ts +++ b/packages/massa-web3/test/experimental/unit/account.spec.ts @@ -4,8 +4,8 @@ import { Account, AccountKeyStore } from '../../../src/experimental/account' import { Version } from '../../../src/experimental/crypto/interfaces/versioner' import path from 'path' -describe('Basic use cases', () => { - test('Account - from private key', async () => { +describe('Account tests', () => { + test('from private key', async () => { const account = await Account.fromPrivateKey( 'S12jWf59Yzf2LimL89soMnAP2VEBDBpfCbZLoEFo36CxEL3j92rZ' ) diff --git a/packages/massa-web3/test/experimental/unit/key.spec.ts b/packages/massa-web3/test/experimental/unit/key.spec.ts index c4f92b79..c86c2a08 100644 --- a/packages/massa-web3/test/experimental/unit/key.spec.ts +++ b/packages/massa-web3/test/experimental/unit/key.spec.ts @@ -3,9 +3,6 @@ import { PrivateKey, PublicKey, } from '../../../src/experimental/basicElements/keys' -import { Version } from '../../../src/experimental/crypto/interfaces/versioner' - -const invalidVersion = -1 as Version describe('PrivateKey and PublicKey tests', () => { let privateKey: PrivateKey @@ -19,14 +16,14 @@ describe('PrivateKey and PublicKey tests', () => { describe('Conversion to and from Bytes', () => { test('PublicKey toBytes and fromBytes', async () => { const publicKey = await PublicKey.fromPrivateKey(privateKey) - const newPubKeyBytes = PublicKey.fromBytes(publicKey.bytes) - expect(newPubKeyBytes.bytes).toEqual(publicKey.bytes) + const newPubKeyBytes = PublicKey.fromBytes(publicKey.toBytes()) + expect(newPubKeyBytes.toBytes()).toEqual(publicKey.toBytes()) }) test('PrivateKey toBytes and fromBytes', async () => { const privateKey = PrivateKey.generate() - const newPKeyBytes = PrivateKey.fromBytes(privateKey.bytes) - expect(newPKeyBytes.bytes).toEqual(privateKey.bytes) + const newPKeyBytes = PrivateKey.fromBytes(privateKey.toBytes()) + expect(newPKeyBytes.toBytes()).toEqual(privateKey.toBytes()) }) }) @@ -79,23 +76,9 @@ describe('PrivateKey and PublicKey tests', () => { test('fromString throws error for invalid public key string', () => { const invalidPublicKeyString = 'invalidPublicKey' - expect(() => - PublicKey.fromString(invalidPublicKeyString, Version.V0) - ).toThrow(/invalid public key string/) - }) - - test('fromString throws error for invalid public key version', () => { - const publicKeyString = publicKey.toString() - expect(() => - PublicKey.fromString(publicKeyString, invalidVersion) - ).toThrow(/unsupported version/) - }) - test('fromString throws error for invalid private key version', () => { - const privateKeyString = privateKey.toString() - - expect(() => - PrivateKey.fromString(privateKeyString, invalidVersion) - ).toThrow(/unsupported version/) + expect(() => PublicKey.fromString(invalidPublicKeyString)).toThrow( + /invalid public key string/ + ) }) }) diff --git a/packages/massa-web3/test/experimental/unit/operationManager.spec.ts b/packages/massa-web3/test/experimental/unit/operationManager.spec.ts index 2a55d4d8..99ed1d97 100644 --- a/packages/massa-web3/test/experimental/unit/operationManager.spec.ts +++ b/packages/massa-web3/test/experimental/unit/operationManager.spec.ts @@ -11,7 +11,7 @@ import { import { PrivateKey, Address } from '../../../src/experimental/basicElements' import 'dotenv/config' -describe('Unit tests', () => { +describe('Operation manager tests', () => { test('serialize - transfer', async () => { const transfer: TransferOperation = { fee: 1n,