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

Introduce Single Sender transaction authenticator #84

Merged
merged 10 commits into from
Oct 25, 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ All notable changes to the Aptos TypeScript SDK will be captured in this file. T
- Support to config a custom client instance
- Changed all Regex based inputs requiring a `0x` to be optional. This is to allow for easier copy/pasting of addresses and keys.
- Change GetAccountResource to take in a generic output type that matches the struct
- Add support for Single Sender

## 0.0.0 (2023-10-18)

Expand Down
21 changes: 20 additions & 1 deletion src/api/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import { AptosConfig } from "./aptosConfig";
import { AccountAddress } from "../core";
import { AccountAddress, PrivateKey, Account as AccountModule } from "../core";
0xmaayan marked this conversation as resolved.
Show resolved Hide resolved
import {
AccountData,
GetAccountCoinsDataResponse,
Expand All @@ -21,6 +21,7 @@ import {
TransactionResponse,
} from "../types";
import {
deriveAccountFromPrivateKey,
getAccountCoinsCount,
getAccountCoinsData,
getAccountCollectionsWithOwnedTokens,
Expand Down Expand Up @@ -358,4 +359,22 @@ export class Account {
...args,
});
}

/**
* Derives an account by providing a private key.
* This functions resolves the provided private key type and derives the public key from it.
*
* If the privateKey is a Secp256k1 type, it derives the account using the derived public key and
* auth key using the SingleKey scheme locally.
*
* If the privateKey is a ED25519 type, it looks up the authentication key on chain, and uses it to resolve
* whether it is a Legacy ED25519 key or a Unified ED25519 key. It then derives the account based
0xmaayan marked this conversation as resolved.
Show resolved Hide resolved
* on that.
*
* @param args.privateKey An account private key
* @returns Account type
*/
async deriveAccountFromPrivateKey(args: { privateKey: PrivateKey }): Promise<AccountModule> {
0xmaayan marked this conversation as resolved.
Show resolved Hide resolved
return deriveAccountFromPrivateKey({ aptosConfig: this.config, ...args });
}
}
1 change: 1 addition & 0 deletions src/client/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export async function aptosRequest<Req, Res>(
}

const errorMessage = errors[result.status];

