Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TSSDKv2][3/n] Account and AuthenticationKey Classes #10210

Merged
merged 7 commits into from
Sep 28, 2023
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
168 changes: 168 additions & 0 deletions ecosystem/typescript/sdk_v2/src/core/account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// Copyright © Aptos Foundation
// SPDX-License-Identifier: Apache-2.0

import nacl from "tweetnacl";
import * as bip39 from "@scure/bip39";
import { AccountAddress } from "./account_address";
import { Hex } from "./hex";
import { bytesToHex } from "@noble/hashes/utils";
import { HexInput } from "../types";
import { PrivateKey, PublicKey, Signature } from "../crypto/asymmetric_crypto";
import { derivePath } from "../utils/hd-key";
import { AuthenticationKey } from "../crypto/authentication_key";
import { Ed25519PrivateKey, Ed25519PublicKey, Ed25519Signature } from "../crypto/ed25519";

/**
* Class for creating and managing account on Aptos network
*
* Use this class to create accounts, sign transactions, and more.
* Note: Creating an account instance does not create the account onchain.
*/
export class Account {
/**
* A private key and public key, associated with the given account
*/
readonly publicKey: PublicKey;
readonly privateKey: PrivateKey;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note the private key doesn't apply to the MultiEd25519.

This is a little weird where we'd probably want the ability to have an account that is either:

