Skip to content

Commit

Permalink
[TSSDKv2][3/n] Account and AuthenticationKey Classes (#10210)
Browse files Browse the repository at this point in the history
* Add helper.ts and fixes based on comments

* add asymmetric_crypto file which includes all crypto base classes as abstract. And have all concrete crypto classes to extend from based abstract classes

* Update crypto classes name to include Ed25519 prefix

* fixes based on comments

* Add account and auth key classes

* update Account class to accept abstract PublicKey and PrivateKey

* update methods' comment
  • Loading branch information
0xmigo authored Sep 28, 2023
1 parent 6d749f1 commit 8596570
Show file tree
Hide file tree
Showing 9 changed files with 503 additions and 1 deletion.
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;

/**
* 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 }) {
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 });

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 });
}

/**
* 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 });
}

/**
* 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
*/
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;

// 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);

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

0 comments on commit 8596570

Please sign in to comment.