Skip to content

Commit

Permalink
Merge branch 'master' into BUX-705/Gitbook
Browse files Browse the repository at this point in the history
  • Loading branch information
Nazarii-4chain authored Apr 10, 2024
2 parents 0263757 + cacb5a4 commit c5f3d61
Show file tree
Hide file tree
Showing 9 changed files with 514 additions and 4 deletions.
49 changes: 49 additions & 0 deletions contacts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package walletclient

import (
"context"
"errors"
"fmt"

"github.com/bitcoin-sv/spv-wallet-go-client/transports"
"github.com/bitcoin-sv/spv-wallet/models"
)

// UpsertContact add or update contact. When adding a new contact, the system utilizes Paymail's PIKE capability to dispatch an invitation request, asking the counterparty to include the current user in their contacts.
func (b *WalletClient) UpsertContact(ctx context.Context, paymail, fullName string, metadata *models.Metadata) (*models.Contact, transports.ResponseError) {
return b.transport.UpsertContact(ctx, paymail, fullName, metadata, "")
}

// UpsertContactForPaymail add or update contact. When adding a new contact, the system utilizes Paymail's PIKE capability to dispatch an invitation request, asking the counterparty to include the current user specified paymail in their contacts.
func (b *WalletClient) UpsertContactForPaymail(ctx context.Context, paymail, fullName string, metadata *models.Metadata, requesterPaymail string) (*models.Contact, transports.ResponseError) {
return b.transport.UpsertContact(ctx, paymail, fullName, metadata, requesterPaymail)
}

// AcceptContact will accept the contact associated with the paymail
func (b *WalletClient) AcceptContact(ctx context.Context, paymail string) transports.ResponseError {
return b.transport.AcceptContact(ctx, paymail)
}

// RejectContact will reject the contact associated with the paymail
func (b *WalletClient) RejectContact(ctx context.Context, paymail string) transports.ResponseError {
return b.transport.RejectContact(ctx, paymail)
}

// ConfirmContact will try to confirm the contact
func (b *WalletClient) ConfirmContact(ctx context.Context, contact *models.Contact, passcode string, period, digits uint) transports.ResponseError {
isTotpValid, err := b.ValidateTotpForContact(contact, passcode, period, digits)
if err != nil {
return transports.WrapError(fmt.Errorf("totp validation failed: %w", err))
}

if !isTotpValid {
return transports.WrapError(errors.New("totp is invalid"))
}

return b.transport.ConfirmContact(ctx, contact.Paymail)
}

// GetContacts will get contacts by conditions
func (b *WalletClient) GetContacts(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata, queryParams *transports.QueryParams) ([]*models.Contact, transports.ResponseError) {
return b.transport.GetContacts(ctx, conditions, metadata, queryParams)
}
145 changes: 145 additions & 0 deletions contacts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package walletclient

import (
"context"
"testing"

"github.com/bitcoin-sv/spv-wallet-go-client/fixtures"
"github.com/bitcoin-sv/spv-wallet/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestContactActionsRouting will test routing
func TestContactActionsRouting(t *testing.T) {
tcs := []struct {
name string
route string
responsePayload string
f func(c *WalletClient) error
}{
{
name: "RejectContact",
route: "/contact/rejected/",
responsePayload: "{}",
f: func(c *WalletClient) error { return c.RejectContact(context.Background(), fixtures.PaymailAddress) },
},
{
name: "AcceptContact",
route: "/contact/accepted/",
responsePayload: "{}",
f: func(c *WalletClient) error { return c.AcceptContact(context.Background(), fixtures.PaymailAddress) },
},
{
name: "GetContacts",
route: "/contact/search/",
responsePayload: "[]",
f: func(c *WalletClient) error {
_, err := c.GetContacts(context.Background(), nil, nil, nil)
return err
},
},
{
name: "UpsertContact",
route: "/contact/",
responsePayload: "{}",
f: func(c *WalletClient) error {
_, err := c.UpsertContact(context.Background(), "", "", nil)
return err
},
},
{
name: "UpsertContactForPaymail",
route: "/contact/",
responsePayload: "{}",
f: func(c *WalletClient) error {
_, err := c.UpsertContactForPaymail(context.Background(), "", "", nil, "")
return err
},
},
{
name: "ConfirmContact",
route: "/contact/confirmed/",
responsePayload: "{}",
f: func(c *WalletClient) error {
contact := models.Contact{PubKey: fixtures.PubKey}

passcode, _ := c.GenerateTotpForContact(&contact, 30, 2)
return c.ConfirmContact(context.Background(), &contact, passcode, 30, 2)
},
},
}

for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
// given
tmq := testTransportHandler{
Type: fixtures.RequestType,
Path: tc.route,
Result: tc.responsePayload,
ClientURL: fixtures.ServerURL,
Client: WithHTTPClient,
}

client := getTestWalletClientWithOpts(tmq, WithXPriv(fixtures.XPrivString))

// when
err := tc.f(client)

// then
assert.NoError(t, err)
})
}

}