  1. A single key account -> which can sign directly there with the private key
  2. A non-signable multi-sig account -> which you can verify, but not sign, and won't have a private key.
  3. Future will have passkeys or other types of accounts, which also may not have all the private information directly.

Additionally, for generated docs, let's try to put the comments above each item individually.

Let's roll with this for right now, but we need to clean this up probably mid next week to ensure there's a way to handle signatures with non single private key accounts.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this was the part where I spent the most time thinking on how to optimize the design, in order to be less confusing and complex.

At the end, I feel like if we want to accommodate all the scenarios, this class might start to get a bit complicate very quickly.


/**
* Account address associated with the account
*/
readonly accountAddress: AccountAddress;

/**
* constructor for Account
*
* TODO: This constructor uses the nacl library directly, which only works with ed25519 keys.
* Need to update this to use the new crypto library if new schemes are added.
*
* @param args.privateKey PrivateKey - private key of the account
* @param args.address AccountAddress - address of the account
*
* This method is private because it should only be called by the factory static methods.
* @returns Account
*/
private constructor(args: { privateKey: PrivateKey; address: AccountAddress }) {
0xmaayan marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am thinking of modifying this to

private constructor(args: { privateKey: PrivateKey; publicKey: PublicKey; address: AccountAddress })

So we don't have to worry about schemes. All properties will just get assigned directly.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably add a PrivateKey -> PublicKey function on the PrivateKey. The great thing about asymmetrical key cryptography, is the public key should be derivable from the private key.

You will want to add an optional publicKey though for MultiEd25519

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is multisig account, then we probably don't have private key and cannot derive the public.

Maybe I will just have a optional privateKey in this case.

private constructor(args: { privateKey?: PrivateKey; publicKey: PublicKey; address: AccountAddress })

const { privateKey, address } = args;

// Derive the public key from the private key
const keyPair = nacl.sign.keyPair.fromSeed(privateKey.toUint8Array().slice(0, 32));
this.publicKey = new Ed25519PublicKey({ hexInput: keyPair.publicKey });
Comment on lines +48 to +50
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is still tied to Ed25519. Can we add the derive function?


this.privateKey = privateKey;
this.accountAddress = address;
}

/**
* Derives an account with random private key and address
*
* @returns Account
*/
static generate(): Account {
const keyPair = nacl.sign.keyPair();
const privateKey = new Ed25519PrivateKey({ value: keyPair.secretKey.slice(0, 32) });
const address = new AccountAddress({ data: Account.authKey({ publicKey: keyPair.publicKey }).toUint8Array() });
return new Account({ privateKey, address });
}
Comment on lines +61 to +66
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably change this to either generate_ed25519 or we can take in some sort of option that provides a scheme.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good one! I think taking in a scheme make sense. And probably default to ed25519 if not provided.


/**
* Derives an account with provided private key
*
* @param args.privateKey Hex - private key of the account
* @returns Account
*/
static fromPrivateKey(args: { privateKey: HexInput }): Account {
const privatekeyHex = Hex.fromHexInput({ hexInput: args.privateKey });
const keyPair = nacl.sign.keyPair.fromSeed(privatekeyHex.toUint8Array().slice(0, 32));
const privateKey = new Ed25519PrivateKey({ value: keyPair.secretKey.slice(0, 32) });
const address = new AccountAddress({ data: Account.authKey({ publicKey: keyPair.publicKey }).toUint8Array() });
return new Account({ privateKey, address });
}

/**
* Derives an account with provided private key and address
* This is intended to be used for account that has it's key rotated
*
* @param args.privateKey Hex - private key of the account
* @param args.address AccountAddress - address of the account
* @returns Account
*/
static fromPrivateKeyAndAddress(args: { privateKey: HexInput; address: AccountAddress }): Account {
const privatekeyHex = Hex.fromHexInput({ hexInput: args.privateKey });
const signingKey = nacl.sign.keyPair.fromSeed(privatekeyHex.toUint8Array().slice(0, 32));
const privateKey = new Ed25519PrivateKey({ value: signingKey.secretKey.slice(0, 32) });
return new Account({ privateKey, address: args.address });
}
Comment on lines +68 to +95
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These can probably be a single one with an optional address (unless we had direct feedback that it was confusing).

Additionally, this would be prime for the derive public key, and all.


/**
* Derives an account with bip44 path and mnemonics,
*
* @param path. (e.g. m/44'/637'/0'/0'/0')
* Detailed description: {@link https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki}
* @param mnemonics.
* @returns AptosAccount
*/
Comment on lines +97 to +104
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should mention this is for ed25519, or we rename it somehow.

static fromDerivationPath(args: { path: string; mnemonic: string }): Account {
const { path, mnemonic } = args;
if (!Account.isValidPath({ path })) {
throw new Error("Invalid derivation path");
}

const normalizeMnemonics = mnemonic
.trim()
.split(/\s+/)
.map((part) => part.toLowerCase())
.join(" ");

const { key } = derivePath(path, bytesToHex(bip39.mnemonicToSeedSync(normalizeMnemonics)));

const signingKey = nacl.sign.keyPair.fromSeed(key.slice(0, 32));
const privateKey = new Ed25519PrivateKey({ value: signingKey.secretKey.slice(0, 32) });
const address = new AccountAddress({ data: Account.authKey({ publicKey: signingKey.publicKey }).toUint8Array() });

return new Account({ privateKey, address });
}

/**
* Check's if the derive path is valid
*/
static isValidPath(args: { path: string }): boolean {
return /^m\/44'\/637'\/[0-9]+'\/[0-9]+'\/[0-9]+'+$/.test(args.path);
}

/**
* This key enables account owners to rotate their private key(s)
* associated with the account without changing the address that hosts their account.
* See here for more info: {@link https://aptos.dev/concepts/accounts#single-signer-authentication}
* @returns Authentication key for the associated account
*/
static authKey(args: { publicKey: HexInput }): Hex {
const publicKey = new Ed25519PublicKey({ hexInput: args.publicKey });
const authKey = AuthenticationKey.fromPublicKey({ publicKey });
return authKey.data;
}

/**
* Sign the given message with the private key.
*
* @param args.data in HexInput format
* @returns Signature
*/
sign(args: { data: HexInput }): Signature {
const signature = this.privateKey.sign({ message: args.data });
return signature;
}

/**
* Verify the given message and signature with the public key.
*
* @param args.message raw message data in HexInput format
* @param args.signature signed message Signature
* @returns
*/
verifySignature(args: { message: HexInput; signature: Signature }): boolean {
const { message, signature } = args;
const rawMessage = Hex.fromHexInput({ hexInput: message }).toUint8Array();
return this.publicKey.verifySignature({ data: rawMessage, signature });
}
}
18 changes: 18 additions & 0 deletions ecosystem/typescript/sdk_v2/src/crypto/asymmetric_crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ export abstract class PublicKey implements Serializable, Deserializable<PublicKe
// Verify the given message with the public key and signature.
abstract verifySignature(args: { data: HexInput; signature: Signature }): boolean;

