From 16cf2c2d0acf11a7145c9636e0a6fce1887034f5 Mon Sep 17 00:00:00 2001 From: optclblast Date: Sun, 5 May 2024 00:48:03 +0300 Subject: [PATCH] join handler implemented --- backend/Makefile | 23 +- backend/cmd/main.go | 2 +- backend/go.mod | 5 +- backend/go.sum | 9 +- backend/internal/factory/service.go | 11 +- backend/internal/factory/wire_gen.go | 10 +- .../interface/rest/controllers/auth.go | 72 + .../interface/{ => rest}/controllers/ping.go | 5 +- .../interface/{ => rest}/controllers/root.go | 3 + backend/internal/interface/rest/domain/dto.go | 20 + .../interface/rest/presenters/auth.go | 34 + backend/internal/interface/rest/server.go | 39 +- backend/internal/pkg/bip32/bip32.go | 348 +++ backend/internal/pkg/bip32/wordslist.go | 2071 +++++++++++++++++ backend/internal/{ => pkg}/config/config.go | 0 backend/internal/{ => pkg}/logger/logger.go | 0 .../internal/{ => pkg}/logger/slogdiscard.go | 0 .../internal/{ => pkg}/logger/slogpretty.go | 0 backend/internal/pkg/models/user.go | 6 + backend/internal/{ => pkg}/sqlutils/tx.go | 0 backend/internal/service/service.go | 2 + .../internal/usecase/interactors/jwt/jwt.go | 39 + 22 files changed, 2667 insertions(+), 32 deletions(-) create mode 100644 backend/internal/interface/rest/controllers/auth.go rename backend/internal/interface/{ => rest}/controllers/ping.go (59%) rename backend/internal/interface/{ => rest}/controllers/root.go (76%) create mode 100644 backend/internal/interface/rest/domain/dto.go create mode 100644 backend/internal/interface/rest/presenters/auth.go create mode 100644 backend/internal/pkg/bip32/bip32.go create mode 100644 backend/internal/pkg/bip32/wordslist.go rename backend/internal/{ => pkg}/config/config.go (100%) rename backend/internal/{ => pkg}/logger/logger.go (100%) rename backend/internal/{ => pkg}/logger/slogdiscard.go (100%) rename backend/internal/{ => pkg}/logger/slogpretty.go (100%) create mode 100644 backend/internal/pkg/models/user.go rename backend/internal/{ => pkg}/sqlutils/tx.go (100%) create mode 100644 backend/internal/usecase/interactors/jwt/jwt.go diff --git a/backend/Makefile b/backend/Makefile index ee2bba6..825238b 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -1,14 +1,27 @@ +PROJECT_DIR = $(CURDIR) +PROJECT_BIN = ${PROJECT_DIR}/bin +TOOLS_BIN = ${PROJECT_BIN}/tools + bin.build: - mkdir -p build - rm -f build/blockd - go build -ldflags="-s -w" -o build/blockd cmd/main.go + mkdir -p ${PROJECT_DIR}/build + rm -f ${PROJECT_DIR}/build/blockd + go build -ldflags="-s -w" -o ${PROJECT_DIR}/build/blockd ${PROJECT_DIR}/cmd/main.go d.build: sudo docker buildx build . -t blockd:latest +d.net: + sudo docker network create --driver bridge --subnet=192.168.2.0/24 --attachable blockd-net + +d.drop-net: + sudo docker network rm blockd-net + +up: + sudo docker compose up -d + .PHONY: run.local run.local: bin.build - ./build/blockd \ + ${PROJECT_DIR}/build/blockd \ -log-level=debug \ -log-local=true \ -log-add-source=true \ @@ -21,7 +34,7 @@ run.local: bin.build .PHONY: run.debug run.debug: bin.build - ./build/blockd \ + ${PROJECT_DIR}/build/blockd \ -log-level=debug \ -log-local=false \ -log-add-source=true \ diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 846cd97..0b726bb 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -7,8 +7,8 @@ import ( "syscall" "github.com/emochka2007/block-accounting/cmd/commands" - "github.com/emochka2007/block-accounting/internal/config" "github.com/emochka2007/block-accounting/internal/factory" + "github.com/emochka2007/block-accounting/internal/pkg/config" cli "github.com/urfave/cli/v2" ) diff --git a/backend/go.mod b/backend/go.mod index aaf8a15..5871c2b 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -5,12 +5,15 @@ go 1.22.2 require ( github.com/fatih/color v1.16.0 github.com/go-chi/chi/v5 v5.0.12 + github.com/go-chi/render v1.0.3 + github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/wire v0.6.0 github.com/urfave/cli/v2 v2.27.2 - golang.org/x/sync v0.7.0 + golang.org/x/crypto v0.18.0 ) require ( + github.com/ajg/form v1.5.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect diff --git a/backend/go.sum b/backend/go.sum index 8ed4195..2280286 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,9 +1,15 @@ +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= +github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI= @@ -24,6 +30,7 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -41,8 +48,6 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/backend/internal/factory/service.go b/backend/internal/factory/service.go index c73a6c8..e21bf52 100644 --- a/backend/internal/factory/service.go +++ b/backend/internal/factory/service.go @@ -7,10 +7,11 @@ import ( "log/slog" "os" - "github.com/emochka2007/block-accounting/internal/config" - "github.com/emochka2007/block-accounting/internal/interface/controllers" "github.com/emochka2007/block-accounting/internal/interface/rest" - "github.com/emochka2007/block-accounting/internal/logger" + "github.com/emochka2007/block-accounting/internal/interface/rest/controllers" + "github.com/emochka2007/block-accounting/internal/interface/rest/presenters" + "github.com/emochka2007/block-accounting/internal/pkg/config" + "github.com/emochka2007/block-accounting/internal/pkg/logger" "github.com/emochka2007/block-accounting/internal/service" "github.com/google/wire" ) @@ -54,6 +55,10 @@ func provideControllers( ) *controllers.RootController { return &controllers.RootController{ Ping: controllers.NewPingController(log.WithGroup("ping-controller")), + Auth: controllers.NewAuthController( + log.WithGroup("auth-controller"), + presenters.NewAuthPresenter(), + ), } } diff --git a/backend/internal/factory/wire_gen.go b/backend/internal/factory/wire_gen.go index ecd20f9..7bc10d6 100644 --- a/backend/internal/factory/wire_gen.go +++ b/backend/internal/factory/wire_gen.go @@ -7,10 +7,11 @@ package factory import ( - "github.com/emochka2007/block-accounting/internal/config" - "github.com/emochka2007/block-accounting/internal/interface/controllers" "github.com/emochka2007/block-accounting/internal/interface/rest" - "github.com/emochka2007/block-accounting/internal/logger" + "github.com/emochka2007/block-accounting/internal/interface/rest/controllers" + "github.com/emochka2007/block-accounting/internal/interface/rest/presenters" + "github.com/emochka2007/block-accounting/internal/pkg/config" + "github.com/emochka2007/block-accounting/internal/pkg/logger" "github.com/emochka2007/block-accounting/internal/service" "log/slog" "os" @@ -57,6 +58,9 @@ func provideControllers( ) *controllers.RootController { return &controllers.RootController{ Ping: controllers.NewPingController(log.WithGroup("ping-controller")), + Auth: controllers.NewAuthController( + log.WithGroup("auth-controller"), presenters.NewAuthPresenter(), + ), } } diff --git a/backend/internal/interface/rest/controllers/auth.go b/backend/internal/interface/rest/controllers/auth.go new file mode 100644 index 0000000..e94598b --- /dev/null +++ b/backend/internal/interface/rest/controllers/auth.go @@ -0,0 +1,72 @@ +package controllers + +import ( + "encoding/json" + "fmt" + "log/slog" + "net/http" + + "github.com/emochka2007/block-accounting/internal/interface/rest/domain" + "github.com/emochka2007/block-accounting/internal/interface/rest/presenters" + "github.com/emochka2007/block-accounting/internal/pkg/bip32" +) + +type AuthController interface { + Join(w http.ResponseWriter, req *http.Request) error + Login(w http.ResponseWriter, req *http.Request) error + Invite(w http.ResponseWriter, req *http.Request) error +} + +type authController struct { + log *slog.Logger + presenter presenters.AuthPresenter + // interactors ... +} + +func NewAuthController( + log *slog.Logger, + presenter presenters.AuthPresenter, +) AuthController { + return &authController{ + log: log, + presenter: presenter, + } +} + +const mnemonicEntropyBitSize int = 256 + +func (c *authController) Join(w http.ResponseWriter, req *http.Request) error { + entropy, err := bip32.NewEntropy(mnemonicEntropyBitSize) + if err != nil { + return fmt.Errorf("error generate new entropy. %w", err) + } + + mnemonic, err := bip32.NewMnemonic(entropy) + if err != nil { + return fmt.Errorf("error generate mnemonic from entropy. %w", err) + } + + // todo create user + + // move to presenter + out, err := json.Marshal(domain.JoinResponse{ + Mnemonic: mnemonic, + }) + if err != nil { + return fmt.Errorf("error marshal join response. %w", err) + } + + w.Write(out) + + // move to presenter + + return nil +} + +func (c *authController) Login(w http.ResponseWriter, req *http.Request) error { + return nil // implement +} + +func (c *authController) Invite(w http.ResponseWriter, req *http.Request) error { + return nil // implement +} diff --git a/backend/internal/interface/controllers/ping.go b/backend/internal/interface/rest/controllers/ping.go similarity index 59% rename from backend/internal/interface/controllers/ping.go rename to backend/internal/interface/rest/controllers/ping.go index 5be0092..8932ad1 100644 --- a/backend/internal/interface/controllers/ping.go +++ b/backend/internal/interface/rest/controllers/ping.go @@ -1,13 +1,12 @@ package controllers import ( - "context" "log/slog" "net/http" ) type PingController interface { - HandlePing(ctx context.Context, req *http.Request, w http.ResponseWriter) error + Ping(w http.ResponseWriter, req *http.Request) error } type pingController struct { @@ -22,7 +21,7 @@ func NewPingController( } } -func (c *pingController) HandlePing(ctx context.Context, req *http.Request, w http.ResponseWriter) error { +func (c *pingController) Ping(w http.ResponseWriter, req *http.Request) error { _, err := w.Write([]byte("pong")) return err diff --git a/backend/internal/interface/controllers/root.go b/backend/internal/interface/rest/controllers/root.go similarity index 76% rename from backend/internal/interface/controllers/root.go rename to backend/internal/interface/rest/controllers/root.go index 15fb840..1d5b182 100644 --- a/backend/internal/interface/controllers/root.go +++ b/backend/internal/interface/rest/controllers/root.go @@ -2,12 +2,15 @@ package controllers type RootController struct { Ping PingController + Auth AuthController } func NewRootController( ping PingController, + auth AuthController, ) *RootController { return &RootController{ Ping: ping, + Auth: auth, } } diff --git a/backend/internal/interface/rest/domain/dto.go b/backend/internal/interface/rest/domain/dto.go new file mode 100644 index 0000000..a6bb79b --- /dev/null +++ b/backend/internal/interface/rest/domain/dto.go @@ -0,0 +1,20 @@ +package domain + +import ( + "encoding/json" + "fmt" +) + +type JoinResponse struct { + Mnemonic string `json:"mnemonic"` +} + +func BuildRequest[T any](data []byte) (*T, error) { + var req T + + if err := json.Unmarshal(data, &req); err != nil { + return nil, fmt.Errorf("error unmarshal request. %w", err) + } + + return &req, nil +} diff --git a/backend/internal/interface/rest/presenters/auth.go b/backend/internal/interface/rest/presenters/auth.go new file mode 100644 index 0000000..2d62eb0 --- /dev/null +++ b/backend/internal/interface/rest/presenters/auth.go @@ -0,0 +1,34 @@ +package presenters + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/emochka2007/block-accounting/internal/interface/rest/domain" +) + +type AuthPresenter interface { + ResponseJoin(w http.ResponseWriter, mnemonic string) error +} + +type authPresenter struct{} + +func NewAuthPresenter() AuthPresenter { + return &authPresenter{} +} + +func (p *authPresenter) ResponseJoin(w http.ResponseWriter, mnemonic string) error { + out, err := json.Marshal(domain.JoinResponse{ + Mnemonic: mnemonic, + }) + if err != nil { + return fmt.Errorf("error marshal join response. %w", err) + } + + if _, err = w.Write(out); err != nil { + return fmt.Errorf("error write response. %w", err) + } + + return nil +} diff --git a/backend/internal/interface/rest/server.go b/backend/internal/interface/rest/server.go index 6235480..a26092f 100644 --- a/backend/internal/interface/rest/server.go +++ b/backend/internal/interface/rest/server.go @@ -7,11 +7,12 @@ import ( "net/http" "sync" - "github.com/emochka2007/block-accounting/internal/config" - "github.com/emochka2007/block-accounting/internal/interface/controllers" - "github.com/emochka2007/block-accounting/internal/logger" + "github.com/emochka2007/block-accounting/internal/interface/rest/controllers" + "github.com/emochka2007/block-accounting/internal/pkg/config" + "github.com/emochka2007/block-accounting/internal/pkg/logger" "github.com/go-chi/chi/v5" mw "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/render" ) type Server struct { @@ -63,23 +64,25 @@ func (s *Server) Serve(ctx context.Context) error { func (s *Server) Close() { s.closeMu.Lock() - defer s.closeMu.Unlock() s.closed = true + + s.closeMu.Unlock() } func (s *Server) buildRouter() { s.Mux = chi.NewRouter() - s.With(mw.Recoverer) - s.With(mw.RequestID) - s.With(s.handleMw) + s.Use(mw.Recoverer) + s.Use(mw.RequestID) + s.Use(s.handleMw) + s.Use(render.SetContentType(render.ContentTypeJSON)) s.Get("/ping", s.handlePing) // debug // auth - s.Post("/join", nil) // new user - s.Post("/login", nil) // login + s.Post("/join", s.handleJoin) // new user + s.Post("/login", nil) // login s.Route("/organization/{organization_id}", func(r chi.Router) { s.Route("/transactions", func(r chi.Router) { @@ -118,22 +121,30 @@ func (s *Server) responseError(w http.ResponseWriter, e error) { func (s *Server) handleMw(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { s.closeMu.RLock() - defer s.closeMu.Unlock() + defer s.closeMu.RUnlock() if s.closed { // keep mutex closed return } + w.Header().Add("Content-Type", "application/json") + next.ServeHTTP(w, r) } return http.HandlerFunc(fn) } -func (s *Server) handlePing(w http.ResponseWriter, req *http.Request) { - s.log.Debug("ping request") - - if err := s.controllers.Ping.HandlePing(s.ctx, req, w); err != nil { +func (s *Server) handleJoin(w http.ResponseWriter, req *http.Request) { + if err := s.controllers.Auth.Join(w, req); err != nil { + s.responseError(w, err) + } +} + +func (s *Server) handlePing(w http.ResponseWriter, req *http.Request) { + s.log.Debug("ping request") + + if err := s.controllers.Ping.Ping(w, req); err != nil { s.responseError(w, err) } } diff --git a/backend/internal/pkg/bip32/bip32.go b/backend/internal/pkg/bip32/bip32.go new file mode 100644 index 0000000..9a74235 --- /dev/null +++ b/backend/internal/pkg/bip32/bip32.go @@ -0,0 +1,348 @@ +package bip32 + +import ( + "crypto/rand" + "crypto/sha256" + "crypto/sha512" + "encoding/binary" + "errors" + "fmt" + "math/big" + "strings" + + "golang.org/x/crypto/pbkdf2" +) + +var ( + // Some bitwise operands for working with big.Ints. + last11BitsMask = big.NewInt(2047) + shift11BitsMask = big.NewInt(2048) + bigOne = big.NewInt(1) + bigTwo = big.NewInt(2) + + // wordLengthChecksumMasksMapping is used to isolate the checksum bits from + //the entropy+checksum byte array. + wordLengthChecksumMasksMapping = map[int]*big.Int{ + 12: big.NewInt(15), + 15: big.NewInt(31), + 18: big.NewInt(63), + 21: big.NewInt(127), + 24: big.NewInt(255), + } + + // wordLengthChecksumShiftMapping is used to lookup the number of operand + // for shifting bits to handle checksums. + wordLengthChecksumShiftMapping = map[int]*big.Int{ + 12: big.NewInt(16), + 15: big.NewInt(8), + 18: big.NewInt(4), + 21: big.NewInt(2), + } + + // wordList is the set of words to use. + wordList []string + + // wordMap is a reverse lookup map for wordList. + wordMap map[string]int +) + +var ( + // ErrInvalidMnemonic is returned when trying to use a malformed mnemonic. + ErrInvalidMnemonic = errors.New("Invalid mnenomic") + + // ErrEntropyLengthInvalid is returned when trying to use an entropy set with + // an invalid size. + ErrEntropyLengthInvalid = errors.New("Entropy length must be [128, 256] and a multiple of 32") + + // ErrValidatedSeedLengthMismatch is returned when a validated seed is not the + // same size as the given seed. This should never happen is present only as a + // sanity assertion. + ErrValidatedSeedLengthMismatch = errors.New("Seed length does not match validated seed length") + + // ErrChecksumIncorrect is returned when entropy has the incorrect checksum. + ErrChecksumIncorrect = errors.New("Checksum incorrect") +) + +func init() { + SetWordList(English) +} + +// SetWordList sets the list of words to use for mnemonics. Currently the list +// that is set is used package-wide. +func SetWordList(list []string) { + wordList = list + wordMap = map[string]int{} + + for i, v := range wordList { + wordMap[v] = i + } +} + +// GetWordList gets the list of words to use for mnemonics. +func GetWordList() []string { + return wordList +} + +// GetWordIndex gets word index in wordMap. +func GetWordIndex(word string) (int, bool) { + idx, ok := wordMap[word] + return idx, ok +} + +// NewEntropy will create random entropy bytes +// so long as the requested size bitSize is an appropriate size. +// +// bitSize has to be a multiple 32 and be within the inclusive range of {128, 256}. +func NewEntropy(bitSize int) ([]byte, error) { + if err := validateEntropyBitSize(bitSize); err != nil { + return nil, err + } + + entropy := make([]byte, bitSize/8) + _, _ = rand.Read(entropy) // err is always nil + + return entropy, nil +} + +// EntropyFromMnemonic takes a mnemonic generated by this library, +// and returns the input entropy used to generate the given mnemonic. +// An error is returned if the given mnemonic is invalid. +func EntropyFromMnemonic(mnemonic string) ([]byte, error) { + mnemonicSlice, isValid := splitMnemonicWords(mnemonic) + if !isValid { + return nil, ErrInvalidMnemonic + } + + // Decode the words into a big.Int. + var ( + wordBytes [2]byte + b = big.NewInt(0) + ) + + for _, v := range mnemonicSlice { + index, found := wordMap[v] + if !found { + return nil, fmt.Errorf("word `%v` not found in reverse map", v) + } + + binary.BigEndian.PutUint16(wordBytes[:], uint16(index)) + b.Mul(b, shift11BitsMask) + b.Or(b, big.NewInt(0).SetBytes(wordBytes[:])) + } + + // Build and add the checksum to the big.Int. + checksum := big.NewInt(0) + checksumMask := wordLengthChecksumMasksMapping[len(mnemonicSlice)] + checksum = checksum.And(b, checksumMask) + + b.Div(b, big.NewInt(0).Add(checksumMask, bigOne)) + + // The entropy is the underlying bytes of the big.Int. Any upper bytes of + // all 0's are not returned so we pad the beginning of the slice with empty + // bytes if necessary. + entropy := b.Bytes() + entropy = padByteSlice(entropy, len(mnemonicSlice)/3*4) + + // Generate the checksum and compare with the one we got from the mneomnic. + entropyChecksumBytes := computeChecksum(entropy) + entropyChecksum := big.NewInt(int64(entropyChecksumBytes[0])) + + if l := len(mnemonicSlice); l != 24 { + checksumShift := wordLengthChecksumShiftMapping[l] + entropyChecksum.Div(entropyChecksum, checksumShift) + } + + if checksum.Cmp(entropyChecksum) != 0 { + return nil, ErrChecksumIncorrect + } + + return entropy, nil +} + +// NewMnemonic will return a string consisting of the mnemonic words for +// the given entropy. +// If the provide entropy is invalid, an error will be returned. +func NewMnemonic(entropy []byte) (string, error) { + // Compute some lengths for convenience. + entropyBitLength := len(entropy) * 8 + checksumBitLength := entropyBitLength / 32 + sentenceLength := (entropyBitLength + checksumBitLength) / 11 + + // Validate that the requested size is supported. + err := validateEntropyBitSize(entropyBitLength) + if err != nil { + return "", err + } + + // Add checksum to entropy. + entropy = addChecksum(entropy) + + // Break entropy up into sentenceLength chunks of 11 bits. + // For each word AND mask the rightmost 11 bits and find the word at that index. + // Then bitshift entropy 11 bits right and repeat. + // Add to the last empty slot so we can work with LSBs instead of MSB. + + // Entropy as an int so we can bitmask without worrying about bytes slices. + entropyInt := new(big.Int).SetBytes(entropy) + + // Slice to hold words in. + words := make([]string, sentenceLength) + + // Throw away big.Int for AND masking. + word := big.NewInt(0) + + for i := sentenceLength - 1; i >= 0; i-- { + // Get 11 right most bits and bitshift 11 to the right for next time. + word.And(entropyInt, last11BitsMask) + entropyInt.Div(entropyInt, shift11BitsMask) + + // Get the bytes representing the 11 bits as a 2 byte slice. + wordBytes := padByteSlice(word.Bytes(), 2) + + // Convert bytes to an index and add that word to the list. + words[i] = wordList[binary.BigEndian.Uint16(wordBytes)] + } + + return strings.Join(words, " "), nil +} + +// MnemonicToByteArray takes a mnemonic string and turns it into a byte array +// suitable for creating another mnemonic. +// An error is returned if the mnemonic is invalid. +func MnemonicToByteArray(mnemonic string, raw ...bool) ([]byte, error) { + var ( + mnemonicSlice = strings.Split(mnemonic, " ") + entropyBitSize = len(mnemonicSlice) * 11 + checksumBitSize = entropyBitSize % 32 + fullByteSize = (entropyBitSize-checksumBitSize)/8 + 1 + ) + + // Turn into raw entropy. + rawEntropyBytes, err := EntropyFromMnemonic(mnemonic) + if err != nil { + return nil, err + } + + // If we want the raw entropy then we're done. + if len(raw) > 0 && raw[0] { + return rawEntropyBytes, nil + } + + // Otherwise add the checksum before returning + return padByteSlice(addChecksum(rawEntropyBytes), fullByteSize), nil +} + +// NewSeedWithErrorChecking creates a hashed seed output given the mnemonic string and a password. +// An error is returned if the mnemonic is not convertible to a byte array. +func NewSeedWithErrorChecking(mnemonic string, password string) ([]byte, error) { + _, err := MnemonicToByteArray(mnemonic) + if err != nil { + return nil, err + } + + return NewSeed(mnemonic, password), nil +} + +// NewSeed creates a hashed seed output given a provided string and password. +// No checking is performed to validate that the string provided is a valid mnemonic. +func NewSeed(mnemonic string, password string) []byte { + return pbkdf2.Key([]byte(mnemonic), []byte("mnemonic"+password), 2048, 64, sha512.New) +} + +// IsMnemonicValid attempts to verify that the provided mnemonic is valid. +// Validity is determined by both the number of words being appropriate, +// and that all the words in the mnemonic are present in the word list. +func IsMnemonicValid(mnemonic string) bool { + _, err := EntropyFromMnemonic(mnemonic) + return err == nil +} + +// Appends to data the first (len(data) / 32)bits of the result of sha256(data) +// Currently only supports data up to 32 bytes. +func addChecksum(data []byte) []byte { + // Get first byte of sha256 + hash := computeChecksum(data) + firstChecksumByte := hash[0] + + // len() is in bytes so we divide by 4 + checksumBitLength := uint(len(data) / 4) + + // For each bit of check sum we want we shift the data one the left + // and then set the (new) right most bit equal to checksum bit at that index + // staring from the left + dataBigInt := new(big.Int).SetBytes(data) + + for i := uint(0); i < checksumBitLength; i++ { + // Bitshift 1 left + dataBigInt.Mul(dataBigInt, bigTwo) + + // Set rightmost bit if leftmost checksum bit is set + if firstChecksumByte&(1<<(7-i)) > 0 { + dataBigInt.Or(dataBigInt, bigOne) + } + } + + return dataBigInt.Bytes() +} + +func computeChecksum(data []byte) []byte { + hasher := sha256.New() + _, _ = hasher.Write(data) // This error is guaranteed to be nil + + return hasher.Sum(nil) +} + +// validateEntropyBitSize ensures that entropy is the correct size for being a +// mnemonic. +func validateEntropyBitSize(bitSize int) error { + if (bitSize%32) != 0 || bitSize < 128 || bitSize > 256 { + return ErrEntropyLengthInvalid + } + + return nil +} + +// padByteSlice returns a byte slice of the given size with contents of the +// given slice left padded and any empty spaces filled with 0's. +func padByteSlice(slice []byte, length int) []byte { + offset := length - len(slice) + if offset <= 0 { + return slice + } + + newSlice := make([]byte, length) + copy(newSlice[offset:], slice) + + return newSlice +} + +// compareByteSlices returns true of the byte slices have equal contents and +// returns false otherwise. +func compareByteSlices(a, b []byte) bool { + if len(a) != len(b) { + return false + } + + for i := range a { + if a[i] != b[i] { + return false + } + } + + return true +} + +func splitMnemonicWords(mnemonic string) ([]string, bool) { + // Create a list of all the words in the mnemonic sentence + words := strings.Fields(mnemonic) + + // Get num of words + numOfWords := len(words) + + // The number of words should be 12, 15, 18, 21 or 24 + if numOfWords%3 != 0 || numOfWords < 12 || numOfWords > 24 { + return nil, false + } + + return words, true +} diff --git a/backend/internal/pkg/bip32/wordslist.go b/backend/internal/pkg/bip32/wordslist.go new file mode 100644 index 0000000..1a998d1 --- /dev/null +++ b/backend/internal/pkg/bip32/wordslist.go @@ -0,0 +1,2071 @@ +package bip32 + +import ( + "fmt" + "hash/crc32" + "strings" +) + +func init() { + // Ensure word list is correct + // $ wget https://raw.githubusercontent.com/bitcoin/bips/master/bip-0039/english.txt + // $ crc32 english.txt + // c1dbd296 + checksum := crc32.ChecksumIEEE([]byte(english)) + if fmt.Sprintf("%x", checksum) != "c1dbd296" { + panic("english checksum invalid") // TODO remove panic + } +} + +// English is a slice of mnemonic words taken from the bip39 specification +// https://raw.githubusercontent.com/bitcoin/bips/master/bip-0039/english.txt +var English = strings.Split(strings.TrimSpace(english), "\n") +var english = `abandon +ability +able +about +above +absent +absorb +abstract +absurd +abuse +access +accident +account +accuse +achieve +acid +acoustic +acquire +across +act +action +actor +actress +actual +adapt +add +addict +address +adjust +admit +adult +advance +advice +aerobic +affair +afford +afraid +again +age +agent +agree +ahead +aim +air +airport +aisle +alarm +album +alcohol +alert +alien +all +alley +allow +almost +alone +alpha +already +also +alter +always +amateur +amazing +among +amount +amused +analyst +anchor +ancient +anger +angle +angry +animal +ankle +announce +annual +another +answer +antenna +antique +anxiety +any +apart +apology +appear +apple +approve +april +arch +arctic +area +arena +argue +arm +armed +armor +army +around +arrange +arrest +arrive +arrow +art +artefact +artist +artwork +ask +aspect +assault +asset +assist +assume +asthma +athlete +atom +attack +attend +attitude +attract +auction +audit +august +aunt +author +auto +autumn +average +avocado +avoid +awake +aware +away +awesome +awful +awkward +axis +baby +bachelor +bacon +badge +bag +balance +balcony +ball +bamboo +banana +banner +bar +barely +bargain +barrel +base +basic +basket +battle +beach +bean +beauty +because +become +beef +before +begin +behave +behind +believe +below +belt +bench +benefit +best +betray +better +between +beyond +bicycle +bid +bike +bind +biology +bird +birth +bitter +black +blade +blame +blanket +blast +bleak +bless +blind +blood +blossom +blouse +blue +blur +blush +board +boat +body +boil +bomb +bone +bonus +book +boost +border +boring +borrow +boss +bottom +bounce +box +boy +bracket +brain +brand +brass +brave +bread +breeze +brick +bridge +brief +bright +bring +brisk +broccoli +broken +bronze +broom +brother +brown +brush +bubble +buddy +budget +buffalo +build +bulb +bulk +bullet +bundle +bunker +burden +burger +burst +bus +business +busy +butter +buyer +buzz +cabbage +cabin +cable +cactus +cage +cake +call +calm +camera +camp +can +canal +cancel +candy +cannon +canoe +canvas +canyon +capable +capital +captain +car +carbon +card +cargo +carpet +carry +cart +case +cash +casino +castle +casual +cat +catalog +catch +category +cattle +caught +cause +caution +cave +ceiling +celery +cement +census +century +cereal +certain +chair +chalk +champion +change +chaos +chapter +charge +chase +chat +cheap +check +cheese +chef +cherry +chest +chicken +chief +child +chimney +choice +choose +chronic +chuckle +chunk +churn +cigar +cinnamon +circle +citizen +city +civil +claim +clap +clarify +claw +clay +clean +clerk +clever +click +client +cliff +climb +clinic +clip +clock +clog +close +cloth +cloud +clown +club +clump +cluster +clutch +coach +coast +coconut +code +coffee +coil +coin +collect +color +column +combine +come +comfort +comic +common +company +concert +conduct +confirm +congress +connect +consider +control +convince +cook +cool +copper +copy +coral +core +corn +correct +cost +cotton +couch +country +couple +course +cousin +cover +coyote +crack +cradle +craft +cram +crane +crash +crater +crawl +crazy +cream +credit +creek +crew +cricket +crime +crisp +critic +crop +cross +crouch +crowd +crucial +cruel +cruise +crumble +crunch +crush +cry +crystal +cube +culture +cup +cupboard +curious +current +curtain +curve +cushion +custom +cute +cycle +dad +damage +damp +dance +danger +daring +dash +daughter +dawn +day +deal +debate +debris +decade +december +decide +decline +decorate +decrease +deer +defense +define +defy +degree +delay +deliver +demand +demise +denial +dentist +deny +depart +depend +deposit +depth +deputy +derive +describe +desert +design +desk +despair +destroy +detail +detect +develop +device +devote +diagram +dial +diamond +diary +dice +diesel +diet +differ +digital +dignity +dilemma +dinner +dinosaur +direct +dirt +disagree +discover +disease +dish +dismiss +disorder +display +distance +divert +divide +divorce +dizzy +doctor +document +dog +doll +dolphin +domain +donate +donkey +donor +door +dose +double +dove +draft +dragon +drama +drastic +draw +dream +dress +drift +drill +drink +drip +drive +drop +drum +dry +duck +dumb +dune +during +dust +dutch +duty +dwarf +dynamic +eager +eagle +early +earn +earth +easily +east +easy +echo +ecology +economy +edge +edit +educate +effort +egg +eight +either +elbow +elder +electric +elegant +element +elephant +elevator +elite +else +embark +embody +embrace +emerge +emotion +employ +empower +empty +enable +enact +end +endless +endorse +enemy +energy +enforce +engage +engine +enhance +enjoy +enlist +enough +enrich +enroll +ensure +enter +entire +entry +envelope +episode +equal +equip +era +erase +erode +erosion +error +erupt +escape +essay +essence +estate +eternal +ethics +evidence +evil +evoke +evolve +exact +example +excess +exchange +excite +exclude +excuse +execute +exercise +exhaust +exhibit +exile +exist +exit +exotic +expand +expect +expire +explain +expose +express +extend +extra +eye +eyebrow +fabric +face +faculty +fade +faint +faith +fall +false +fame +family +famous +fan +fancy +fantasy +farm +fashion +fat +fatal +father +fatigue +fault +favorite +feature +february +federal +fee +feed +feel +female +fence +festival +fetch +fever +few +fiber +fiction +field +figure +file +film +filter +final +find +fine +finger +finish +fire +firm +first +fiscal +fish +fit +fitness +fix +flag +flame +flash +flat +flavor +flee +flight +flip +float +flock +floor +flower +fluid +flush +fly +foam +focus +fog +foil +fold +follow +food +foot +force +forest +forget +fork +fortune +forum +forward +fossil +foster +found +fox +fragile +frame +frequent +fresh +friend +fringe +frog +front +frost +frown +frozen +fruit +fuel +fun +funny +furnace +fury +future +gadget +gain +galaxy +gallery +game +gap +garage +garbage +garden +garlic +garment +gas +gasp +gate +gather +gauge +gaze +general +genius +genre +gentle +genuine +gesture +ghost +giant +gift +giggle +ginger +giraffe +girl +give +glad +glance +glare +glass +glide +glimpse +globe +gloom +glory +glove +glow +glue +goat +goddess +gold +good +goose +gorilla +gospel +gossip +govern +gown +grab +grace +grain +grant +grape +grass +gravity +great +green +grid +grief +grit +grocery +group +grow +grunt +guard +guess +guide +guilt +guitar +gun +gym +habit +hair +half +hammer +hamster +hand +happy +harbor +hard +harsh +harvest +hat +have +hawk +hazard +head +health +heart +heavy +hedgehog +height +hello +helmet +help +hen +hero +hidden +high +hill +hint +hip +hire +history +hobby +hockey +hold +hole +holiday +hollow +home +honey +hood +hope +horn +horror +horse +hospital +host +hotel +hour +hover +hub +huge +human +humble +humor +hundred +hungry +hunt +hurdle +hurry +hurt +husband +hybrid +ice +icon +idea +identify +idle +ignore +ill +illegal +illness +image +imitate +immense +immune +impact +impose +improve +impulse +inch +include +income +increase +index +indicate +indoor +industry +infant +inflict +inform +inhale +inherit +initial +inject +injury +inmate +inner +innocent +input +inquiry +insane +insect +inside +inspire +install +intact +interest +into +invest +invite +involve +iron +island +isolate +issue +item +ivory +jacket +jaguar +jar +jazz +jealous +jeans +jelly +jewel +job +join +joke +journey +joy +judge +juice +jump +jungle +junior +junk +just +kangaroo +keen +keep +ketchup +key +kick +kid +kidney +kind +kingdom +kiss +kit +kitchen +kite +kitten +kiwi +knee +knife +knock +know +lab +label +labor +ladder +lady +lake +lamp +language +laptop +large +later +latin +laugh +laundry +lava +law +lawn +lawsuit +layer +lazy +leader +leaf +learn +leave +lecture +left +leg +legal +legend +leisure +lemon +lend +length +lens +leopard +lesson +letter +level +liar +liberty +library +license +life +lift +light +like +limb +limit +link +lion +liquid +list +little +live +lizard +load +loan +lobster +local +lock +logic +lonely +long +loop +lottery +loud +lounge +love +loyal +lucky +luggage +lumber +lunar +lunch +luxury +lyrics +machine +mad +magic +magnet +maid +mail +main +major +make +mammal +man +manage +mandate +mango +mansion +manual +maple +marble +march +margin +marine +market +marriage +mask +mass +master +match +material +math +matrix +matter +maximum +maze +meadow +mean +measure +meat +mechanic +medal +media +melody +melt +member +memory +mention +menu +mercy +merge +merit +merry +mesh +message +metal +method +middle +midnight +milk +million +mimic +mind +minimum +minor +minute +miracle +mirror +misery +miss +mistake +mix +mixed +mixture +mobile +model +modify +mom +moment +monitor +monkey +monster +month +moon +moral +more +morning +mosquito +mother +motion +motor +mountain +mouse +move +movie +much +muffin +mule +multiply +muscle +museum +mushroom +music +must +mutual +myself +mystery +myth +naive +name +napkin +narrow +nasty +nation +nature +near +neck +need +negative +neglect +neither +nephew +nerve +nest +net +network +neutral +never +news +next +nice +night +noble +noise +nominee +noodle +normal +north +nose +notable +note +nothing +notice +novel +now +nuclear +number +nurse +nut +oak +obey +object +oblige +obscure +observe +obtain +obvious +occur +ocean +october +odor +off +offer +office +often +oil +okay +old +olive +olympic +omit +once +one +onion +online +only +open +opera +opinion +oppose +option +orange +orbit +orchard +order +ordinary +organ +orient +original +orphan +ostrich +other +outdoor +outer +output +outside +oval +oven +over +own +owner +oxygen +oyster +ozone +pact +paddle +page +pair +palace +palm +panda +panel +panic +panther +paper +parade +parent +park +parrot +party +pass +patch +path +patient +patrol +pattern +pause +pave +payment +peace +peanut +pear +peasant +pelican +pen +penalty +pencil +people +pepper +perfect +permit +person +pet +phone +photo +phrase +physical +piano +picnic +picture +piece +pig +pigeon +pill +pilot +pink +pioneer +pipe +pistol +pitch +pizza +place +planet +plastic +plate +play +please +pledge +pluck +plug +plunge +poem +poet +point +polar +pole +police +pond +pony +pool +popular +portion +position +possible +post +potato +pottery +poverty +powder +power +practice +praise +predict +prefer +prepare +present +pretty +prevent +price +pride +primary +print +priority +prison +private +prize +problem +process +produce +profit +program +project +promote +proof +property +prosper +protect +proud +provide +public +pudding +pull +pulp +pulse +pumpkin +punch +pupil +puppy +purchase +purity +purpose +purse +push +put +puzzle +pyramid +quality +quantum +quarter +question +quick +quit +quiz +quote +rabbit +raccoon +race +rack +radar +radio +rail +rain +raise +rally +ramp +ranch +random +range +rapid +rare +rate +rather +raven +raw +razor +ready +real +reason +rebel +rebuild +recall +receive +recipe +record +recycle +reduce +reflect +reform +refuse +region +regret +regular +reject +relax +release +relief +rely +remain +remember +remind +remove +render +renew +rent +reopen +repair +repeat +replace +report +require +rescue +resemble +resist +resource +response +result +retire +retreat +return +reunion +reveal +review +reward +rhythm +rib +ribbon +rice +rich +ride +ridge +rifle +right +rigid +ring +riot +ripple +risk +ritual +rival +river +road +roast +robot +robust +rocket +romance +roof +rookie +room +rose +rotate +rough +round +route +royal +rubber +rude +rug +rule +run +runway +rural +sad +saddle +sadness +safe +sail +salad +salmon +salon +salt +salute +same +sample +sand +satisfy +satoshi +sauce +sausage +save +say +scale +scan +scare +scatter +scene +scheme +school +science +scissors +scorpion +scout +scrap +screen +script +scrub +sea +search +season +seat +second +secret +section +security +seed +seek +segment +select +sell +seminar +senior +sense +sentence +series +service +session +settle +setup +seven +shadow +shaft +shallow +share +shed +shell +sheriff +shield +shift +shine +ship +shiver +shock +shoe +shoot +shop +short +shoulder +shove +shrimp +shrug +shuffle +shy +sibling +sick +side +siege +sight +sign +silent +silk +silly +silver +similar +simple +since +sing +siren +sister +situate +six +size +skate +sketch +ski +skill +skin +skirt +skull +slab +slam +sleep +slender +slice +slide +slight +slim +slogan +slot +slow +slush +small +smart +smile +smoke +smooth +snack +snake +snap +sniff +snow +soap +soccer +social +sock +soda +soft +solar +soldier +solid +solution +solve +someone +song +soon +sorry +sort +soul +sound +soup +source +south +space +spare +spatial +spawn +speak +special +speed +spell +spend +sphere +spice +spider +spike +spin +spirit +split +spoil +sponsor +spoon +sport +spot +spray +spread +spring +spy +square +squeeze +squirrel +stable +stadium +staff +stage +stairs +stamp +stand +start +state +stay +steak +steel +stem +step +stereo +stick +still +sting +stock +stomach +stone +stool +story +stove +strategy +street +strike +strong +struggle +student +stuff +stumble +style +subject +submit +subway +success +such +sudden +suffer +sugar +suggest +suit +summer +sun +sunny +sunset +super +supply +supreme +sure +surface +surge +surprise +surround +survey +suspect +sustain +swallow +swamp +swap +swarm +swear +sweet +swift +swim +swing +switch +sword +symbol +symptom +syrup +system +table +tackle +tag +tail +talent +talk +tank +tape +target +task +taste +tattoo +taxi +teach +team +tell +ten +tenant +tennis +tent +term +test +text +thank +that +theme +then +theory +there +they +thing +this +thought +three +thrive +throw +thumb +thunder +ticket +tide +tiger +tilt +timber +time +tiny +tip +tired +tissue +title +toast +tobacco +today +toddler +toe +together +toilet +token +tomato +tomorrow +tone +tongue +tonight +tool +tooth +top +topic +topple +torch +tornado +tortoise +toss +total +tourist +toward +tower +town +toy +track +trade +traffic +tragic +train +transfer +trap +trash +travel +tray +treat +tree +trend +trial +tribe +trick +trigger +trim +trip +trophy +trouble +truck +true +truly +trumpet +trust +truth +try +tube +tuition +tumble +tuna +tunnel +turkey +turn +turtle +twelve +twenty +twice +twin +twist +two +type +typical +ugly +umbrella +unable +unaware +uncle +uncover +under +undo +unfair +unfold +unhappy +uniform +unique +unit +universe +unknown +unlock +until +unusual +unveil +update +upgrade +uphold +upon +upper +upset +urban +urge +usage +use +used +useful +useless +usual +utility +vacant +vacuum +vague +valid +valley +valve +van +vanish +vapor +various +vast +vault +vehicle +velvet +vendor +venture +venue +verb +verify +version +very +vessel +veteran +viable +vibrant +vicious +victory +video +view +village +vintage +violin +virtual +virus +visa +visit +visual +vital +vivid +vocal +voice +void +volcano +volume +vote +voyage +wage +wagon +wait +walk +wall +walnut +want +warfare +warm +warrior +wash +wasp +waste +water +wave +way +wealth +weapon +wear +weasel +weather +web +wedding +weekend +weird +welcome +west +wet +whale +what +wheat +wheel +when +where +whip +whisper +wide +width +wife +wild +will +win +window +wine +wing +wink +winner +winter +wire +wisdom +wise +wish +witness +wolf +woman +wonder +wood +wool +word +work +world +worry +worth +wrap +wreck +wrestle +wrist +write +wrong +yard +year +yellow +you +young +youth +zebra +zero +zone +zoo +` diff --git a/backend/internal/config/config.go b/backend/internal/pkg/config/config.go similarity index 100% rename from backend/internal/config/config.go rename to backend/internal/pkg/config/config.go diff --git a/backend/internal/logger/logger.go b/backend/internal/pkg/logger/logger.go similarity index 100% rename from backend/internal/logger/logger.go rename to backend/internal/pkg/logger/logger.go diff --git a/backend/internal/logger/slogdiscard.go b/backend/internal/pkg/logger/slogdiscard.go similarity index 100% rename from backend/internal/logger/slogdiscard.go rename to backend/internal/pkg/logger/slogdiscard.go diff --git a/backend/internal/logger/slogpretty.go b/backend/internal/pkg/logger/slogpretty.go similarity index 100% rename from backend/internal/logger/slogpretty.go rename to backend/internal/pkg/logger/slogpretty.go diff --git a/backend/internal/pkg/models/user.go b/backend/internal/pkg/models/user.go new file mode 100644 index 0000000..4078291 --- /dev/null +++ b/backend/internal/pkg/models/user.go @@ -0,0 +1,6 @@ +package models + +type UserIdentity interface { + Id() string + Mnemonic() string +} diff --git a/backend/internal/sqlutils/tx.go b/backend/internal/pkg/sqlutils/tx.go similarity index 100% rename from backend/internal/sqlutils/tx.go rename to backend/internal/pkg/sqlutils/tx.go diff --git a/backend/internal/service/service.go b/backend/internal/service/service.go index 648295a..b8af814 100644 --- a/backend/internal/service/service.go +++ b/backend/internal/service/service.go @@ -43,6 +43,8 @@ func (s *ServiceImpl) Run(ctx context.Context) error { select { case <-ctx.Done(): + s.log.Info("shutting down service") + return nil case err := <-errch: return fmt.Errorf("error at service runtime. %w", err) diff --git a/backend/internal/usecase/interactors/jwt/jwt.go b/backend/internal/usecase/interactors/jwt/jwt.go new file mode 100644 index 0000000..05bee4e --- /dev/null +++ b/backend/internal/usecase/interactors/jwt/jwt.go @@ -0,0 +1,39 @@ +package jwt + +import ( + "fmt" + "time" + + "github.com/emochka2007/block-accounting/internal/pkg/models" + "github.com/golang-jwt/jwt/v5" +) + +type JWTInteractor interface { + NewToken(user models.UserIdentity, duration time.Duration) (string, error) +} + +type jwtInteractor struct { + secret []byte +} + +func NewWardenJWT(secret []byte) JWTInteractor { + return &jwtInteractor{secret} +} + +// NewToken creates new JWT token for given user +func (w *jwtInteractor) NewToken(user models.UserIdentity, duration time.Duration) (string, error) { + token := jwt.New(jwt.SigningMethodHS256) + + claims := token.Claims.(jwt.MapClaims) + claims["uid"] = user.Id() + claims["exp"] = time.Now().Add(duration).Unix() + + secret := w.secret + + tokenString, err := token.SignedString([]byte(secret)) + if err != nil { + return "", fmt.Errorf("error sign token. %w", err) + } + + return tokenString, nil +}