From 181679e78906807464165f6dc8faa6203ec2f2ee Mon Sep 17 00:00:00 2001 From: Philip Vu Date: Wed, 20 Nov 2024 16:59:44 -0800 Subject: [PATCH] Add support for AIP-80 parsing and formatting Make ToAIP80 return error, fix bytes parsing --- CHANGELOG.md | 2 + crypto/ed25519.go | 14 ++++- crypto/ed25519_test.go | 13 +++-- crypto/privateKey.go | 93 +++++++++++++++++++++++++++++++++ crypto/secp256k1.go | 12 ++++- crypto/secp256k1_test.go | 13 +++-- examples/fungible_asset/main.go | 2 +- 7 files changed, 137 insertions(+), 12 deletions(-) create mode 100644 crypto/privateKey.go diff --git a/CHANGELOG.md b/CHANGELOG.md index c95a9f2..f5b43fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ adheres to the format set out by [Keep a Changelog](https://keepachangelog.com/e # Unreleased +- Add AIP-80 support for Ed25519 and Secp256k1 private keys + # v1.2.0 (11/15/2024) - [`Fix`][`Breaking`] Fix MultiKey implementation to be more consistent with the rest of the SDKs diff --git a/crypto/ed25519.go b/crypto/ed25519.go index 5f05ad2..787689a 100644 --- a/crypto/ed25519.go +++ b/crypto/ed25519.go @@ -4,10 +4,11 @@ import ( "crypto/ed25519" "errors" "fmt" + "io" + "github.com/aptos-labs/aptos-go-sdk/bcs" "github.com/aptos-labs/aptos-go-sdk/internal/util" "github.com/hdevalence/ed25519consensus" - "io" ) //region Ed25519PrivateKey @@ -151,6 +152,10 @@ func (key *Ed25519PrivateKey) Bytes() []byte { // Implements: // - [CryptoMaterial] func (key *Ed25519PrivateKey) FromBytes(bytes []byte) (err error) { + bytes, err = ParsePrivateKey(bytes, PrivateKeyVariantEd25519, false) + if err != nil { + return err + } if len(bytes) != ed25519.SeedSize { return fmt.Errorf("invalid ed25519 private key size %d", len(bytes)) } @@ -166,6 +171,11 @@ func (key *Ed25519PrivateKey) ToHex() string { return util.BytesToHex(key.Bytes()) } +// ToAIP80 formats the private key to AIP-80 compliant string +func (key *Ed25519PrivateKey) ToAIP80() (formattedString string, err error) { + return FormatPrivateKey(key.ToHex(), PrivateKeyVariantEd25519) +} + // FromHex sets the [Ed25519PrivateKey] to the bytes represented by the hex string, with or without a leading 0x // // Errors if the hex string is not valid, or if the bytes length is not [ed25519.SeedSize]. @@ -173,7 +183,7 @@ func (key *Ed25519PrivateKey) ToHex() string { // Implements: // - [CryptoMaterial] func (key *Ed25519PrivateKey) FromHex(hexStr string) (err error) { - bytes, err := util.ParseHex(hexStr) + bytes, err := ParsePrivateKey(hexStr, PrivateKeyVariantEd25519) if err != nil { return err } diff --git a/crypto/ed25519_test.go b/crypto/ed25519_test.go index d746091..23324fc 100644 --- a/crypto/ed25519_test.go +++ b/crypto/ed25519_test.go @@ -2,13 +2,15 @@ package crypto import ( "crypto/ed25519" + "testing" + "github.com/aptos-labs/aptos-go-sdk/bcs" "github.com/aptos-labs/aptos-go-sdk/internal/util" "github.com/stretchr/testify/assert" - "testing" ) -const testEd25519PrivateKey = "0xc5338cd251c22daa8c9c9cc94f498cc8a5c7e1d2e75287a5dda91096fe64efa5" +const testEd25519PrivateKey = "ed25519-priv-0xc5338cd251c22daa8c9c9cc94f498cc8a5c7e1d2e75287a5dda91096fe64efa5" +const testEd25519PrivateKeyHex = "0xc5338cd251c22daa8c9c9cc94f498cc8a5c7e1d2e75287a5dda91096fe64efa5" const testEd25519PublicKey = "0xde19e5d1880cac87d57484ce9ed2e84cf0f9599f12e7cc3a52e4e7657a763f2c" const testEd25519Address = "0x978c213990c4833df71548df7ce49d54c759d6b6d932de22b24d56060b7af2aa" const testEd25519Message = "0x68656c6c6f20776f726c64" @@ -18,7 +20,7 @@ func TestEd25519Keys(t *testing.T) { testEd25519PrivateKeyBytes := []byte{0xc5, 0x33, 0x8c, 0xd2, 0x51, 0xc2, 0x2d, 0xaa, 0x8c, 0x9c, 0x9c, 0xc9, 0x4f, 0x49, 0x8c, 0xc8, 0xa5, 0xc7, 0xe1, 0xd2, 0xe7, 0x52, 0x87, 0xa5, 0xdd, 0xa9, 0x10, 0x96, 0xfe, 0x64, 0xef, 0xa5} // First ensure bytes and hex are the same - readBytes, err := util.ParseHex(testEd25519PrivateKey) + readBytes, err := util.ParseHex(testEd25519PrivateKeyHex) assert.NoError(t, err) assert.Equal(t, testEd25519PrivateKeyBytes, readBytes) @@ -33,7 +35,10 @@ func TestEd25519Keys(t *testing.T) { // The outputs should match as well assert.Equal(t, privateKey.Bytes(), testEd25519PrivateKeyBytes) - assert.Equal(t, privateKey.ToHex(), testEd25519PrivateKey) + assert.Equal(t, privateKey.ToHex(), testEd25519PrivateKeyHex) + formattedString, err := privateKey.ToAIP80() + assert.NoError(t, err) + assert.Equal(t, formattedString, testEd25519PrivateKey) // Auth key should match assert.Equal(t, testEd25519Address, privateKey.AuthKey().ToHex()) diff --git a/crypto/privateKey.go b/crypto/privateKey.go new file mode 100644 index 0000000..88cdfe5 --- /dev/null +++ b/crypto/privateKey.go @@ -0,0 +1,93 @@ +package crypto + +import ( + "fmt" + "strings" + + "github.com/aptos-labs/aptos-go-sdk/internal/util" +) + +// PrivateKeyVariant represents the type of private key +type PrivateKeyVariant string + +const ( + PrivateKeyVariantEd25519 PrivateKeyVariant = "ed25519" + PrivateKeyVariantSecp256k1 PrivateKeyVariant = "secp256k1" +) + +// AIP80Prefixes contains the AIP-80 compliant prefixes for each private key type +var AIP80Prefixes = map[PrivateKeyVariant]string{ + PrivateKeyVariantEd25519: "ed25519-priv-", + PrivateKeyVariantSecp256k1: "secp256k1-priv-", +} + +// FormatPrivateKey formats a hex input to an AIP-80 compliant string +func FormatPrivateKey(privateKey any, keyType PrivateKeyVariant) (formattedString string, err error) { + aip80Prefix := AIP80Prefixes[keyType] + + var hexStr string + switch v := privateKey.(type) { + case string: + // Remove the prefix if it exists + if strings.HasPrefix(v, aip80Prefix) { + parts := strings.Split(v, "-") + v = parts[2] + } + + // If it's already a string, just ensure it's properly formatted + var strBytes, err = util.ParseHex(v) + if err != nil { + return "", err + } + + // Reformat to have 0x prefix + hexStr = util.BytesToHex(strBytes) + case []byte: + hexStr = util.BytesToHex(v) + default: + return "", fmt.Errorf("unsupported private key type: must be string or []byte") + } + + return fmt.Sprintf("%s%s", aip80Prefix, hexStr), nil +} + +// ParseHexInput parses a hex input that may be bytes, hex string, or an AIP-80 compliant string to bytes. +// +// You may optionally pass in a boolean to strictly enforce AIP-80 compliance. +func ParsePrivateKey(value any, keyType PrivateKeyVariant, strict ...bool) (bytes []byte, err error) { + aip80Prefix := AIP80Prefixes[keyType] + + // Get the first boolean if it exists, otherwise nil + var strictness *bool = nil + if len(strict) > 1 { + return nil, fmt.Errorf("strictness must be a single boolean") + } else if len(strict) == 1 { + strictness = &strict[0] + } + + switch v := value.(type) { + case string: + if (strictness == nil || !*strictness) && !strings.HasPrefix(v, aip80Prefix) { + bytes, err := util.ParseHex(v) + if err != nil { + return nil, err + } + + // If strictness is not explicitly false, warn about non-AIP-80 compliance + if strictness == nil { + fmt.Printf("[Aptos SDK] It is recommended that private keys are AIP-80 compliant (https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-80.md). You can fix the private key by formatting it with crypto.FormatPrivateKey") + } + + return bytes, nil + } else if strings.HasPrefix(v, aip80Prefix) { + // Parse for AIP-80 compliant String input + parts := strings.Split(v, "-") + return util.ParseHex(parts[2]) + } + return nil, fmt.Errorf("invalid hex string input while parsing private key. Must be AIP-80 compliant string") + case []byte: + return v, nil + default: + return nil, fmt.Errorf("unsupported private key type: must be string or []byte") + } +} diff --git a/crypto/secp256k1.go b/crypto/secp256k1.go index 1dc4868..ab921fd 100644 --- a/crypto/secp256k1.go +++ b/crypto/secp256k1.go @@ -3,6 +3,7 @@ package crypto import ( "crypto/ecdsa" "fmt" + "github.com/aptos-labs/aptos-go-sdk/bcs" "github.com/aptos-labs/aptos-go-sdk/internal/util" ethCrypto "github.com/ethereum/go-ethereum/crypto" @@ -98,6 +99,10 @@ func (key *Secp256k1PrivateKey) Bytes() []byte { // Implements: // - [CryptoMaterial] func (key *Secp256k1PrivateKey) FromBytes(bytes []byte) (err error) { + bytes, err = ParsePrivateKey(bytes, PrivateKeyVariantSecp256k1, false) + if err != nil { + return err + } if len(bytes) != Secp256k1PrivateKeyLength { return fmt.Errorf("invalid secp256k1 private key size %d", len(bytes)) } @@ -117,6 +122,11 @@ func (key *Secp256k1PrivateKey) ToHex() string { return util.BytesToHex(key.Bytes()) } +// ToAIP80 formats the private key to AIP-80 compliant string +func (key *Secp256k1PrivateKey) ToAIP80() (formattedString string, err error) { + return FormatPrivateKey(key.ToHex(), PrivateKeyVariantSecp256k1) +} + //endregion // FromHex populates the [Secp256k1PrivateKey] from a hex string @@ -126,7 +136,7 @@ func (key *Secp256k1PrivateKey) ToHex() string { // Implements: // - [CryptoMaterial] func (key *Secp256k1PrivateKey) FromHex(hexStr string) (err error) { - bytes, err := util.ParseHex(hexStr) + bytes, err := ParsePrivateKey(hexStr, PrivateKeyVariantSecp256k1) if err != nil { return err } diff --git a/crypto/secp256k1_test.go b/crypto/secp256k1_test.go index 0d8322f..6f70d6e 100644 --- a/crypto/secp256k1_test.go +++ b/crypto/secp256k1_test.go @@ -1,14 +1,16 @@ package crypto import ( + "testing" + "github.com/aptos-labs/aptos-go-sdk/bcs" "github.com/aptos-labs/aptos-go-sdk/internal/util" "github.com/stretchr/testify/assert" - "testing" ) const ( - testSecp256k1PrivateKey = "0xd107155adf816a0a94c6db3c9489c13ad8a1eda7ada2e558ba3bfa47c020347e" + testSecp256k1PrivateKey = "secp256k1-priv-0xd107155adf816a0a94c6db3c9489c13ad8a1eda7ada2e558ba3bfa47c020347e" + testSecp256k1PrivateKeyHex = "0xd107155adf816a0a94c6db3c9489c13ad8a1eda7ada2e558ba3bfa47c020347e" testSecp256k1PublicKey = "0x04acdd16651b839c24665b7e2033b55225f384554949fef46c397b5275f37f6ee95554d70fb5d9f93c5831ebf695c7206e7477ce708f03ae9bb2862dc6c9e033ea" testSecp256k1Address = "0x5792c985bc96f436270bd2a3c692210b09c7febb8889345ceefdbae4bacfe498" testSecp256k1MessageEncoded = "0x68656c6c6f20776f726c64" @@ -16,7 +18,7 @@ const ( ) func TestSecp256k1Keys(t *testing.T) { - testSecp256k1PrivateKeyBytes, err := util.ParseHex(testSecp256k1PrivateKey) + testSecp256k1PrivateKeyBytes, err := util.ParseHex(testSecp256k1PrivateKeyHex) assert.NoError(t, err) // Either bytes or hex should work @@ -30,7 +32,10 @@ func TestSecp256k1Keys(t *testing.T) { // The outputs should match as well assert.Equal(t, privateKey.Bytes(), testSecp256k1PrivateKeyBytes) - assert.Equal(t, privateKey.ToHex(), testSecp256k1PrivateKey) + assert.Equal(t, privateKey.ToHex(), testSecp256k1PrivateKeyHex) + formattedString, err := privateKey.ToAIP80() + assert.NoError(t, err) + assert.Equal(t, formattedString, testSecp256k1PrivateKey) // Auth key should match singleSender := SingleSigner{privateKey} diff --git a/examples/fungible_asset/main.go b/examples/fungible_asset/main.go index 182efc9..09c8c3c 100644 --- a/examples/fungible_asset/main.go +++ b/examples/fungible_asset/main.go @@ -9,7 +9,7 @@ import ( "github.com/aptos-labs/aptos-go-sdk/crypto" ) -const testEd25519PrivateKey = "0xc5338cd251c22daa8c9c9cc94f498cc8a5c7e1d2e75287a5dda91096fe64efa5" +const testEd25519PrivateKey = "ed25519-priv-0xc5338cd251c22daa8c9c9cc94f498cc8a5c7e1d2e75287a5dda91096fe64efa5" const rupeePublisherAddress = "0x978c213990c4833df71548df7ce49d54c759d6b6d932de22b24d56060b7af2aa" // These come from fungible_asset.json