throw new AptosApiError(
options,
result,
Expand Down
107 changes: 74 additions & 33 deletions src/core/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,31 @@ import { Ed25519PrivateKey, Ed25519PublicKey } from "./crypto/ed25519";
import { MultiEd25519PublicKey } from "./crypto/multiEd25519";
import { Secp256k1PrivateKey, Secp256k1PublicKey } from "./crypto/secp256k1";
import { Hex } from "./hex";
import { HexInput, SigningScheme } from "../types";
import { GenerateAccount, HexInput, SigningScheme, SigningSchemeInput } from "../types";
import { derivePrivateKeyFromMnemonic, KeyType } from "../utils/hdKey";
import { AnyPublicKey } from "./crypto/anyPublicKey";

/**
* 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 on-chain.
*
* Since [AIP-55](https://github.com/aptos-foundation/AIPs/pull/263) Aptos supports
* `Legacy` and `Unified` authentications.
*
* @Legacy includes `ED25519` and `MultiED25519`
* @Unified includes `SingleSender` and `MultiSender`, where currently
* `SingleSender` supports `ED25519` and `Secp256k1`, and `MultiSender` supports
* `MultiED25519`.
*
* In TypeScript SDK, we support all of these options
* @generate default to generate Unified keys, with an optional `legacy` boolean argument
* that lets you generate new keys conforming to the Legacy authentication.
* @fromPrivateKey derives an account by a provided private key and address, with an optional
* `legacy` boolean argument that lets you generate new keys conforming to the Legacy authentication.
* @fromDerivationPath derives an account with bip44 path and mnemonics,
*
*/
export class Account {
/**
Expand Down Expand Up @@ -45,23 +62,31 @@ export class Account {
*
* @param args.privateKey PrivateKey - private key of the account
* @param args.address AccountAddress - address of the account
* @param args.legacy optional. If set to true, the keypair generated is a Legacy keypair. Defaults
* to generating a Unified keypair
*
* 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;
private constructor(args: { privateKey: PrivateKey; address: AccountAddress; legacy?: boolean }) {
const { privateKey, address, legacy } = args;
0xmaayan marked this conversation as resolved.
Show resolved Hide resolved

// Derive the public key from the private key
this.publicKey = privateKey.publicKey();

// Derive the signing scheme from the public key
if (this.publicKey instanceof Ed25519PublicKey) {
this.signingScheme = SigningScheme.Ed25519;
if (legacy) {
this.signingScheme = SigningScheme.Ed25519;
} else {
this.publicKey = new AnyPublicKey(this.publicKey);
this.signingScheme = SigningScheme.SingleKey;
}
} else if (this.publicKey instanceof MultiEd25519PublicKey) {
this.signingScheme = SigningScheme.MultiEd25519;
} else if (this.publicKey instanceof Secp256k1PublicKey) {
this.signingScheme = SigningScheme.Secp256k1Ecdsa;
this.publicKey = new AnyPublicKey(this.publicKey);
this.signingScheme = SigningScheme.SingleKey;
} else {
throw new Error("Can not create new Account, unsupported public key type");
}
Expand All @@ -71,56 +96,70 @@ export class Account {
}

/**
* Derives an account with random private key and address
* Derives an account with random private key and address.
* Default generation is using the Unified flow with ED25519 key
*
* @param args optional. Unify GenerateAccount type for Legacy and Unified keys
*
* Account input type to generate an account using Legacy
* Ed25519 or MultiEd25519 keys or without a specified `scheme`.
* ```
* GenerateAccountWithLegacyKey = {
* scheme?: SigningSchemeInput.Ed25519 | SigningSchemeInput.MultiEd25519;
* legacy: true;
* };
* ```
*
* @param scheme optional SigningScheme - type of SigningScheme to use. Default to Ed25519
* Currently only Ed25519 and Secp256k1 are supported
* Account input type to generate an account using Unified
* Secp256k1Ecdsa key
* In this case `legacy` is always false
* ```
* GenerateAccountWithUnifiedKey = {
* scheme: SigningSchemeInput.Secp256k1Ecdsa;
0xmaayan marked this conversation as resolved.
Show resolved Hide resolved
* legacy?: false;
* };
* ```
*
* @returns Account with the given signing scheme
*/
static generate(scheme?: SigningScheme): Account {
static generate(args?: GenerateAccount): Account {
let privateKey: PrivateKey;

switch (scheme) {
case SigningScheme.Secp256k1Ecdsa:
switch (args?.scheme) {
case SigningSchemeInput.Secp256k1Ecdsa:
privateKey = Secp256k1PrivateKey.generate();
break;
// TODO: Add support for MultiEd25519
// TODO: Add support for MultiEd25519 as AnyMultiKey
default:
privateKey = Ed25519PrivateKey.generate();
}

let publicKey = privateKey.publicKey();
if (!args?.legacy) {
publicKey = new AnyPublicKey(privateKey.publicKey());
}

const address = new AccountAddress({
data: Account.authKey({
publicKey: privateKey.publicKey(),
publicKey, // TODO support AnyMultiKey
}).toUint8Array(),
});
return new Account({ privateKey, address });
return new Account({ privateKey, address, legacy: args?.legacy });
}

/**
* Derives an account with provided private key
*
* @param privateKey Hex - private key of the account
* @returns Account
*/
static fromPrivateKey(privateKey: PrivateKey): Account {
const publicKey = privateKey.publicKey();
const authKey = Account.authKey({ publicKey });
const address = new AccountAddress({ data: authKey.toUint8Array() });
return Account.fromPrivateKeyAndAddress({ 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 privateKey PrivateKey - private key of the account
* @param address The account address
* @param args.legacy optional. If set to true, the keypair generated is a Legacy keypair. Defaults
* to generating a Unified keypair
*
* @param args.privateKey Hex - private key of the account
* @param args.address AccountAddress - address of the account
* @returns Account
*/
static fromPrivateKeyAndAddress(args: { privateKey: PrivateKey; address: AccountAddress }): Account {
return new Account(args);
static fromPrivateKey(args: { privateKey: PrivateKey; address: AccountAddress; legacy?: boolean }): Account {
const { privateKey, address, legacy } = args;
return new Account({ privateKey, address, legacy });
}

/**
Expand All @@ -133,10 +172,12 @@ export class Account {
*/
static fromDerivationPath(args: { path: string; mnemonic: string }): Account {
const { path, mnemonic } = args;

const { key } = derivePrivateKeyFromMnemonic(KeyType.ED25519, path, mnemonic);
const privateKey = new Ed25519PrivateKey(key);
return Account.fromPrivateKey(privateKey);
const publicKey = privateKey.publicKey();
const authKey = Account.authKey({ publicKey });
0xmaayan marked this conversation as resolved.
Show resolved Hide resolved
const address = new AccountAddress({ data: authKey.toUint8Array() });
return new Account({ privateKey, address, legacy: true });
0xmaayan marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down
42 changes: 29 additions & 13 deletions src/core/authenticationKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { AccountAddress } from "./accountAddress";
import { PublicKey } from "./crypto/asymmetricCrypto";
import { Ed25519PublicKey } from "./crypto/ed25519";
import { MultiEd25519PublicKey } from "./crypto/multiEd25519";
import { Secp256k1PublicKey } from "./crypto/secp256k1";
import { Hex } from "./hex";
import { AuthenticationKeyScheme, HexInput, SigningScheme } from "../types";
import { AnyPublicKey } from "./crypto/anyPublicKey";

/**
* Each account stores an authentication key. Authentication key enables account owners to rotate
Expand Down Expand Up @@ -53,17 +53,32 @@ export class AuthenticationKey {
* This allows for the creation of AuthenticationKeys that are not derived from Public Keys directly
* @param args
*/
private static fromBytesAndScheme(args: { bytes: HexInput; scheme: AuthenticationKeyScheme }) {
const { bytes, scheme } = args;
const inputBytes = Hex.fromHexInput(bytes).toUint8Array();
const authKeyBytes = new Uint8Array(inputBytes.length + 1);
authKeyBytes.set(inputBytes);
authKeyBytes.set([scheme], inputBytes.length);
public static fromPublicKeyAndScheme(args: { publicKey: PublicKey; scheme: AuthenticationKeyScheme }) {
const { publicKey, scheme } = args;
let authKeyBytes: Uint8Array;

// TODO - support multied25519 key and MultiKey
switch (scheme) {
case SigningScheme.SingleKey: {
const singleKeyBytes = publicKey.bcsToBytes();
authKeyBytes = new Uint8Array([...singleKeyBytes, scheme]);
break;
}
case SigningScheme.Ed25519:
case SigningScheme.MultiEd25519: {
const ed25519PublicKeyBytes = publicKey.toUint8Array();
const inputBytes = Hex.fromHexInput(ed25519PublicKeyBytes).toUint8Array();
authKeyBytes = new Uint8Array([...inputBytes, scheme]);
break;
}
default:
throw new Error(`Scheme ${scheme} is not supported`);
}

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

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

/**
Expand All @@ -77,17 +92,18 @@ export class AuthenticationKey {

let scheme: number;
if (publicKey instanceof Ed25519PublicKey) {
// for legacy support
scheme = SigningScheme.Ed25519.valueOf();
} else if (publicKey instanceof MultiEd25519PublicKey) {
// for legacy support
scheme = SigningScheme.MultiEd25519.valueOf();
} else if (publicKey instanceof Secp256k1PublicKey) {
scheme = SigningScheme.Secp256k1Ecdsa.valueOf();
} else if (publicKey instanceof AnyPublicKey) {
scheme = SigningScheme.SingleKey.valueOf();
} else {
throw new Error("No supported authentication scheme for public key");
}

const pubKeyBytes = publicKey.toUint8Array();
return AuthenticationKey.fromBytesAndScheme({ bytes: pubKeyBytes, scheme });
return AuthenticationKey.fromPublicKeyAndScheme({ publicKey, scheme });
}

/**
Expand Down
84 changes: 84 additions & 0 deletions src/core/crypto/anyPublicKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Serializer, Deserializer } from "../../bcs";
import { AnyPublicKeyVariant, HexInput } from "../../types";
import { AnySignature } from "./anySignature";
import { PublicKey } from "./asymmetricCrypto";
import { Ed25519PublicKey, Ed25519Signature } from "./ed25519";
import { Secp256k1PublicKey, Secp256k1Signature } from "./secp256k1";

export class AnyPublicKey extends PublicKey {
public readonly publicKey: PublicKey;

constructor(publicKey: PublicKey) {
super();
this.publicKey = publicKey;
}

/**
* Get the public key in bytes (Uint8Array).
*
* @returns Uint8Array representation of the public key
*/
toUint8Array(): Uint8Array {
return this.publicKey.toUint8Array();
}

/**
* Get the public key as a hex string with the 0x prefix.
*
* @returns string representation of the public key
*/
toString(): string {
return this.publicKey.toString();
}

/**
* Verifies a signed data with a public key
*
* @param args.message message
* @param args.signature The signature
* @returns true if the signature is valid
*/
verifySignature(args: { message: HexInput; signature: AnySignature }): boolean {
const { message, signature } = args;
if (this.isED25519Signature(signature)) {
return this.publicKey.verifySignature({ message, signature: signature.signature });
// eslint-disable-next-line no-else-return
} else if (this.isSecp256k1Signature(signature)) {
return this.publicKey.verifySignature({ message, signature: signature.signature });
} else {
throw new Error("Unknown public key type");
}
}

isED25519Signature(signature: AnySignature): boolean {
return this.publicKey instanceof Ed25519PublicKey && signature.signature instanceof Ed25519Signature;
}

isSecp256k1Signature(signature: AnySignature): boolean {
return this.publicKey instanceof Secp256k1PublicKey && signature.signature instanceof Secp256k1Signature;
}

serialize(serializer: Serializer): void {
if (this.publicKey instanceof Ed25519PublicKey) {
serializer.serializeU32AsUleb128(AnyPublicKeyVariant.Ed25519);
this.publicKey.serialize(serializer);
} else if (this.publicKey instanceof Secp256k1PublicKey) {
serializer.serializeU32AsUleb128(AnyPublicKeyVariant.Secp256k1);
this.publicKey.serialize(serializer);
} else {
throw new Error("Unknown public key type");
}
}

static deserialize(deserializer: Deserializer): AnyPublicKey {
const index = deserializer.deserializeUleb128AsU32();
switch (index) {
case AnyPublicKeyVariant.Ed25519:
return new AnyPublicKey(Ed25519PublicKey.load(deserializer));
case AnyPublicKeyVariant.Secp256k1:
return new AnyPublicKey(Secp256k1PublicKey.load(deserializer));
default:
throw new Error(`Unknown variant index for AnyPublicKey: ${index}`);
}
}
}
Loading
Loading