// Convert the public key to bytes or Uint8Array.
abstract toUint8Array(): Uint8Array;

// Convert the public key to a hex string with the 0x prefix.
abstract toString(): string;

// TODO: This should be a static method.
abstract deserialize(deserializer: Deserializer): PublicKey;
abstract serialize(serializer: Serializer): void;
Expand All @@ -17,12 +23,24 @@ export abstract class PrivateKey implements Serializable, Deserializable<Private
// Sign the given message with the private key.
abstract sign(args: { message: HexInput }): Signature;

// Convert the private key to bytes or Uint8Array.
abstract toUint8Array(): Uint8Array;

// Convert the private key to a hex string with the 0x prefix.
abstract toString(): string;

// TODO: This should be a static method.
abstract deserialize(deserializer: Deserializer): PrivateKey;
abstract serialize(serializer: Serializer): void;
}

export abstract class Signature implements Serializable, Deserializable<Signature> {
// Convert the signature to bytes or Uint8Array.
abstract toUint8Array(): Uint8Array;

// Convert the signature to a hex string with the 0x prefix.
abstract toString(): string;

// TODO: This should be a static method.
abstract deserialize(deserializer: Deserializer): Signature;
abstract serialize(serializer: Serializer): void;
Expand Down
104 changes: 104 additions & 0 deletions ecosystem/typescript/sdk_v2/src/crypto/authentication_key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright © Aptos Foundation
// SPDX-License-Identifier: Apache-2.0

import { sha3_256 as sha3Hash } from "@noble/hashes/sha3";
import { AccountAddress, Hex } from "../core";
import { HexInput } from "../types";
import { MultiEd25519PublicKey } from "./multi_ed25519";
import { PublicKey } from "./asymmetric_crypto";

/**
* Each account stores an authentication key. Authentication key enables account owners to rotate
* their private key(s) associated with the account without changing the address that hosts their account.
* @see {@link https://aptos.dev/concepts/accounts | Account Basics}
*
* Note: AuthenticationKey only supports Ed25519 and MultiEd25519 public keys for now.
*
* Account addresses can be derived from AuthenticationKey
*/
export class AuthenticationKey {
// Length of AuthenticationKey in bytes(Uint8Array)
static readonly LENGTH: number = 32;

// Scheme identifier for MultiEd25519 signatures used to derive authentication keys for MultiEd25519 public keys
static readonly MULTI_ED25519_SCHEME: number = 1;

// Scheme identifier for Ed25519 signatures used to derive authentication key for MultiEd25519 public key
static readonly ED25519_SCHEME: number = 0;
Comment on lines +23 to +27
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that a full list of schemes is here:

// TODO: in the future, can tie these to the AccountAuthenticator enum directly with https://github.com/rust-lang/rust/issues/60553
#[derive(Debug)]
#[repr(u8)]
pub enum Scheme {
Ed25519 = 0,
MultiEd25519 = 1,
// ... add more schemes here
/// Scheme identifier used to derive addresses (not the authentication key) of objects and
/// resources accounts. This application serves to domain separate hashes. Without such
/// separation, an adversary could create (and get a signer for) a these accounts
/// when a their address matches matches an existing address of a MultiEd25519 wallet.
DeriveAuid = 251,
DeriveObjectAddressFromObject = 252,
DeriveObjectAddressFromGuid = 253,
DeriveObjectAddressFromSeed = 254,
DeriveResourceAccountAddress = 255,
}

I'd probably put them in numerical order

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add a scheme enum to TS SDK.


// Scheme identifier used when hashing an account's address together with a seed to derive the address (not the
// authentication key) of a resource account.
static readonly DERIVE_RESOURCE_ACCOUNT_SCHEME: number = 255;

// Actual data of AuthenticationKey, in Hex format
public readonly data: Hex;

constructor(args: { data: HexInput }) {
const { data } = args;
const hex = Hex.fromHexInput({ hexInput: data });
if (hex.toUint8Array().length !== AuthenticationKey.LENGTH) {
throw new Error(`Authentication Key length should be ${AuthenticationKey.LENGTH}`);
}
this.data = hex;
}

toString(): string {
return this.data.toString();
}

toUint8Array(): Uint8Array {
return this.data.toUint8Array();
}

/**
* Converts a K-of-N MultiEd25519PublicKey to AuthenticationKey with:
* `auth_key = sha3-256(p_1 | … | p_n | K | 0x01)`. `K` represents the K-of-N required for
* authenticating the transaction. `0x01` is the 1-byte scheme for multisig.
*
* @param multiPublicKey A K-of-N MultiPublicKey
* @returns AuthenticationKey
*/
static fromMultiPublicKey(args: { multiPublicKey: MultiEd25519PublicKey }): AuthenticationKey {
const { multiPublicKey } = args;
const multiPubKeyBytes = multiPublicKey.toUint8Array();

const bytes = new Uint8Array(multiPubKeyBytes.length + 1);
bytes.set(multiPubKeyBytes);
bytes.set([AuthenticationKey.MULTI_ED25519_SCHEME], multiPubKeyBytes.length);

const hash = sha3Hash.create();
hash.update(bytes);

return new AuthenticationKey({ data: hash.digest() });
}

/**
* Converts a PublicKey(s) to AuthenticationKey
*
* @param publicKey
* @returns AuthenticationKey
*/
static fromPublicKey(args: { publicKey: PublicKey }): AuthenticationKey {
const { publicKey } = args;
const pubKeyBytes = publicKey.toUint8Array();

const bytes = new Uint8Array(pubKeyBytes.length + 1);
bytes.set(pubKeyBytes);
bytes.set([AuthenticationKey.ED25519_SCHEME], pubKeyBytes.length);

const hash = sha3Hash.create();
hash.update(bytes);
Comment on lines +85 to +90
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, could reduce duplicate code to a separate function with a different scheme, since all auth keys are derived the same way.


return new AuthenticationKey({ data: hash.digest() });
}

/**
* Derives an account address from AuthenticationKey. Since current AccountAddress is 32 bytes,
* AuthenticationKey bytes are directly translated to AccountAddress.
*
* @returns AccountAddress
*/
derivedAddress(): AccountAddress {
return new AccountAddress({ data: this.data.toUint8Array() });
}
}
2 changes: 1 addition & 1 deletion ecosystem/typescript/sdk_v2/src/crypto/ed25519.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export class Ed25519PrivateKey extends PrivateKey {
/**
* Generate a new random private key.
*
* @returns
* @returns Ed25519PrivateKey
*/
static generate(): Ed25519PrivateKey {
const keyPair = nacl.sign.keyPair();
Expand Down
13 changes: 13 additions & 0 deletions ecosystem/typescript/sdk_v2/src/crypto/multi_ed25519.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Serializer } from "../bcs/serializer";
import { Ed25519PublicKey, Ed25519Signature } from "./ed25519";
import { PublicKey, Signature } from "./asymmetric_crypto";
import { HexInput } from "../types";
import { Hex } from "../core/hex";

export class MultiEd25519PublicKey extends PublicKey {
// Maximum number of public keys supported
Expand Down Expand Up @@ -71,6 +72,10 @@ export class MultiEd25519PublicKey extends PublicKey {
return bytes;
}

toString(): string {
return Hex.fromHexInput({ hexInput: this.toUint8Array() }).toString();
}

verifySignature(args: { data: HexInput; signature: MultiEd25519Signature }): boolean {
throw new Error("TODO - Method not implemented.");
}
Expand Down Expand Up @@ -135,6 +140,10 @@ export class MultiEd25519Signature extends Signature {
);
}

if (signatures.length > MultiEd25519Signature.MAX_SIGNATURES_SUPPORTED) {
throw new Error(`The number of signatures cannot be greater than ${MultiEd25519Signature.MAX_SIGNATURES_SUPPORTED}`);
}

this.signatures = signatures;
this.bitmap = bitmap;
}
Expand All @@ -153,6 +162,10 @@ export class MultiEd25519Signature extends Signature {
return bytes;
}

toString(): string {
return Hex.fromHexInput({ hexInput: this.toUint8Array() }).toString();
}

/**
* Helper method to create a bitmap out of the specified bit positions
* @param bits The bitmap positions that should be set. A position starts at index 0.
Expand Down
Loading