Skip to content

Commit

Permalink
Add support for AIP-80 parsing and formatting (#107)
Browse files Browse the repository at this point in the history
Make ToAIP80 return error, fix bytes parsing

Co-authored-by: Greg Nazario <[email protected]>
  • Loading branch information
GhostWalker562 and gregnazario authored Nov 22, 2024
1 parent db6654a commit 5606ce9
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 12 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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
- Add support for optional ledger version in FA functions and APT balance

# v1.2.0 (11/15/2024)
Expand Down
14 changes: 12 additions & 2 deletions crypto/ed25519.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
}
Expand All @@ -166,14 +171,19 @@ 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].
//
// 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
}
Expand Down
13 changes: 9 additions & 4 deletions crypto/ed25519_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)

Expand All @@ -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())
Expand Down
93 changes: 93 additions & 0 deletions crypto/privateKey.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
12 changes: 11 additions & 1 deletion crypto/secp256k1.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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))
}
Expand All @@ -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
Expand All @@ -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
}
Expand Down
13 changes: 9 additions & 4 deletions crypto/secp256k1_test.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
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"
testSecp256k1Signature = "0xd0d634e843b61339473b028105930ace022980708b2855954b977da09df84a770c0b68c29c8ca1b5409a5085b0ec263be80e433c83fcf6debb82f3447e71edca"
)

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
Expand All @@ -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}
Expand Down
2 changes: 1 addition & 1 deletion examples/fungible_asset/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 5606ce9

Please sign in to comment.