-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
Changes from all commits
2208ec7
bb2232c
7c4cc93
83a1599
8e44808
1c78b70
f5a97b0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 }) { | ||
0xmaayan marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am thinking of modifying this to
So we don't have to worry about schemes. All properties will just get assigned directly. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
|
||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should probably change this to either There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 }); | ||
} | ||
} |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that a full list of schemes is here: aptos-core/types/src/transaction/authenticator.rs Lines 349 to 365 in c2c8ce3
I'd probably put them in numerical order There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() }); | ||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||
} |
There was a problem hiding this comment.
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:
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.
There was a problem hiding this comment.
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.