├── .gitignore ├── Dockerfile ├── README.md ├── common.go ├── config └── config.go ├── datatype └── datatype.go ├── go.mod ├── go.sum ├── handler.go ├── http.go ├── main.go ├── nonce.go ├── proof.go ├── state_init.go └── tonapi.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | .vscode/ 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | 18 | .idea -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20 AS gobuild 2 | WORKDIR /build-dir 3 | COPY go.mod . 4 | COPY go.sum . 5 | RUN go mod download all 6 | COPY . . 7 | RUN go build -o /tmp/tonproof github.com/tonkeeper/tonproof 8 | 9 | 10 | FROM ubuntu AS tonproof 11 | RUN apt-get update && \ 12 | apt-get install -y openssl ca-certificates 13 | COPY --from=gobuild /tmp/tonproof /app/tonproof 14 | CMD ["/app/tonproof"] 15 | 16 | 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # demo-dapp-backend 2 | 3 | The example of [demo-dapp](https://github.com/ton-connect/demo-dapp-with-backend) backend with authorization by ton address. 4 | 5 | Authorization process is: 6 | 1. Client fetches payload to be signed by wallet: 7 | ``` 8 | /ton-proof/generatePayload 9 | 10 | response: 11 | "E5B4ARS6CdOI2b5e1jz0jnS-x-a3DgfNXprrg_3pec0=" 12 | ``` 13 | 14 | 2. Client connects to the wallet via TonConnect 2.0 and passes `ton_proof` request with specified payload. 15 | See the [frontend SDK](https://github.com/ton-connect/sdk/tree/main/packages/sdk) for more details. 16 | 17 | 3. User approves connection and client receives signed payload with additional prefixes. 18 | 4. Client sends signed result to the backend. Backend checks correctnes of the all prefixes and signature correctness and returns auth token: 19 | ``` 20 | /ton-proof/checkProof 21 | { 22 | "address": "0:f63660ff947e5fe6ed4a8f729f1b24ef859497d0483aaa9d9ae48414297c4e1b", // user's address 23 | "network": "-239", // "-239" for mainnet and "-1" for testnet 24 | "proof": { 25 | "timestamp": 1668094767, // unix epoch seconds 26 | "domain": { 27 | "lengthBytes": 21, 28 | "value": "ton-connect.github.io" 29 | }, 30 | "signature": "28tWSg8RDB3P/iIYupySINq1o3F5xLodndzNFHOtdi16Z+MuII8LAPnHLT3E6WTB27//qY4psU5Rf5/aJaIIAA==", 31 | "payload": "E5B4ARS6CdOI2b5e1jz0jnS-x-a3DgfNXprrg_3pec0=" // payload from the step 1. 32 | } 33 | } 34 | 35 | response: 36 | { 37 | "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZGRyZXNzIjoiMDpmNjM2NjBmZjk0N2U1ZmU2ZWQ0YThmNzI5ZjFiMjRlZjg1OTQ5N2QwNDgzYWFhOWQ5YWU0ODQxNDI5N2M0ZTFiIiwiZXhwIjoxNjY4MDk4NDkwfQ.13sg3Mgt2hT9_vChan3bmQkp_Wsigj9YjSoKABTsVGA" 38 | } 39 | ``` 40 | 41 | See `ton_proof` details in the [docs](https://github.com/ton-connect/docs/blob/main/requests-responses.md#address-proof-signature-ton_proof). 42 | 43 | 5. Client can access auth-required endpoints: 44 | ``` 45 | /dapp/getAccountInfo?network=-239 46 | Bearer 47 | 48 | response: 49 | json 50 | ``` 51 | 52 | # Ton Proof JS verification 53 | You can find an example of the ton_proof verification in JavaScript [here](https://gist.github.com/TrueCarry/cac00bfae051f7028085aa018c2a05c6). 54 | -------------------------------------------------------------------------------- /common.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | type HttpRes struct { 10 | Message string `json:"message,omitempty" example:"status ok"` 11 | StatusCode int `json:"statusCode,omitempty" example:"200"` 12 | } 13 | 14 | func HttpResOk() HttpRes { 15 | return HttpRes{ 16 | Message: "OK", 17 | StatusCode: http.StatusOK, 18 | } 19 | } 20 | 21 | func HttpResError(errMsg string, statusCode int) (int, HttpRes) { 22 | return statusCode, HttpRes{ 23 | Message: errMsg, 24 | StatusCode: statusCode, 25 | } 26 | } 27 | 28 | func HttpResErrorWithLog(errMsg string, statusCode int, log *log.Entry) (int, HttpRes) { 29 | if log != nil { 30 | log.Error(errMsg) 31 | } 32 | return statusCode, HttpRes{ 33 | Message: errMsg, 34 | StatusCode: statusCode, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/caarlos0/env/v6" 7 | ) 8 | 9 | var Config = struct { 10 | Port int `env:"PORT" envDefault:"8081"` 11 | }{} 12 | 13 | var Proof = struct { 14 | PayloadSignatureKey string `env:"TONPROOF_PAYLOAD_SIGNATURE_KEY"` 15 | PayloadLifeTimeSec int64 `env:"TONPROOF_PAYLOAD_LIFETIME_SEC" envDefault:"300"` 16 | ProofLifeTimeSec int64 `env:"TONPROOF_PROOF_LIFETIME_SEC" envDefault:"300"` 17 | ExampleDomain string `env:"TONPROOF_EXAMPLE_DOMAIN" envDefault:"ton-connect.github.io"` 18 | }{} 19 | 20 | func LoadConfig() { 21 | if err := env.Parse(&Config); err != nil { 22 | log.Fatalf("config parsing failed: %v\n", err) 23 | } 24 | if err := env.Parse(&Proof); err != nil { 25 | log.Panicf("[‼️ Config parsing failed] %+v\n", err) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /datatype/datatype.go: -------------------------------------------------------------------------------- 1 | package datatype 2 | 3 | type Domain struct { 4 | LengthBytes uint32 `json:"lengthBytes"` 5 | Value string `json:"value"` 6 | } 7 | 8 | type MessageInfo struct { 9 | Timestamp int64 `json:"timestamp"` 10 | Domain Domain `json:"domain"` 11 | Signature string `json:"signature"` 12 | Payload string `json:"payload"` 13 | StateInit string `json:"state_init"` 14 | } 15 | 16 | type TonProof struct { 17 | Address string `json:"address"` 18 | Network string `json:"network"` 19 | Proof MessageInfo `json:"proof"` 20 | } 21 | 22 | type ParsedMessage struct { 23 | Workchain int32 24 | Address []byte 25 | Timstamp int64 26 | Domain Domain 27 | Signature []byte 28 | Payload string 29 | StateInit string 30 | } 31 | 32 | type Payload struct { 33 | ExpirtionTime int64 34 | Signature string 35 | } 36 | 37 | type AccountInfo struct { 38 | Address struct { 39 | Bounceable string `json:"bounceable"` 40 | NonBounceable string `json:"non_bounceable"` 41 | Raw string `json:"raw"` 42 | } `json:"address"` 43 | Balance int64 `json:"balance"` 44 | Status string `json:"status"` 45 | } 46 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tonkeeper/tonproof 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/caarlos0/env/v6 v6.10.1 7 | github.com/golang-jwt/jwt v3.2.2+incompatible 8 | github.com/labstack/echo/v4 v4.9.1 9 | github.com/sirupsen/logrus v1.9.0 10 | github.com/tonkeeper/tongo v1.0.3 11 | ) 12 | 13 | require ( 14 | github.com/labstack/gommon v0.4.0 // indirect 15 | github.com/mattn/go-colorable v0.1.12 // indirect 16 | github.com/mattn/go-isatty v0.0.14 // indirect 17 | github.com/oasisprotocol/curve25519-voi v0.0.0-20220328075252-7dd334e3daae // indirect 18 | github.com/snksoft/crc v1.1.0 // indirect 19 | github.com/valyala/bytebufferpool v1.0.0 // indirect 20 | github.com/valyala/fasttemplate v1.2.1 // indirect 21 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect 22 | golang.org/x/exp v0.0.0-20230116083435-1de6713980de // indirect 23 | golang.org/x/net v0.0.0-20220728030405-41545e8bf201 // indirect 24 | golang.org/x/sys v0.1.0 // indirect 25 | golang.org/x/text v0.3.7 // indirect 26 | golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/caarlos0/env/v6 v6.10.1 h1:t1mPSxNpei6M5yAeu1qtRdPAK29Nbcf/n3G7x+b3/II= 2 | github.com/caarlos0/env/v6 v6.10.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 7 | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 8 | github.com/labstack/echo/v4 v4.9.1 h1:GliPYSpzGKlyOhqIbG8nmHBo3i1saKWFOgh41AN3b+Y= 9 | github.com/labstack/echo/v4 v4.9.1/go.mod h1:Pop5HLc+xoc4qhTZ1ip6C0RtP7Z+4VzRLWZZFKqbbjo= 10 | github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= 11 | github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= 12 | github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 13 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= 14 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 15 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 16 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 17 | github.com/oasisprotocol/curve25519-voi v0.0.0-20220328075252-7dd334e3daae h1:7smdlrfdcZic4VfsGKD2ulWL804a4GVphr4s7WZxGiY= 18 | github.com/oasisprotocol/curve25519-voi v0.0.0-20220328075252-7dd334e3daae/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s= 19 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 20 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 21 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= 22 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 23 | github.com/snksoft/crc v1.1.0 h1:HkLdI4taFlgGGG1KvsWMpz78PkOC9TkPVpTV/cuWn48= 24 | github.com/snksoft/crc v1.1.0/go.mod h1:5/gUOsgAm7OmIhb6WJzw7w5g2zfJi4FrHYgGPdshE+A= 25 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 26 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 27 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 28 | github.com/tonkeeper/tongo v1.0.3 h1:8NqSb5nUPh2Prp8UDXH5BQk035y6vxR46PE0aZAcsCs= 29 | github.com/tonkeeper/tongo v1.0.3/go.mod h1:Nn/5t5MGQHBUuEm56O135FIYwk9WpPgDITjWPk+fPGc= 30 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 31 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 32 | github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= 33 | github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 34 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= 35 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 36 | golang.org/x/exp v0.0.0-20230116083435-1de6713980de h1:DBWn//IJw30uYCgERoxCg84hWtA97F4wMiKOIh00Uf0= 37 | golang.org/x/exp v0.0.0-20230116083435-1de6713980de/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= 38 | golang.org/x/net v0.0.0-20220728030405-41545e8bf201 h1:bvOltf3SADAfG05iRml8lAB3qjoEX5RCyN4K6G5v3N0= 39 | golang.org/x/net v0.0.0-20220728030405-41545e8bf201/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= 40 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 41 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 42 | golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 43 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 44 | golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= 45 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 46 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 47 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 48 | golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ= 49 | golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 50 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 51 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 52 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 53 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 54 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/tonkeeper/tongo" 7 | "io" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/golang-jwt/jwt" 12 | "github.com/labstack/echo/v4" 13 | log "github.com/sirupsen/logrus" 14 | "github.com/tonkeeper/tonproof/datatype" 15 | ) 16 | 17 | type jwtCustomClaims struct { 18 | Address string `json:"address"` 19 | jwt.StandardClaims 20 | } 21 | 22 | type handler struct { 23 | sharedSecret string 24 | payloadTtl time.Duration 25 | } 26 | 27 | func newHandler(sharedSecret string, payloadTtl time.Duration) *handler { 28 | h := handler{ 29 | sharedSecret: sharedSecret, 30 | payloadTtl: payloadTtl, 31 | } 32 | return &h 33 | } 34 | 35 | func (h *handler) ProofHandler(c echo.Context) error { 36 | ctx := c.Request().Context() 37 | log := log.WithContext(ctx).WithField("prefix", "ProofHandler") 38 | b, err := io.ReadAll(c.Request().Body) 39 | if err != nil { 40 | return c.JSON(HttpResErrorWithLog(err.Error(), http.StatusBadRequest, log)) 41 | } 42 | var tp datatype.TonProof 43 | err = json.Unmarshal(b, &tp) 44 | if err != nil { 45 | return c.JSON(HttpResErrorWithLog(err.Error(), http.StatusBadRequest, log)) 46 | } 47 | 48 | // check payload 49 | err = checkPayload(tp.Proof.Payload, h.sharedSecret) 50 | if err != nil { 51 | return c.JSON(HttpResErrorWithLog("payload verification failed: "+err.Error(), http.StatusBadRequest, log)) 52 | } 53 | 54 | parsed, err := ConvertTonProofMessage(ctx, &tp) 55 | if err != nil { 56 | return c.JSON(HttpResErrorWithLog(err.Error(), http.StatusBadRequest, log)) 57 | } 58 | 59 | net := networks[tp.Network] 60 | if net == nil { 61 | return c.JSON(HttpResErrorWithLog(fmt.Sprintf("undefined network: %v", tp.Network), http.StatusBadRequest, log)) 62 | } 63 | addr, err := tongo.ParseAccountID(tp.Address) 64 | if err != nil { 65 | return c.JSON(HttpResErrorWithLog(fmt.Sprintf("invalid account: %v", tp.Address), http.StatusBadRequest, log)) 66 | } 67 | check, err := CheckProof(ctx, addr, net, parsed) 68 | if err != nil { 69 | return c.JSON(HttpResErrorWithLog("proof checking error: "+err.Error(), http.StatusBadRequest, log)) 70 | } 71 | if !check { 72 | return c.JSON(HttpResErrorWithLog("proof verification failed", http.StatusBadRequest, log)) 73 | } 74 | 75 | claims := &jwtCustomClaims{ 76 | tp.Address, 77 | jwt.StandardClaims{ 78 | ExpiresAt: time.Now().AddDate(10, 0, 0).Unix(), 79 | }, 80 | } 81 | 82 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 83 | 84 | t, err := token.SignedString([]byte(h.sharedSecret)) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | return c.JSON(http.StatusOK, echo.Map{ 90 | "token": t, 91 | }) 92 | } 93 | 94 | func (h *handler) PayloadHandler(c echo.Context) error { 95 | log := log.WithContext(c.Request().Context()).WithField("prefix", "PayloadHandler") 96 | 97 | payload, err := generatePayload(h.sharedSecret, h.payloadTtl) 98 | if err != nil { 99 | c.JSON(HttpResErrorWithLog(err.Error(), http.StatusBadRequest, log)) 100 | } 101 | 102 | return c.JSON(http.StatusOK, echo.Map{ 103 | "payload": payload, 104 | }) 105 | } 106 | 107 | func (h *handler) GetAccountInfo(c echo.Context) error { 108 | ctx := c.Request().Context() 109 | log := log.WithContext(ctx).WithField("prefix", "GetAccountInfo") 110 | user := c.Get("user").(*jwt.Token) 111 | claims := user.Claims.(*jwtCustomClaims) 112 | addr, err := tongo.ParseAccountID(claims.Address) 113 | if err != nil { 114 | return c.JSON(HttpResErrorWithLog(fmt.Sprintf("can't parse acccount: %v", claims.Address), http.StatusBadRequest, log)) 115 | } 116 | 117 | net := networks[c.QueryParam("network")] 118 | if net == nil { 119 | return c.JSON(HttpResErrorWithLog(fmt.Sprintf("undefined network: %v", c.QueryParam("network")), http.StatusBadRequest, log)) 120 | } 121 | 122 | address, err := GetAccountInfo(c.Request().Context(), addr, net) 123 | if err != nil { 124 | return c.JSON(HttpResErrorWithLog(fmt.Sprintf("get account info error: %v", err), http.StatusBadRequest, log)) 125 | 126 | } 127 | return c.JSON(http.StatusOK, address) 128 | } 129 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/labstack/echo/v4" 5 | "github.com/labstack/echo/v4/middleware" 6 | "github.com/tonkeeper/tonproof/config" 7 | ) 8 | 9 | func registerHandlers(e *echo.Echo, h *handler) { 10 | proof := e.Group("/ton-proof") 11 | proof.POST("/generatePayload", h.PayloadHandler, middleware.CORSWithConfig(middleware.CORSConfig{ 12 | AllowOrigins: []string{"*"}, 13 | AllowMethods: []string{echo.POST}, 14 | })) 15 | proof.POST("/checkProof", h.ProofHandler, middleware.CORSWithConfig(middleware.CORSConfig{ 16 | AllowOrigins: []string{"*"}, 17 | AllowMethods: []string{echo.POST}, 18 | })) 19 | dapp := e.Group("/dapp") 20 | dapp.Use(middleware.CORS()) 21 | dapp.GET("/getAccountInfo", h.GetAccountInfo, middleware.CORSWithConfig(middleware.CORSConfig{ 22 | AllowOrigins: []string{"*"}, 23 | AllowMethods: []string{echo.GET}, 24 | }), middleware.JWTWithConfig(middleware.JWTConfig{ 25 | Claims: &jwtCustomClaims{}, 26 | SigningKey: []byte(config.Proof.PayloadSignatureKey), 27 | })) 28 | 29 | } 30 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/tonkeeper/tongo/liteapi" 6 | "time" 7 | 8 | "github.com/tonkeeper/tonproof/config" 9 | 10 | _ "net/http/pprof" 11 | 12 | "github.com/labstack/echo/v4" 13 | "github.com/labstack/echo/v4/middleware" 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | func main() { 18 | log.Info("Tonproof is running") 19 | config.LoadConfig() 20 | 21 | e := echo.New() 22 | e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{ 23 | Skipper: nil, 24 | DisableStackAll: true, 25 | DisablePrintStack: false, 26 | })) 27 | e.Use(middleware.Logger()) 28 | var err error 29 | networks["-239"], err = liteapi.NewClientWithDefaultMainnet() 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | networks["-3"], err = liteapi.NewClientWithDefaultTestnet() 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | h := newHandler(config.Proof.PayloadSignatureKey, time.Duration(config.Proof.ProofLifeTimeSec)*time.Second) 39 | 40 | registerHandlers(e, h) 41 | 42 | log.Fatal(e.Start(fmt.Sprintf(":%v", config.Config.Port))) 43 | } 44 | -------------------------------------------------------------------------------- /nonce.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/rand" 6 | "crypto/sha256" 7 | "crypto/subtle" 8 | "encoding/binary" 9 | "encoding/hex" 10 | "fmt" 11 | "time" 12 | ) 13 | 14 | func generatePayload(secret string, ttl time.Duration) (string, error) { 15 | payload := make([]byte, 16, 48) 16 | _, err := rand.Read(payload[:8]) 17 | if err != nil { 18 | return "", fmt.Errorf("could not generate nonce") 19 | } 20 | binary.BigEndian.PutUint64(payload[8:16], uint64(time.Now().Add(ttl).Unix())) 21 | h := hmac.New(sha256.New, []byte(secret)) 22 | h.Write(payload) 23 | payload = h.Sum(payload) 24 | return hex.EncodeToString(payload[:32]), nil 25 | } 26 | 27 | func checkPayload(payload, secret string) error { 28 | b, err := hex.DecodeString(payload) 29 | if err != nil { 30 | return err 31 | } 32 | if len(b) != 32 { 33 | return fmt.Errorf("invalid payload length") 34 | } 35 | h := hmac.New(sha256.New, []byte(secret)) 36 | h.Write(b[:16]) 37 | sign := h.Sum(nil) 38 | if subtle.ConstantTimeCompare(b[16:], sign[:16]) != 1 { 39 | return fmt.Errorf("invalid payload signature") 40 | } 41 | if time.Since(time.Unix(int64(binary.BigEndian.Uint64(b[8:16])), 0)) > 0 { 42 | return fmt.Errorf("payload expired") 43 | } 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /proof.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/ed25519" 6 | "crypto/sha256" 7 | "fmt" 8 | "github.com/tonkeeper/tongo" 9 | "github.com/tonkeeper/tongo/liteapi" 10 | "time" 11 | 12 | "encoding/base64" 13 | "encoding/binary" 14 | "encoding/hex" 15 | 16 | log "github.com/sirupsen/logrus" 17 | "github.com/tonkeeper/tonproof/config" 18 | "github.com/tonkeeper/tonproof/datatype" 19 | ) 20 | 21 | const ( 22 | tonProofPrefix = "ton-proof-item-v2/" 23 | tonConnectPrefix = "ton-connect" 24 | ) 25 | 26 | func SignatureVerify(pubkey ed25519.PublicKey, message, signature []byte) bool { 27 | return ed25519.Verify(pubkey, message, signature) 28 | } 29 | 30 | func ConvertTonProofMessage(ctx context.Context, tp *datatype.TonProof) (*datatype.ParsedMessage, error) { 31 | log := log.WithContext(ctx).WithField("prefix", "ConverTonProofMessage") 32 | 33 | addr, err := tongo.ParseAccountID(tp.Address) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | var parsedMessage datatype.ParsedMessage 39 | 40 | sig, err := base64.StdEncoding.DecodeString(tp.Proof.Signature) 41 | if err != nil { 42 | log.Error(err) 43 | return nil, err 44 | } 45 | 46 | parsedMessage.Workchain = addr.Workchain 47 | parsedMessage.Address = addr.Address[:] 48 | parsedMessage.Domain = tp.Proof.Domain 49 | parsedMessage.Timstamp = tp.Proof.Timestamp 50 | parsedMessage.Signature = sig 51 | parsedMessage.Payload = tp.Proof.Payload 52 | parsedMessage.StateInit = tp.Proof.StateInit 53 | return &parsedMessage, nil 54 | } 55 | 56 | func CreateMessage(ctx context.Context, message *datatype.ParsedMessage) ([]byte, error) { 57 | wc := make([]byte, 4) 58 | binary.BigEndian.PutUint32(wc, uint32(message.Workchain)) 59 | 60 | ts := make([]byte, 8) 61 | binary.LittleEndian.PutUint64(ts, uint64(message.Timstamp)) 62 | 63 | dl := make([]byte, 4) 64 | binary.LittleEndian.PutUint32(dl, message.Domain.LengthBytes) 65 | m := []byte(tonProofPrefix) 66 | m = append(m, wc...) 67 | m = append(m, message.Address...) 68 | m = append(m, dl...) 69 | m = append(m, []byte(message.Domain.Value)...) 70 | m = append(m, ts...) 71 | m = append(m, []byte(message.Payload)...) 72 | log.Info(string(m)) 73 | messageHash := sha256.Sum256(m) 74 | fullMes := []byte{0xff, 0xff} 75 | fullMes = append(fullMes, []byte(tonConnectPrefix)...) 76 | fullMes = append(fullMes, messageHash[:]...) 77 | res := sha256.Sum256(fullMes) 78 | log.Info(hex.EncodeToString(res[:])) 79 | return res[:], nil 80 | } 81 | 82 | func CheckProof(ctx context.Context, address tongo.AccountID, net *liteapi.Client, tonProofReq *datatype.ParsedMessage) (bool, error) { 83 | log := log.WithContext(ctx).WithField("prefix", "CheckProof") 84 | pubKey, err := GetWalletPubKey(ctx, address, net) 85 | if err != nil { 86 | if tonProofReq.StateInit == "" { 87 | log.Errorf("get wallet address error: %v", err) 88 | return false, err 89 | } 90 | if ok, err := CompareStateInitWithAddress(address, tonProofReq.StateInit); err != nil || !ok { 91 | return ok, err 92 | } 93 | pubKey, err = ParseStateInit(tonProofReq.StateInit) 94 | if err != nil { 95 | log.Errorf("parse wallet state init error: %v", err) 96 | return false, err 97 | } 98 | } 99 | 100 | if time.Now().After(time.Unix(tonProofReq.Timstamp, 0).Add(time.Duration(config.Proof.ProofLifeTimeSec) * time.Second)) { 101 | msgErr := "proof has been expired" 102 | log.Error(msgErr) 103 | return false, fmt.Errorf(msgErr) 104 | } 105 | 106 | if tonProofReq.Domain.Value != config.Proof.ExampleDomain { 107 | msgErr := fmt.Sprintf("wrong domain: %v", tonProofReq.Domain) 108 | log.Error(msgErr) 109 | return false, fmt.Errorf(msgErr) 110 | } 111 | 112 | mes, err := CreateMessage(ctx, tonProofReq) 113 | if err != nil { 114 | log.Errorf("create message error: %v", err) 115 | return false, err 116 | } 117 | 118 | return SignatureVerify(pubKey, mes, tonProofReq.Signature), nil 119 | } 120 | -------------------------------------------------------------------------------- /state_init.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "fmt" 7 | 8 | "github.com/tonkeeper/tongo" 9 | "github.com/tonkeeper/tongo/boc" 10 | "github.com/tonkeeper/tongo/tlb" 11 | "github.com/tonkeeper/tongo/wallet" 12 | ) 13 | 14 | var knownHashes = make(map[string]wallet.Version) 15 | 16 | func init() { 17 | for i := wallet.Version(0); i <= wallet.V4R2; i++ { 18 | ver := wallet.GetCodeHashByVer(i) 19 | knownHashes[hex.EncodeToString(ver[:])] = i 20 | } 21 | } 22 | 23 | func ParseStateInit(stateInit string) ([]byte, error) { 24 | cells, err := boc.DeserializeBocBase64(stateInit) 25 | if err != nil || len(cells) != 1 { 26 | return nil, err 27 | } 28 | var state tlb.StateInit 29 | err = tlb.Unmarshal(cells[0], &state) 30 | if err != nil { 31 | return nil, err 32 | } 33 | if !state.Data.Exists || !state.Code.Exists { 34 | return nil, fmt.Errorf("empty init state") 35 | } 36 | codeHash, err := state.Code.Value.Value.HashString() 37 | if err != nil { 38 | return nil, err 39 | } 40 | version, prs := knownHashes[codeHash] 41 | if !prs { 42 | return nil, fmt.Errorf("unknown code hash") 43 | } 44 | var pubKey tlb.Bits256 45 | switch version { 46 | case wallet.V1R1, wallet.V1R2, wallet.V1R3, wallet.V2R1, wallet.V2R2: 47 | var data wallet.DataV1V2 48 | err = tlb.Unmarshal(&state.Data.Value.Value, &data) 49 | if err != nil { 50 | return nil, err 51 | } 52 | pubKey = data.PublicKey 53 | case wallet.V3R1, wallet.V3R2, wallet.V4R1, wallet.V4R2: 54 | var data wallet.DataV3 55 | err = tlb.Unmarshal(&state.Data.Value.Value, &data) 56 | if err != nil { 57 | return nil, err 58 | } 59 | pubKey = data.PublicKey 60 | } 61 | 62 | return pubKey[:], nil 63 | } 64 | 65 | func CompareStateInitWithAddress(a tongo.AccountID, stateInit string) (bool, error) { 66 | cells, err := boc.DeserializeBocBase64(stateInit) 67 | if err != nil || len(cells) != 1 { 68 | return false, err 69 | } 70 | h, err := cells[0].Hash() 71 | if err != nil { 72 | return false, err 73 | } 74 | return bytes.Equal(h, a.Address[:]), nil 75 | } 76 | -------------------------------------------------------------------------------- /tonapi.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/ed25519" 6 | "fmt" 7 | "github.com/tonkeeper/tongo" 8 | "github.com/tonkeeper/tongo/abi" 9 | "github.com/tonkeeper/tongo/liteapi" 10 | "github.com/tonkeeper/tonproof/datatype" 11 | "math/big" 12 | ) 13 | 14 | const ( 15 | GetWalletPath = "/v1/wallet/getWalletPublicKey" 16 | GetAccountInfoPath = "/v1/account/getInfo" 17 | ) 18 | 19 | var networks = map[string]*liteapi.Client{} 20 | 21 | func GetAccountInfo(ctx context.Context, address tongo.AccountID, net *liteapi.Client) (*datatype.AccountInfo, error) { 22 | account, err := net.GetAccountState(ctx, address) 23 | if err != nil { 24 | return nil, err 25 | } 26 | accountInfo := datatype.AccountInfo{ 27 | Balance: int64(account.Account.Account.Storage.Balance.Grams), 28 | Status: string(account.Account.Status()), 29 | } 30 | accountInfo.Address.Raw = address.ToRaw() 31 | accountInfo.Address.Bounceable = address.ToHuman(true, false) 32 | accountInfo.Address.NonBounceable = address.ToHuman(false, false) 33 | 34 | return &accountInfo, nil 35 | } 36 | 37 | func GetWalletPubKey(ctx context.Context, address tongo.AccountID, net *liteapi.Client) (ed25519.PublicKey, error) { 38 | _, result, err := abi.GetPublicKey(ctx, net, address) 39 | if err != nil { 40 | return nil, err 41 | } 42 | if r, ok := result.(abi.GetPublicKeyResult); ok { 43 | i := big.Int(r.PublicKey) 44 | b := i.Bytes() 45 | if len(b) < 24 || len(b) > 32 { //govno kakoe-to 46 | return nil, fmt.Errorf("invalid publock key") 47 | } 48 | return append(make([]byte, 32-len(b)), b...), nil //make padding if first bytes are empty 49 | } 50 | return nil, fmt.Errorf("can't get publick key") 51 | } 52 | --------------------------------------------------------------------------------