func TestConfirmContact(t *testing.T) {
t.Run("TOTP is valid - call Confirm Action", func(t *testing.T) {
// given
tmq := testTransportHandler{
Type: fixtures.RequestType,
Path: "/contact/confirmed/",
Result: "{}",
ClientURL: fixtures.ServerURL,
Client: WithHTTPClient,
}

client := getTestWalletClientWithOpts(tmq, WithXPriv(fixtures.XPrivString))

contact := &models.Contact{PubKey: fixtures.PubKey}
totp, err := client.GenerateTotpForContact(contact, 30, 2)
require.NoError(t, err)

// when
result := client.ConfirmContact(context.Background(), contact, totp, 30, 2)

// then
require.Nil(t, result)
})

t.Run("TOTP is invalid - do not call Confirm Action", func(t *testing.T) {
// given
tmq := testTransportHandler{
Type: fixtures.RequestType,
Path: "/unknown/",
Result: "{}",
ClientURL: fixtures.ServerURL,
Client: WithHTTPClient,
}

client := getTestWalletClientWithOpts(tmq, WithXPriv(fixtures.XPrivString))

alice := &models.Contact{PubKey: "034252e5359a1de3b8ec08e6c29b80594e88fb47e6ae9ce65ee5a94f0d371d2cde"}
a_totp, err := client.GenerateTotpForContact(alice, 30, 2)
require.NoError(t, err)

bob := &models.Contact{PubKey: "02dde493752f7bc89822ed8a13e0e4aa04550c6c4430800e4be1e5e5c2556cf65b"}

// when
result := client.ConfirmContact(context.Background(), bob, a_totp, 30, 2)

// then
require.NotNil(t, result)
require.Equal(t, result.Error(), "totp is invalid")
})
}
1 change: 1 addition & 0 deletions fixtures/fixtures.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const (
XPrivString = "xprv9s21ZrQH143K3N6qVJQAu4EP51qMcyrKYJLkLgmYXgz58xmVxVLSsbx2DfJUtjcnXK8NdvkHMKfmmg5AJT2nqqRWUrjSHX29qEJwBgBPkJQ"
AccessKeyString = "7779d24ca6f8821f225042bf55e8f80aa41b08b879b72827f51e41e6523b9cd0"
PaymailAddress = "[email protected]"
PubKey = "034252e5359a1de3b8ec08e6c29b80594e88fb47e6ae9ce65ee5a94f0d371d2cde"
)

func MarshallForTestHandler(object any) string {
Expand Down
4 changes: 3 additions & 1 deletion go.mod

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

117 changes: 117 additions & 0 deletions totp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package walletclient

import (
"encoding/base32"
"encoding/hex"
"errors"
"fmt"
"time"

"github.com/bitcoin-sv/spv-wallet-go-client/utils"
"github.com/bitcoin-sv/spv-wallet/models"
"github.com/bitcoinschema/go-bitcoin/v2"
"github.com/libsv/go-bk/bec"
"github.com/libsv/go-bk/bip32"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
)

var ErrClientInitNoXpriv = errors.New("init client with xPriv first")

const (
// Default number of seconds a TOTP is valid for.
TotpDefaultPeriod uint = 30
// Default TOTP length
TotpDefaultDigits uint = 2
)

// GenerateTotpForContact creates one time-based one-time password based on secret shared between the user and the contact
func (b *WalletClient) GenerateTotpForContact(contact *models.Contact, period, digits uint) (string, error) {
secret, err := sharedSecret(b, contact)
if err != nil {
return "", err
}

opts := getTotpOpts(period, digits)
return totp.GenerateCodeCustom(string(secret), time.Now(), *opts)
}

// ValidateTotpForContact validates one time-based one-time password based on secret shared between the user and the contact
func (b *WalletClient) ValidateTotpForContact(contact *models.Contact, passcode string, period, digits uint) (bool, error) {
secret, err := sharedSecret(b, contact)
if err != nil {
return false, err
}

opts := getTotpOpts(period, digits)
return totp.ValidateCustom(passcode, string(secret), time.Now(), *opts)
}

func sharedSecret(b *WalletClient, c *models.Contact) (string, error) {
privKey, pubKey, err := getSharedSecretFactors(b, c)
if err != nil {
return "", err
}

x, _ := bec.S256().ScalarMult(pubKey.X, pubKey.Y, privKey.D.Bytes())
return base32.StdEncoding.EncodeToString(x.Bytes()), nil
}

func getTotpOpts(period, digits uint) *totp.ValidateOpts {
if period == 0 {
period = TotpDefaultPeriod
}

if digits == 0 {
digits = TotpDefaultDigits
}

return &totp.ValidateOpts{
Period: period,
Digits: otp.Digits(digits),
}
}

func getSharedSecretFactors(b *WalletClient, c *models.Contact) (*bec.PrivateKey, *bec.PublicKey, error) {
if b.xPriv == nil {
return nil, nil, ErrClientInitNoXpriv
}

xpriv, err := deriveXprivForPki(b.xPriv)
if err != nil {
return nil, nil, err
}

privKey, err := xpriv.ECPrivKey()
if err != nil {
return nil, nil, err
}

pubKey, err := convertPubKey(c.PubKey)
if err != nil {
return nil, nil, fmt.Errorf("contact's PubKey is invalid: %w", err)
}

return privKey, pubKey, nil
}

func deriveXprivForPki(xpriv *bip32.ExtendedKey) (*bip32.ExtendedKey, error) {
// PKI derivation path: m/0/0/0
// NOTICE: we currently do not support PKI rotation; however, adjustments will be made if and when we decide to implement it

pkiXpriv, err := bitcoin.GetHDKeyByPath(xpriv, utils.ChainExternal, 0)
if err != nil {
return nil, err
}

return pkiXpriv.Child(0)
}

func convertPubKey(pubKey string) (*bec.PublicKey, error) {
hex, err := hex.DecodeString(pubKey)
if err != nil {
return nil, err
}

return bec.ParsePubKey(hex, bec.S256())
}
Loading

0 comments on commit c5f3d61

Please sign in to comment.