Skip to content

Commit

Permalink
feat(SPV-1419): manage contacts flow example with proper TOTP generat…
Browse files Browse the repository at this point in the history
…ion & validation (#320)

Co-authored-by: chris-4chain <[email protected]>
  • Loading branch information
wregulski and chris-4chain authored Jan 30, 2025
1 parent 164f1bf commit ad3ec9d
Show file tree
Hide file tree
Showing 6 changed files with 254 additions and 56 deletions.
16 changes: 8 additions & 8 deletions examples/Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,6 @@ tasks:
- go run ../walletkeys/cmd/main.go
- echo "=================================================================="

generate_totp:
desc: "Generate totp."
silent: true
cmds:
- echo "=================================================================="
- go run ./generate_totp/generate_totp.go
- echo "=================================================================="

get_balance:
desc: "Get balance as User."
silent: true
Expand Down Expand Up @@ -86,6 +78,14 @@ tasks:
- go run ./list_transactions/list_transactions.go
- echo "=================================================================="

manage_contacts:
desc: "Show possible contact scenario with TOTP generation&validation"
silent: true
cmds:
- echo "=================================================================="
- go run ./manage_contacts/manage_contacts.go
- echo "=================================================================="

send_op_return:
desc: "Create draft transaction, finalize transaction and record transaction as User."
silent: true
Expand Down
43 changes: 0 additions & 43 deletions examples/generate_totp/generate_totp.go

This file was deleted.

73 changes: 73 additions & 0 deletions examples/manage_contacts/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package main

import (
"fmt"
"log"

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

type user struct {
xPriv string
xPub string
paymail string
}

type verificationResults struct {
bobValidatedAlicesTotp bool
aliceValidatedBobsTotp bool
}

func examplePaymailCorrectlyEdited(paymail string) string {
if paymail == "" || paymail == "example.com" {
log.Fatal("Invalid configuration - please replace the paymail domain with your own domain")
}
return paymail
}

func assertNoError[T any](val T, err error) T {
if err != nil {
log.Fatalf("unexpected error: %v", err)
}
return val
}

func logSecureMessage(from, to, totp string) {
fmt.Printf("\n!!! SECURE COMMUNICATION REQUIRED !!!\n%s's TOTP code for %s:\n", from, to)
fmt.Printf("TOTP code: %s\n", totp)
fmt.Print("Share using: encrypted message, secure email, phone call or in-person meeting.\n")
}

func mapToContactModel(resp *response.Contact) *models.Contact {
return &models.Contact{
ID: resp.ID,
FullName: resp.FullName,
Paymail: resp.Paymail,
PubKey: resp.PubKey,
Status: resp.Status,
}
}

func setupUsers() {
fmt.Println("0. Setting up users (optional)")

// Create account for Alice
assertNoError(clients.admin.CreateXPub(ctx, &commands.CreateUserXpub{
XPub: config.alice.xPub,
}))
assertNoError(clients.admin.CreatePaymail(ctx, &commands.CreatePaymail{
Key: config.alice.xPub,
Address: config.alice.paymail,
}))

// Create account for Bob
assertNoError(clients.admin.CreateXPub(ctx, &commands.CreateUserXpub{
XPub: config.bob.xPub,
}))
assertNoError(clients.admin.CreatePaymail(ctx, &commands.CreatePaymail{
Key: config.bob.xPub,
Address: config.bob.paymail,
}))
}
168 changes: 168 additions & 0 deletions examples/manage_contacts/manage_contacts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package main

import (
"context"
"fmt"
"log"

wallet "github.com/bitcoin-sv/spv-wallet-go-client"
"github.com/bitcoin-sv/spv-wallet-go-client/commands"
"github.com/bitcoin-sv/spv-wallet-go-client/examples"
"github.com/bitcoin-sv/spv-wallet-go-client/examples/exampleutil"
)

// !!! Adjust the paymail domain to the domain supported by the spv-wallet server
const yourPaymailDomain = "example.com"

// Example configuration – adjust as needed.
// It holds the values required to present the example.
var config = struct {
setupUsers bool
totpDigits uint
totpPeriods uint
server string
paymailDomain string
alice user
bob user
}{
// We assume that the users: Alice and Bob are already registered.
// If they're not, please set this to true to make the example create them.
setupUsers: false,

totpDigits: 2,
totpPeriods: 1200,
server: "http://localhost:3003",
paymailDomain: examplePaymailCorrectlyEdited(yourPaymailDomain),
alice: user{
xPriv: "xprv9s21ZrQH143K2jMwweKF33hFDDvwxEooDtXbZ7mGTJQfmSs8aD77ThuYDsfNrgBAbHr9Yx8FrPaukMLHpxFUyyvBuzAJBMpd4a2xFxr6qts",
xPub: "xpub661MyMwAqRbcFDSR3frFQBdymFmSMhXeb7TCMWAt1dweeFCH7kRN1WE257E65MufrqngaLK46ERg5LHHouHiS8DvHKovmo5VhjLs5vgwqdp",
paymail: "alice" + "@" + yourPaymailDomain,
},
bob: user{
xPriv: "xprv9s21ZrQH143K3DkTDsWwvUb3pwgKoYGp9hxYe2coqZz3pvE1kQfe1dQLdcN82XSeLmw1nGpMZLnXZktf9hFJTu9NRLBpQnGHwYpo4SmszZY",
xPub: "xpub661MyMwAqRbcFhpvKu3xHcXnNyWpCzzfWvt9SR2RPuX2hiZAHwytZRipUtM4qG2PPPF5pZttP3grZM9N9MR5jSek7RRgyggsLJAWFJJUAko",
paymail: "bob" + "@" + yourPaymailDomain,
},
}

var clients = struct {
alice *wallet.UserAPI
bob *wallet.UserAPI
admin *wallet.AdminAPI
}{
alice: assertNoError(wallet.NewUserAPIWithXPriv(exampleutil.NewDefaultConfig(), config.alice.xPriv)),
bob: assertNoError(wallet.NewUserAPIWithXPriv(exampleutil.NewDefaultConfig(), config.bob.xPriv)),
admin: assertNoError(wallet.NewAdminAPIWithXPriv(exampleutil.NewDefaultConfig(), examples.AdminXPriv)),
}

var ctx = context.Background()

func verificationFlow() (*verificationResults, error) {
fmt.Println("\n1. Creating initial contacts")

alicePaymail := config.alice.paymail
bobPaymail := config.bob.paymail

_, err := clients.alice.UpsertContact(ctx, commands.UpsertContact{
ContactPaymail: bobPaymail,
FullName: "Bob Smith",
RequesterPaymail: alicePaymail,
})
if err != nil {
return nil, fmt.Errorf("failed to create Bob's contact for Alice: %w", err)
}

_, err = clients.bob.UpsertContact(ctx, commands.UpsertContact{
ContactPaymail: alicePaymail,
FullName: "Alice Smith",
RequesterPaymail: bobPaymail,
})
if err != nil {
return nil, fmt.Errorf("failed to create Alice's contact for Bob: %w", err)
}

respBob, err := clients.alice.ContactWithPaymail(ctx, bobPaymail)
if err != nil {
return nil, fmt.Errorf("failed to get Bob's contact: %w", err)
}
bobContact := mapToContactModel(respBob)

respAlice, err := clients.bob.ContactWithPaymail(ctx, alicePaymail)
if err != nil {
return nil, fmt.Errorf("failed to get Alice's contact: %w", err)
}
aliceContact := mapToContactModel(respAlice)

fmt.Println("\n2. Alice initiates verification")
aliceTotpForBob, err := clients.alice.GenerateTotpForContact(bobContact, config.totpPeriods, config.totpDigits)
if err != nil {
return nil, fmt.Errorf("failed to generate Alice's TOTP for Bob: %w", err)
}
logSecureMessage("Alice", "Bob", aliceTotpForBob)

fmt.Println("\n3. Bob validates Alice's TOTP")
bobValidationErr := clients.bob.ValidateTotpForContact(aliceContact, aliceTotpForBob, respBob.Paymail, config.totpPeriods, config.totpDigits)
bobValidatedAlicesTotp := bobValidationErr == nil
fmt.Printf("Validation status: %v\n", bobValidatedAlicesTotp)

fmt.Println("\n4. Bob initiates verification")
bobTotpForAlice, err := clients.bob.GenerateTotpForContact(aliceContact, config.totpPeriods, config.totpDigits)
if err != nil {
return nil, fmt.Errorf("failed to generate Bob's TOTP for Alice: %w", err)
}
logSecureMessage("Bob", "Alice", bobTotpForAlice)

fmt.Println("\n5. Alice validates Bob's TOTP")
aliceValidationErr := clients.alice.ValidateTotpForContact(bobContact, bobTotpForAlice, respAlice.Paymail, config.totpPeriods, config.totpDigits)
aliceValidatedBobsTotp := aliceValidationErr == nil
fmt.Printf("Validation status: %v\n", aliceValidatedBobsTotp)

return &verificationResults{
bobValidatedAlicesTotp: bobValidatedAlicesTotp,
aliceValidatedBobsTotp: aliceValidatedBobsTotp,
}, nil
}

func finalizeAndCleanup(results *verificationResults) error {
isFullyVerified := results.bobValidatedAlicesTotp && results.aliceValidatedBobsTotp
fmt.Printf("\nBidirectional verification complete: %v\n", isFullyVerified)

if isFullyVerified {
fmt.Println("\n6. Admin confirms verified contacts")
if err := clients.admin.ConfirmContacts(ctx, &commands.ConfirmContacts{
PaymailA: config.alice.paymail,
PaymailB: config.bob.paymail,
}); err != nil {
_ = fmt.Errorf("failed to confirm contacts: %w", err)
}
}

fmt.Println("\n7. Cleaning up contacts")
if err := clients.alice.RemoveContact(ctx, config.bob.paymail); err != nil {
return fmt.Errorf("failed to remove Bob's contact: %w", err)
}

if err := clients.bob.RemoveContact(ctx, config.alice.paymail); err != nil {
return fmt.Errorf("failed to remove Alice's contact: %w", err)
}

return nil
}

func main() {
if config.setupUsers {
setupUsers()
} else {
fmt.Println("We assume that the users: Alice and Bob are already registered.")
fmt.Println("If they're not, please set config.setupUsers to true to make the example create them.")
}

results, err := verificationFlow()
if err != nil {
log.Fatalf("Error during verification flow: %v", err)
}

if err := finalizeAndCleanup(results); err != nil {
log.Fatalf("Error during cleanup: %v", err)
}
}
6 changes: 3 additions & 3 deletions internal/api/v1/user/totp/totp.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ func (b *API) GenerateTotpForContact(contact *models.Contact, period, digits uin
}

// ValidateTotpForContact validates a TOTP for a contact.
func (b *API) ValidateTotpForContact(contact *models.Contact, passcode, requesterPaymail string, period, digits uint) error {
sharedSecret, err := b.makeSharedSecret(contact)
func (b *API) ValidateTotpForContact(generatorContact *models.Contact, passcode, validatorContact string, period, digits uint) error {
sharedSecret, err := b.makeSharedSecret(generatorContact)
if err != nil {
return fmt.Errorf("ValidateTotpForContact: error when making shared secret: %w", err)
}

opts := getTotpOpts(period, digits)
valid, err := totp.ValidateCustom(passcode, directedSecret(sharedSecret, requesterPaymail), time.Now(), *opts)
valid, err := totp.ValidateCustom(passcode, directedSecret(sharedSecret, validatorContact), time.Now(), *opts)
if err != nil {
return fmt.Errorf("ValidateTotpForContact: error when validating TOTP: %w", err)
}
Expand Down
4 changes: 2 additions & 2 deletions user_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -398,12 +398,12 @@ func (u *UserAPI) GenerateTotpForContact(contact *models.Contact, period, digits
}

// ValidateTotpForContact validates a TOTP code for the specified contact.
func (u *UserAPI) ValidateTotpForContact(contact *models.Contact, passcode, requesterPaymail string, period, digits uint) error {
func (u *UserAPI) ValidateTotpForContact(generatorContact *models.Contact, passcode, validatorPaymail string, period, digits uint) error {
if u.totpAPI == nil {
return errors.New("totp client not initialized - xPriv authentication required")
}

if err := u.totpAPI.ValidateTotpForContact(contact, passcode, requesterPaymail, period, digits); err != nil {
if err := u.totpAPI.ValidateTotpForContact(generatorContact, passcode, validatorPaymail, period, digits); err != nil {
return fmt.Errorf("failed to validate TOTP for contact: %w", err)
}

Expand Down

0 comments on commit ad3ec9d

Please sign in to comment.