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

740-3 wallet package #769

Merged
merged 18 commits into from
Oct 19, 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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ wallet_*.json
wallet-plugin*

coverage.out
internal/handler/wallet/unit-test.log
**/unit-test.log
build/*
!build/appicon.png
node_modules
Expand Down
36 changes: 32 additions & 4 deletions pkg/types/encrypted_private_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,15 @@
return append([]byte{EncryptedPrivateKeyLastVersion}, ed25519.Sign(privateKeyInClear.Bytes(), digest[:])...), nil
}

// SignWithPrivateKey signs the given data using the private key. Private key is destroyed.
func (e *EncryptedPrivateKey) SignWithPrivateKey(privateKey *memguard.LockedBuffer, data []byte) []byte {
digest := blake3.Sum256(data)

defer privateKey.Destroy()

return append([]byte{EncryptedPrivateKeyLastVersion}, ed25519.Sign(privateKey.Bytes(), digest[:])...)

Check warning on line 111 in pkg/types/encrypted_private_key.go

View check run for this annotation

Codecov / codecov/patch

pkg/types/encrypted_private_key.go#L106-L111

Added lines #L106 - L111 were not covered by tests
}

// PublicKey returns the public key corresponding to the private key. Password is destroyed.
func (e *EncryptedPrivateKey) PublicKey(password *memguard.LockedBuffer, salt, nonce []byte) (*PublicKey, error) {
privateKeyInClear, err := privateKey(password, salt, nonce, e.Data)
Expand All @@ -123,8 +132,8 @@
}

// PrivateKeyTextInClear returns the private key in clear. Password is destroyed.
func (e *EncryptedPrivateKey) PrivateKeyTextInClear(password *memguard.LockedBuffer, salt, nonce, encryptedKey []byte) (*memguard.LockedBuffer, error) {
privateKeyInClear, err := privateKey(password, salt, nonce, encryptedKey)
func (e *EncryptedPrivateKey) PrivateKeyTextInClear(password *memguard.LockedBuffer, salt, nonce []byte) (*memguard.LockedBuffer, error) {
privateKeyInClear, err := e.PrivateKeyBytesInClear(password, salt, nonce)

Check warning on line 136 in pkg/types/encrypted_private_key.go

View check run for this annotation

Codecov / codecov/patch

pkg/types/encrypted_private_key.go#L135-L136

Added lines #L135 - L136 were not covered by tests
if err != nil {
return nil, fmt.Errorf("failed to get private key: %w", err)
}
Expand All @@ -141,8 +150,17 @@
return privateKeyBuffer, nil
}

func (e *EncryptedPrivateKey) PrivateKeyBytesInClear(password *memguard.LockedBuffer, salt, nonce []byte) (*memguard.LockedBuffer, error) {
privateKeyInClear, err := privateKey(password, salt, nonce, e.Data)
if err != nil {
return nil, fmt.Errorf("PrivateKeyTextInClear: %w", err)
}

Check warning on line 157 in pkg/types/encrypted_private_key.go

View check run for this annotation

Codecov / codecov/patch

pkg/types/encrypted_private_key.go#L156-L157

Added lines #L156 - L157 were not covered by tests

return privateKeyInClear, nil
}

// HasAccess returns true if the password is valid for the account. It destroys the password.
func (e *EncryptedPrivateKey) HasAccess(password *memguard.LockedBuffer, salt, nonce, encryptedKey []byte) bool {
func (e *EncryptedPrivateKey) HasAccess(password *memguard.LockedBuffer, salt, nonce []byte) bool {

Check warning on line 163 in pkg/types/encrypted_private_key.go

View check run for this annotation

Codecov / codecov/patch

pkg/types/encrypted_private_key.go#L163

Added line #L163 was not covered by tests
privateKeyInClear, err := privateKey(password, salt, nonce, e.Data)
if err != nil {
return false
Expand All @@ -163,5 +181,15 @@
return nil, err
}

return crypto.UnsealSecret(aeadCipher, nonce[:], encryptedKey)
secret, err := crypto.UnsealSecret(aeadCipher, nonce[:], encryptedKey)
if err != nil {
return nil, err
}

Check warning on line 187 in pkg/types/encrypted_private_key.go

View check run for this annotation

Codecov / codecov/patch

pkg/types/encrypted_private_key.go#L186-L187

Added lines #L186 - L187 were not covered by tests

data := secret.Bytes()
result := memguard.NewBuffer(64)
result.Copy(data[1:])
secret.Destroy()

return result, nil
}
13 changes: 12 additions & 1 deletion pkg/types/encrypted_private_key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func TestEncryptedPrivateKey(t *testing.T) {
aeadCipher, secretKey, err := crypto.NewSecretCipher(samplePassword.Bytes(), sampleSalt)
assert.NoError(t, err)
// secretBuffer is the secret key in clear without the version.
secretBuffer := memguard.NewBufferFromBytes([]byte{216, 39, 16, 253, 102, 99, 172, 42, 205, 205, 17, 23, 123, 144, 171, 13, 91, 219, 194, 251, 186, 234, 11, 222, 23, 221, 6, 75, 22, 61, 235, 254, 45, 150, 188, 218, 203, 190, 65, 56, 44, 162, 62, 82, 227, 210, 25, 108, 186, 101, 231, 161, 172, 210, 9, 223, 201, 92, 107, 50, 182, 161, 138, 147})
secretBuffer := memguard.NewBufferFromBytes([]byte{0, 216, 39, 16, 253, 102, 99, 172, 42, 205, 205, 17, 23, 123, 144, 171, 13, 91, 219, 194, 251, 186, 234, 11, 222, 23, 221, 6, 75, 22, 61, 235, 254, 45, 150, 188, 218, 203, 190, 65, 56, 44, 162, 62, 82, 227, 210, 25, 108, 186, 101, 231, 161, 172, 210, 9, 223, 201, 92, 107, 50, 182, 161, 138, 147})
encryptedSecret := crypto.SealSecret(aeadCipher, sampleNonce, secretBuffer)

secretKey.Destroy()
Expand Down Expand Up @@ -135,4 +135,15 @@ func TestEncryptedPrivateKey(t *testing.T) {
expectedSignature := []byte{45, 150, 188, 218, 203, 190, 65, 56, 44, 162, 62, 82, 227, 210, 25, 108, 186, 101, 231, 161, 172, 210, 9, 223, 201, 92, 107, 50, 182, 161, 138, 147}
assert.Equal(t, expectedSignature, publicKey.Data)
})

t.Run("PrivateKeyBytesInClear", func(t *testing.T) {
samplePassword := memguard.NewBufferFromBytes([]byte("bonjour"))

// Get the public key from the private key
privateKeyBytesInClear, err := sampleEncryptedPrivateKey.PrivateKeyBytesInClear(samplePassword, sampleSalt, sampleNonce)
assert.NoError(t, err)

expectedPrivateKeyBytesInClear := []byte{0xd8, 0x27, 0x10, 0xfd, 0x66, 0x63, 0xac, 0x2a, 0xcd, 0xcd, 0x11, 0x17, 0x7b, 0x90, 0xab, 0xd, 0x5b, 0xdb, 0xc2, 0xfb, 0xba, 0xea, 0xb, 0xde, 0x17, 0xdd, 0x6, 0x4b, 0x16, 0x3d, 0xeb, 0xfe, 0x2d, 0x96, 0xbc, 0xda, 0xcb, 0xbe, 0x41, 0x38, 0x2c, 0xa2, 0x3e, 0x52, 0xe3, 0xd2, 0x19, 0x6c, 0xba, 0x65, 0xe7, 0xa1, 0xac, 0xd2, 0x9, 0xdf, 0xc9, 0x5c, 0x6b, 0x32, 0xb6, 0xa1, 0x8a, 0x93}
assert.Equal(t, expectedPrivateKeyBytesInClear, privateKeyBytesInClear.Bytes())
})
}
25 changes: 19 additions & 6 deletions pkg/wallet/account/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
return nil, fmt.Errorf("generating random nonce: %w", err)
}

privateKeyBytes = append([]byte{types.EncryptedPrivateKeyLastVersion}, []byte(privateKeyBytes)...)
privateKey := memguard.NewBufferFromBytes(privateKeyBytes)

encryptedSecret, err := seal(privateKey, password, salt[:], nonce[:])
Expand Down Expand Up @@ -147,16 +148,18 @@
}

seed, privateKeyVersion, err := base58.CheckDecode(string(privateKeyText.Bytes()[1:])) // omit the first byte because it's 'S' for secret key

seedBuffer := memguard.NewBufferFromBytes(seed)
if err != nil {
seedBuffer.Destroy()
return nil, fmt.Errorf("%w: decoding base58 private key: %w", ErrInvalidPrivateKey, err)
}

privateKey := memguard.NewBufferFromBytes(ed25519.NewKeyFromSeed(seedBuffer.Bytes()))
seedBuffer := memguard.NewBufferFromBytes(seed)

privateKeyBytes := ed25519.NewKeyFromSeed(seedBuffer.Bytes())
seedBuffer.Destroy()

privateKeyBytes = append([]byte{privateKeyVersion}, privateKeyBytes...)
privateKey := memguard.NewBufferFromBytes(privateKeyBytes)

encryptedSecret, err := seal(privateKey, password, salt[:], nonce[:])
if err != nil {
return nil, fmt.Errorf("sealing secret: %w", err)
Expand Down Expand Up @@ -193,6 +196,7 @@
if err != nil {
return nil, fmt.Errorf("creating secret cipher: %w", err)
}

encryptedSecret := crypto.SealSecret(aeadCipher, nonce[:], privateKey)

secretKey.Destroy()
Expand All @@ -202,21 +206,30 @@

// PrivateKeyTextInClear returns the private key in clear and destroys the password.
func (a *Account) PrivateKeyTextInClear(password *memguard.LockedBuffer) (*memguard.LockedBuffer, error) {
return a.CipheredData.PrivateKeyTextInClear(password, a.Salt[:], a.Nonce[:], a.CipheredData.Data)
return a.CipheredData.PrivateKeyTextInClear(password, a.Salt[:], a.Nonce[:])
}

func (a *Account) PrivateKeyBytesInClear(password *memguard.LockedBuffer) (*memguard.LockedBuffer, error) {
return a.CipheredData.PrivateKeyBytesInClear(password, a.Salt[:], a.Nonce[:])

Check warning on line 213 in pkg/wallet/account/account.go

View check run for this annotation

Codecov / codecov/patch

pkg/wallet/account/account.go#L212-L213

Added lines #L212 - L213 were not covered by tests
}

// Sign signs the data with the private key and destroys the password.
func (a *Account) Sign(password *memguard.LockedBuffer, data []byte) ([]byte, error) {
return a.CipheredData.Sign(password, a.Salt[:], a.Nonce[:], data)
}

// SignWithPrivateKey signs the data with the private key and destroys the private key.
func (a *Account) SignWithPrivateKey(privateKey *memguard.LockedBuffer, data []byte) []byte {
return a.CipheredData.SignWithPrivateKey(privateKey, data)

Check warning on line 223 in pkg/wallet/account/account.go

View check run for this annotation

Codecov / codecov/patch

pkg/wallet/account/account.go#L222-L223

Added lines #L222 - L223 were not covered by tests
}

func (a *Account) Marshal() ([]byte, error) {
return yaml.Marshal(a)
}

// HasAccess returns true if the password is valid for the account. It destroys the password.
func (a *Account) HasAccess(password *memguard.LockedBuffer) bool {
return a.CipheredData.HasAccess(password, a.Salt[:], a.Nonce[:], a.CipheredData.Data)
return a.CipheredData.HasAccess(password, a.Salt[:], a.Nonce[:])

Check warning on line 232 in pkg/wallet/account/account.go

View check run for this annotation

Codecov / codecov/patch

pkg/wallet/account/account.go#L232

Added line #L232 was not covered by tests
}

func (a *Account) Unmarshal(data []byte) error {
Expand Down
6 changes: 3 additions & 3 deletions pkg/wallet/account/account_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func newAccount(t *testing.T) *Account {
samplePassword := memguard.NewBufferFromBytes([]byte(password))
privateKey := memguard.NewBufferFromBytes([]byte(privateKeyText))

// Call the New function with the test values
// Call the NewFromPrivateKey function with the test values
account, err := NewFromPrivateKey(samplePassword, nickname, privateKey)
assert.NoError(t, err)

Expand All @@ -52,10 +52,10 @@ func TestNewAccountFromPrivateKey(t *testing.T) {
assert.Equal(t, nickname, account.Nickname)

expectedPublicKey := []byte{45, 150, 188, 218, 203, 190, 65, 56, 44, 162, 62, 82, 227, 210, 25, 108, 186, 101, 231, 161, 172, 210, 9, 223, 201, 92, 107, 50, 182, 161, 138, 147}
assert.Equal(t, expectedPublicKey, account.PublicKey.Object.Data)
assert.Equal(t, expectedPublicKey, account.PublicKey.Data)

expectedAddress := []byte{0x77, 0x13, 0x86, 0x8f, 0xe5, 0x5a, 0xd1, 0xdb, 0x9c, 0x8, 0x30, 0x7c, 0x61, 0x5e, 0xdf, 0xc0, 0xc8, 0x3b, 0x5b, 0xd9, 0x88, 0xec, 0x2e, 0x3c, 0xe9, 0xe4, 0x1c, 0xf1, 0xf9, 0x4d, 0xc5, 0xd1}
assert.Equal(t, expectedAddress, account.Address.Object.Data)
assert.Equal(t, expectedAddress, account.Address.Data)
})
}

Expand Down
113 changes: 113 additions & 0 deletions pkg/walletmanager/filesystem.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package walletmanager

import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/massalabs/station-massa-wallet/pkg/types"
"github.com/massalabs/station-massa-wallet/pkg/wallet/account"
)

const (
directoryName = "massa-station-wallet"
FileModeUserReadWriteOnly = 0o600
)

var ErrUnmarshalAccount = errors.New("unmarshaling account")

// Path returns the path where the account yaml file are stored.
func Path() (string, error) {
configDir, err := os.UserConfigDir()
if err != nil {
return "", fmt.Errorf("getting user config directory: %w", err)
}

Check warning on line 26 in pkg/walletmanager/filesystem.go

View check run for this annotation

Codecov / codecov/patch

pkg/walletmanager/filesystem.go#L22-L26

Added lines #L22 - L26 were not covered by tests

path := filepath.Join(configDir, directoryName)

// create the directory if it doesn't exist
if _, err := os.Stat(path); os.IsNotExist(err) {
err = os.MkdirAll(path, os.ModePerm)
if err != nil {
return "", fmt.Errorf("creating account directory '%s': %w", path, err)
}

Check warning on line 35 in pkg/walletmanager/filesystem.go

View check run for this annotation

Codecov / codecov/patch

pkg/walletmanager/filesystem.go#L28-L35

Added lines #L28 - L35 were not covered by tests
}

return path, nil

Check warning on line 38 in pkg/walletmanager/filesystem.go

View check run for this annotation

Codecov / codecov/patch

pkg/walletmanager/filesystem.go#L38

Added line #L38 was not covered by tests
}

func (w *Wallet) AccountPath(nickname string) (string, error) {
return filepath.Join(w.WalletPath, Filename(nickname)), nil
}

// filename returns the wallet filename based on the given nickname.
func Filename(nickname string) string {
return fmt.Sprintf("wallet_%s.yaml", nickname)
}

func (w *Wallet) nicknameFromFilePath(filePath string) string {
_, nicknameFromFileName := filepath.Split(filePath)
nicknameFromFileName = strings.TrimPrefix(nicknameFromFileName, "wallet_")

return strings.TrimSuffix(nicknameFromFileName, ".yaml")
}

func (w *Wallet) Persist(acc account.Account) error {
filePath, err := w.AccountPath(acc.Nickname)
if err != nil {
return err
}

Check warning on line 61 in pkg/walletmanager/filesystem.go

View check run for this annotation

Codecov / codecov/patch

pkg/walletmanager/filesystem.go#L60-L61

Added lines #L60 - L61 were not covered by tests

data, err := acc.Marshal()
if err != nil {
return fmt.Errorf("marshaling account: %w", err)
}

Check warning on line 66 in pkg/walletmanager/filesystem.go

View check run for this annotation

Codecov / codecov/patch

pkg/walletmanager/filesystem.go#L65-L66

Added lines #L65 - L66 were not covered by tests

err = os.WriteFile(filePath, data, FileModeUserReadWriteOnly)
if err != nil {
return fmt.Errorf("writing wallet to '%s: %w", filePath, err)
}

Check warning on line 71 in pkg/walletmanager/filesystem.go

View check run for this annotation

Codecov / codecov/patch

pkg/walletmanager/filesystem.go#L70-L71

Added lines #L70 - L71 were not covered by tests

return nil
}

func (w *Wallet) Load(filePath string) (*account.Account, error) {
data, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("reading wallet from '%s': %w", filePath, err)
}

acc := account.Account{}

err = acc.Unmarshal(data)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrUnmarshalAccount, err)
}

// Nickname in the file is optional, if it's not set, we use the filename
if acc.Nickname == "" {
acc.Nickname = w.nicknameFromFilePath(filePath)
}

if !account.NicknameIsValid(acc.Nickname) {
return nil, fmt.Errorf("%w: the provided nickname is invalid: %s", account.ErrInvalidNickname, acc.Nickname) // TODO: add unit test
}

Check warning on line 96 in pkg/walletmanager/filesystem.go

View check run for this annotation

Codecov / codecov/patch

pkg/walletmanager/filesystem.go#L95-L96

Added lines #L95 - L96 were not covered by tests

// Address in the file is optional, if it's not set, we use the public key
if acc.Address == nil || acc.Address.Object == nil || acc.Address.Object.Data == nil {
acc.Address = types.NewAddressFromPublicKey(acc.PublicKey)
}

return &acc, nil
}

func (w *Wallet) deleteFile(filePath string) error {
err := os.Remove(filePath)
if err != nil {
return fmt.Errorf("deleting file '%s': %w", filePath, err)
}

Check warning on line 110 in pkg/walletmanager/filesystem.go

View check run for this annotation

Codecov / codecov/patch

pkg/walletmanager/filesystem.go#L109-L110

Added lines #L109 - L110 were not covered by tests

return nil
}
72 changes: 72 additions & 0 deletions pkg/walletmanager/retrocompatibility.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package walletmanager

import (
"fmt"
"os"
"path"
"path/filepath"
"runtime"
"strings"

"github.com/massalabs/station/pkg/logger"
)

// MigrateWallet moves the wallet from the old location (executable file) to the new one (user config).
func (w *Wallet) MigrateWallet() error {
oldPath, err := GetWorkDir()
if err != nil {
return fmt.Errorf("reading config directory '%s': %w", oldPath, err)
}

Check warning on line 19 in pkg/walletmanager/retrocompatibility.go

View check run for this annotation

Codecov / codecov/patch

pkg/walletmanager/retrocompatibility.go#L18-L19

Added lines #L18 - L19 were not covered by tests

files, err := os.ReadDir(oldPath)
if err != nil {
return fmt.Errorf("reading working directory '%s': %w", oldPath, err)
}

Check warning on line 24 in pkg/walletmanager/retrocompatibility.go

View check run for this annotation

Codecov / codecov/patch

pkg/walletmanager/retrocompatibility.go#L23-L24

Added lines #L23 - L24 were not covered by tests

for _, f := range files {
fileName := f.Name()
oldFilePath := path.Join(oldPath, fileName)

if strings.HasPrefix(fileName, "wallet_") && strings.HasSuffix(fileName, ".yaml") {
newFilePath := path.Join(w.WalletPath, fileName)

// Skip if new file path exists
if _, err := os.Stat(newFilePath); err == nil {
continue

Check warning on line 35 in pkg/walletmanager/retrocompatibility.go

View check run for this annotation

Codecov / codecov/patch

pkg/walletmanager/retrocompatibility.go#L31-L35

Added lines #L31 - L35 were not covered by tests
}

logger.Infof("Migrating wallet from", oldFilePath, "to", newFilePath)

err = os.Rename(oldFilePath, newFilePath)
if err != nil {
logger.Errorf("moving account file from '%s' to '%s': %w", oldFilePath, newFilePath, err)
}

Check warning on line 43 in pkg/walletmanager/retrocompatibility.go

View check run for this annotation

Codecov / codecov/patch

pkg/walletmanager/retrocompatibility.go#L38-L43

Added lines #L38 - L43 were not covered by tests
}
}

return nil
}

func GetWorkDir() (string, error) {
ex, err := os.Executable()
if err != nil {
return "", fmt.Errorf("getting executable path: %w", err)
}

Check warning on line 54 in pkg/walletmanager/retrocompatibility.go

View check run for this annotation

Codecov / codecov/patch

pkg/walletmanager/retrocompatibility.go#L53-L54

Added lines #L53 - L54 were not covered by tests

if runtime.GOOS == "darwin" {
// On macOS, the executable is in a subdirectory of the working directory.
// We need to go up 4 levels to get the working directory.
// wallet-plugin.app/Contents/MacOS/wallet-plugin
return filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(ex)))), nil
}

Check warning on line 61 in pkg/walletmanager/retrocompatibility.go

View check run for this annotation

Codecov / codecov/patch

pkg/walletmanager/retrocompatibility.go#L57-L61

Added lines #L57 - L61 were not covered by tests

dir := filepath.Dir(ex)

// Helpful when developing:
// when running `go run`, the executable is in a temporary directory.
if strings.Contains(dir, "go-build") {
return ".", nil
}

return filepath.Dir(ex), nil

Check warning on line 71 in pkg/walletmanager/retrocompatibility.go

View check run for this annotation

Codecov / codecov/patch

pkg/walletmanager/retrocompatibility.go#L71

Added line #L71 was not covered by tests
}
Loading
Loading