diff --git a/app.go b/app.go index 9fe43cd..3540415 100644 --- a/app.go +++ b/app.go @@ -120,7 +120,7 @@ func (a *App) GetOrder(id string) (*Result, error) { // UpdateOrder update order's note. func (a *App) UpdateOrder(id string, note string) (*Result, error) { - return a.session.put("/api/v1/app/order/" + id, map[string]interface{}{ + return a.session.put("/api/v1/app/order/"+id, map[string]interface{}{ "note": note, }) } @@ -151,8 +151,8 @@ func (a *App) Transfer(to, coinType, value string) (*Result, error) { } return a.session.post("/api/v1/app/"+coinType+"/transfer", map[string]interface{}{ - "to": to, - "value": value, + "to": to, + "value": value, "note": "", "message": "", }) @@ -344,6 +344,10 @@ func (a *App) getSecret() string { return a.Secret } +func (a *App) getPubKey() string { + return "" +} + func (a *App) getAddr() string { return a.Addr } diff --git a/business.go b/business.go new file mode 100644 index 0000000..d1850f2 --- /dev/null +++ b/business.go @@ -0,0 +1,183 @@ +package jadepoolsaas + +import ( + "encoding/base64" + "os" +) + +// NewBusinessWithAddr creates a new business instance with server addr, business key, the private key pem file of your service and the public key pem file of xpert. +func NewBusinessWithAddr(addr, businessKey, pemFilePath, pubPemFilePath string) (*Business, error) { + data, err := os.ReadFile(pemFilePath) + if err != nil { + return nil, err + } + + var pubData []byte + if len(pubPemFilePath) > 0 { + pubData, err = os.ReadFile(pubPemFilePath) + if err != nil { + return nil, err + } + } + + a := &Business{ + Addr: addr, + Key: businessKey, + Secret: base64.StdEncoding.EncodeToString(data), + } + if len(pubData) > 0 { + a.PubKey = base64.StdEncoding.EncodeToString(pubData) + } + a.session = &session{client: a} + return a, nil +} + +// ClientGet fetch client info. +func (b *Business) ClientGet(userID uint) (*BusinessResult, error) { + return b.session.businessGetWithParams("/api/v1/business/client", map[string]interface{}{ + "userID": userID, + }) +} + +// ClientsGet fetch clients. +func (b *Business) ClientsGet(page, amount uint) (*BusinessResult, error) { + return b.session.businessGetWithParams("/api/v1/business/clients", map[string]interface{}{ + "page": page, + "amount": amount, + }) +} + +// ClientCardsGet fetch client's cards. +func (b *Business) ClientCardsGet(userID uint) (*BusinessResult, error) { + return b.session.businessGetWithParams("/api/v1/business/client/cards", map[string]interface{}{ + "userID": userID, + }) +} + +// AssetsGet fetch all assets in the wallet. +func (b *Business) AssetsGet() (*BusinessResult, error) { + return b.session.businessGet("/api/v1/business/assets") +} + +// WalletBalancesGet fetch the asset balance in the wallet. +func (b *Business) WalletBalancesGet(userID, assetID uint) (*BusinessResult, error) { + return b.session.businessGetWithParams("/api/v1/business/wallet/balances", map[string]interface{}{ + "userID": userID, + "assetID": assetID, + }) +} + +// BalanceSettle settle the balance. +func (b *Business) BalanceSettle(userID, assetID uint, mType, sequence, amount string) (*BusinessResult, error) { + return b.session.businessPost("/api/v1/business/balance/settle", map[string]interface{}{ + "userID": userID, + "assetID": assetID, + "type": mType, + "sequence": sequence, + "amount": amount, + }) +} + +// BalanceLock lock the balance. +func (b *Business) BalanceLock(userID, assetID uint, sequence, amount string) (*BusinessResult, error) { + return b.session.businessPut("/api/v1/business/balance/lock", map[string]interface{}{ + "userID": userID, + "assetID": assetID, + "sequence": sequence, + "amount": amount, + }) +} + +// BalanceUnlock unlock the balance. +func (b *Business) BalanceUnlock(userID, assetID uint, sequence, amount string) (*BusinessResult, error) { + return b.session.businessPut("/api/v1/business/balance/unlock", map[string]interface{}{ + "userID": userID, + "assetID": assetID, + "sequence": sequence, + "amount": amount, + }) +} + +// Transfer transfer. +func (b *Business) Transfer(from, to, assetID uint, sequence, amount, note string) (*BusinessResult, error) { + return b.session.businessPost("/api/v1/business/transfer", map[string]interface{}{ + "from": from, + "to": to, + "sequence": sequence, + "assetID": assetID, + "amount": amount, + "note": note, + }) +} + +// Swap swap. +func (b *Business) Swap(from, fromAssetID, officialAssetID uint, sequence, fromAmount, officialAmount, note string) (*BusinessResult, error) { + return b.session.businessPost("/api/v1/business/swap", map[string]interface{}{ + "from": from, + "fromAssetID": fromAssetID, + "officialAssetID": officialAssetID, + "sequence": sequence, + "fromAmount": fromAmount, + "officialAmount": officialAmount, + "note": note, + }) +} + +// Batch batch. +func (b *Business) Batch(cmd []*BatchCommand) (*BusinessResult, error) { + return b.session.businessPost("/api/v1/business/batch", map[string]interface{}{ + "cmd": cmd, + }) +} + +// BatchCommand ... +type BatchCommand struct { + Name string `json:"name"` + Args interface{} `json:"args"` +} + +// OrderGetBySequence fetch the order by the sequence. +func (b *Business) OrderGetBySequence(sequence string) (*BusinessResult, error) { + return b.session.businessGet("/api/v1/business/order/sequence/" + sequence) +} + +// TransactionsGet fetch transactions. +func (b *Business) TransactionsGet(userID uint, mType, status string, page, amount uint) (*BusinessResult, error) { + return b.session.businessGetWithParams("/api/v1/business/transactions", map[string]interface{}{ + "userID": userID, + "type": mType, + "status": status, + "page": page, + "amount": amount, + }) +} + +// Business represents a business instance. +type Business struct { + Addr string + Key string + Secret string + PubKey string + + session *session +} + +func (b *Business) getKey() string { + return b.Key +} + +func (b *Business) getKeyHeaderName() string { + return "X-BUSINESS-KEY" +} + +func (b *Business) getSecret() string { + return b.Secret +} + +func (b *Business) getPubKey() string { + return b.PubKey +} + +func (b *Business) getAddr() string { + return b.Addr +} diff --git a/cmd/business/main.go b/cmd/business/main.go new file mode 100644 index 0000000..ff8251b --- /dev/null +++ b/cmd/business/main.go @@ -0,0 +1,287 @@ +package main + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/base64" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "io/ioutil" + "net/http" + "os" + "time" + + "github.com/btcsuite/btcd/btcec" + "github.com/labstack/echo/v4" +) + +// ParseEcdsaPubKeyFromPem ... +func ParseEcdsaPubKeyFromPem(pemContent []byte) (*btcec.PublicKey, error) { + block, _ := pem.Decode(pemContent) + if block == nil { + return nil, errors.New("invalid pem") + } + + var ecp ecPublicKey + _, err := asn1.Unmarshal(block.Bytes, &ecp) + if err != nil { + return nil, err + } + + return btcec.ParsePubKey(ecp.PublicKey.RightAlign(), btcec.S256()) +} + +// ParseEcdsaPrivateKeyFromPem ... +func ParseEcdsaPrivateKeyFromPem(pemContent []byte) (*btcec.PrivateKey, error) { + block, _ := pem.Decode(pemContent) + if block == nil { + return nil, errors.New("invalid pem") + } + + var ecp ecPrivateKey + _, err := asn1.Unmarshal(block.Bytes, &ecp) + if err != nil { + return nil, err + } + + priKey, _ := btcec.PrivKeyFromBytes(btcec.S256(), ecp.PrivateKey) + return priKey, nil +} + +//This type provides compatibility with the btcec package +type ecPrivateKey struct { + Version int + PrivateKey []byte + NamedCurveOID asn1.ObjectIdentifier `asn1:"optional,explicit,tag:0"` + PublicKey asn1.BitString `asn1:"optional,explicit,tag:1"` +} + +//This type provides compatibility with the btcec package +type ecPublicKey struct { + Raw asn1.RawContent + Algorithm pkix.AlgorithmIdentifier + PublicKey asn1.BitString +} + +func genShareKey() string { + apiGatewayPubKey := string(`LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZZd0VBWUhLb1pJemowQ0FRWUZLNEVFQUFvRFFnQUVyb0QxMG53SzJkcElqYSszb1pncGNtbk40MWFCc0FFSQpsSE9MMEpublJad3pRa3k5cmFDSW5iSk9YcGtzcDBFUVZqdDVkdkJUMEw3b2pXQXFVSlk3b1E9PQotLS0tLUVORCBQVUJMSUMgS0VZLS0tLS0K`) + myPriKey := string(`LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IUUNBUUVFSUNrOGw2UFJ6QTRhWjVVZkl1aXViaGplUDJBSTEvVENJajE2OUwzWU9xekFvQWNHQlN1QkJBQUsKb1VRRFFnQUVvVVpwRXNCRFpWTC9TQ29DanlreXAwdXRNMDc2b29YNUU2eEtzQW5TZlpOMFEwM3VlbGVVL09aeAovMmxsUXFBdU9aVlFLNE9aSGdFODh1c3RWVkY3YWc9PQotLS0tLUVORCBFQyBQUklWQVRFIEtFWS0tLS0tCg==`) + + apiGatewayPubKeyPem, err := base64.StdEncoding.DecodeString(apiGatewayPubKey) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + pubKey, err := ParseEcdsaPubKeyFromPem(apiGatewayPubKeyPem) + if err != nil { + fmt.Println(err) + os.Exit(2) + } + priKeyPem, err := base64.StdEncoding.DecodeString(myPriKey) + if err != nil { + fmt.Println(err) + os.Exit(3) + } + priKey, err := ParseEcdsaPrivateKeyFromPem(priKeyPem) + if err != nil { + fmt.Println(err) + os.Exit(4) + } + + aesKey := btcec.GenerateSharedSecret(priKey, pubKey) + fmt.Println(string(aesKey)) + + return base64.StdEncoding.EncodeToString(aesKey) +} + +type encryptedReqBody struct { + Encrypted string `json:"encrypted"` +} + +type encryptedResBody struct { + Encrypted string `json:"encrypted"` + Iv string `json:"iv"` +} + +func GenerateRandomBytes(n int) ([]byte, error) { + b := make([]byte, n) + _, err := rand.Read(b) + + if err != nil { + return nil, err + } + + return b, nil +} + +func encryptResBody(msg, aesKey string) ([]byte, []byte, error) { + iv, err := GenerateRandomBytes(16) + if err != nil { + return nil, nil, errors.New("iv generaton error") + } + + mKey, err := base64.StdEncoding.DecodeString(aesKey) + if err != nil { + return nil, nil, errors.New("invalid aes key") + } + + encrypted, err := AESEncryptStr(msg, mKey, iv) + if err != nil { + return nil, nil, err + } + return []byte(encrypted), iv, nil +} + +func decryptReqBody(encrypted *encryptedReqBody, aesKey, iv string) ([]byte, error) { + aesIV, err := base64.StdEncoding.DecodeString(iv) + if err != nil || len(aesIV) != 16 { + return nil, errors.New("invalid aes iv") + } + + mKey, err := base64.StdEncoding.DecodeString(aesKey) + if err != nil { + return nil, errors.New("invalid aes key") + } + + decrypted, err := AESDecryptStr(encrypted.Encrypted, mKey, aesIV) + if err != nil { + return nil, err + } + return []byte(decrypted), nil +} + +// AESEncryptStr base64加密字符串 +func AESEncryptStr(src string, key, iv []byte) (encmess string, err error) { + ciphertext, err := AESEncrypt([]byte(src), key, iv) + if err != nil { + return + } + + encmess = base64.StdEncoding.EncodeToString(ciphertext) + return +} + +// AESEncrypt 加密 +func AESEncrypt(src []byte, key []byte, iv []byte) ([]byte, error) { + if len(iv) == 0 { + iv = key[:16] + } + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + src = padding(src, block.BlockSize()) + blockMode := cipher.NewCBCEncrypter(block, iv) + blockMode.CryptBlocks(src, src) + return src, nil +} + +// 填充数据 +func padding(src []byte, blockSize int) []byte { + padNum := blockSize - len(src)%blockSize + pad := bytes.Repeat([]byte{byte(padNum)}, padNum) + return append(src, pad...) +} + +// 去掉填充数据 +func unpadding(src []byte) []byte { + n := len(src) + unPadNum := int(src[n-1]) + return src[:n-unPadNum] +} + +func AESDecryptStr(src string, key, iv []byte) (string, error) { + bsrc, err := base64.StdEncoding.DecodeString(src) + bret, err := AESDecrypt(bsrc, key, iv) + if err != nil { + return "", err + } + return string(bret), nil +} + +// AESDecrypt 解密 +func AESDecrypt(src []byte, key []byte, iv []byte) ([]byte, error) { + if len(iv) == 0 { + iv = key[:16] + } + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + blockMode := cipher.NewCBCDecrypter(block, iv) + blockMode.CryptBlocks(src, src) + src = unpadding(src) + return src, nil +} + +func main() { + aesKey := genShareKey() + + e := echo.New() + e.GET("/ping", func(c echo.Context) error { + return c.String(http.StatusOK, "Hello, World!") + }) + e.POST("/pong", func(c echo.Context) error { + requestBody, _ := ioutil.ReadAll(c.Request().Body) + fmt.Println(string(requestBody)) + + for key, value := range c.Request().Header { + fmt.Print(key) + + for _, v := range value { + fmt.Printf("\t%v", v) + } + + fmt.Println() + } + + iv := c.Request().Header.Get("X-Encrypt-Iv") + userId := c.Request().Header.Get("X-User-Id") + var encrypted encryptedReqBody + err := json.Unmarshal(requestBody, &encrypted) + if err != nil { + fmt.Printf("err: %v\n", err) + } + + if err == nil && len(encrypted.Encrypted) > 0 { + fmt.Printf("request msg: %v\n", encrypted.Encrypted) + // fmt.Printf("%v\n", aesKey) + fmt.Printf("request iv: %v\n", iv) + + request_plain_msg, err := decryptReqBody(&encrypted, aesKey, iv) + if err != nil { + fmt.Printf("err: %v\n", err) + } + + fmt.Printf("request decrypted msg: %v\n", string(request_plain_msg)) + } + + timeStr := time.Now().Format("2006-01-02 15:04:05") + response_plain_msg := fmt.Sprintf(`{"kkk":"hahaha","userID":"%v","ts":%v}`, userId, timeStr) + msg, ivNew, err2 := encryptResBody(response_plain_msg, aesKey) + if err2 != nil { + fmt.Printf("%v\n", err2) + } + + ivBase64 := base64.StdEncoding.EncodeToString(ivNew) + + fmt.Printf("response msg: %v\n", string(msg)) + fmt.Printf("response iv: %v\n", ivBase64) + + u := &encryptedResBody{ + Encrypted: string(msg), + Iv: ivBase64, + } + + c.Response().Header().Set("X-Encrypted", "true") + + return c.JSON(http.StatusOK, u) + }) + e.Logger.Fatal(e.Start(":8080")) +} diff --git a/cmd/ctl/main.go b/cmd/ctl/main.go index 06edd22..91aa0e9 100644 --- a/cmd/ctl/main.go +++ b/cmd/ctl/main.go @@ -17,6 +17,7 @@ func runCommand(arguments docopt.Opts) (*sdk.Result, error) { action, _ := arguments.String("") params := arguments[""].([]string) addr, _ := arguments.String("--address") + pubKey, _ := arguments.String("--pubkey") switch action { case "CreateAddress": @@ -364,6 +365,242 @@ func runCommand(arguments docopt.Opts) (*sdk.Result, error) { } return getKYC(addr, key, secret).ApplicationSubmit(params[0]) + case "BusinessAssetsGet": + result, err := getBusiness(addr, key, secret, pubKey).AssetsGet() + if err != nil { + return nil, err + } + return result.ToResult(), nil + case "BusinessClientGet": + if len(params) != 1 { + return nil, errors.New("invalid params") + } + + uid, err := strconv.ParseUint(params[0], 10, 64) + if err != nil { + return nil, errors.New("invalid params") + } + + result, err := getBusiness(addr, key, secret, pubKey).ClientGet(uint(uid)) + if err != nil { + return nil, err + } + return result.ToResult(), nil + case "BusinessClientsGet": + if len(params) != 2 { + return nil, errors.New("invalid params") + } + + page, err := strconv.ParseUint(params[0], 10, 64) + if err != nil { + return nil, errors.New("invalid params") + } + + amount, err := strconv.ParseUint(params[1], 10, 64) + if err != nil { + return nil, errors.New("invalid params") + } + + result, err := getBusiness(addr, key, secret, pubKey).ClientsGet(uint(page), uint(amount)) + if err != nil { + return nil, err + } + return result.ToResult(), nil + case "BusinessClientCardsGet": + if len(params) != 1 { + return nil, errors.New("invalid params") + } + + uid, err := strconv.ParseUint(params[0], 10, 64) + if err != nil { + return nil, errors.New("invalid params") + } + + result, err := getBusiness(addr, key, secret, pubKey).ClientCardsGet(uint(uid)) + if err != nil { + return nil, err + } + return result.ToResult(), nil + case "BusinessWalletBalancesGet": + if len(params) != 2 { + return nil, errors.New("invalid params") + } + + uid, err := strconv.ParseUint(params[0], 10, 64) + if err != nil { + return nil, errors.New("invalid params") + } + + aid, err := strconv.ParseUint(params[1], 10, 64) + if err != nil { + return nil, errors.New("invalid params") + } + + result, err := getBusiness(addr, key, secret, pubKey).WalletBalancesGet(uint(uid), uint(aid)) + if err != nil { + return nil, err + } + return result.ToResult(), nil + case "BusinessBalanceSettle": + if len(params) != 5 { + return nil, errors.New("invalid params") + } + + uid, err := strconv.ParseUint(params[1], 10, 64) + if err != nil { + return nil, errors.New("invalid params") + } + + aid, err := strconv.ParseUint(params[2], 10, 64) + if err != nil { + return nil, errors.New("invalid params") + } + + result, err := getBusiness(addr, key, secret, pubKey).BalanceSettle(uint(uid), uint(aid), params[0], params[3], params[4]) + if err != nil { + return nil, err + } + return result.ToResult(), nil + case "BusinessBalanceLock": + if len(params) != 4 { + return nil, errors.New("invalid params") + } + + uid, err := strconv.ParseUint(params[1], 10, 64) + if err != nil { + return nil, errors.New("invalid params") + } + + aid, err := strconv.ParseUint(params[2], 10, 64) + if err != nil { + return nil, errors.New("invalid params") + } + + result, err := getBusiness(addr, key, secret, pubKey).BalanceLock(uint(uid), uint(aid), params[0], params[3]) + if err != nil { + return nil, err + } + return result.ToResult(), nil + case "BusinessBalanceUnlock": + if len(params) != 4 { + return nil, errors.New("invalid params") + } + + uid, err := strconv.ParseUint(params[1], 10, 64) + if err != nil { + return nil, errors.New("invalid params") + } + + aid, err := strconv.ParseUint(params[2], 10, 64) + if err != nil { + return nil, errors.New("invalid params") + } + + result, err := getBusiness(addr, key, secret, pubKey).BalanceUnlock(uint(uid), uint(aid), params[0], params[3]) + if err != nil { + return nil, err + } + return result.ToResult(), nil + case "BusinessTransfer": + if len(params) != 5 { + return nil, errors.New("invalid params") + } + + aid, err := strconv.ParseUint(params[1], 10, 64) + if err != nil { + return nil, errors.New("invalid params") + } + + fid, err := strconv.ParseUint(params[3], 10, 64) + if err != nil { + return nil, errors.New("invalid params") + } + + tid, err := strconv.ParseUint(params[4], 10, 64) + if err != nil { + return nil, errors.New("invalid params") + } + + result, err := getBusiness(addr, key, secret, pubKey).Transfer(uint(fid), uint(tid), uint(aid), params[0], params[2], "") + if err != nil { + return nil, err + } + return result.ToResult(), nil + case "BusinessSwap": + if len(params) != 6 { + return nil, errors.New("invalid params") + } + + uid, err := strconv.ParseUint(params[1], 10, 64) + if err != nil { + return nil, errors.New("invalid params") + } + + aid, err := strconv.ParseUint(params[2], 10, 64) + if err != nil { + return nil, errors.New("invalid params") + } + + oaid, err := strconv.ParseUint(params[4], 10, 64) + if err != nil { + return nil, errors.New("invalid params") + } + + result, err := getBusiness(addr, key, secret, pubKey).Swap(uint(uid), uint(aid), uint(oaid), params[0], params[3], params[5], "") + if err != nil { + return nil, err + } + return result.ToResult(), nil + case "BusinessBatch": + if len(params) != 1 { + return nil, errors.New("invalid params") + } + + cmd := []*sdk.BatchCommand{} + err := json.NewDecoder(strings.NewReader(params[0])).Decode(&cmd) + if err != nil { + return nil, err + } + result, err := getBusiness(addr, key, secret, pubKey).Batch(cmd) + if err != nil { + return nil, err + } + return result.ToResult(), nil + case "BusinessOrderGet": + if len(params) != 1 { + return nil, errors.New("invalid params") + } + + result, err := getBusiness(addr, key, secret, pubKey).OrderGetBySequence(params[0]) + if err != nil { + return nil, err + } + return result.ToResult(), nil + case "BusinessTransactionsGet": + if len(params) != 5 { + return nil, errors.New("invalid params") + } + + uid, err := strconv.ParseUint(params[0], 10, 64) + if err != nil { + return nil, errors.New("invalid params") + } + + page, err := strconv.ParseUint(params[3], 10, 64) + if err != nil { + return nil, errors.New("invalid params") + } + + amount, err := strconv.ParseUint(params[4], 10, 64) + if err != nil { + return nil, errors.New("invalid params") + } + + result, err := getBusiness(addr, key, secret, pubKey).TransactionsGet(uint(uid), params[1], params[2], uint(page), uint(amount)) + if err != nil { + return nil, err + } + return result.ToResult(), nil default: return nil, errors.New("unknown action: " + action) } @@ -387,16 +624,22 @@ func getKYC(addr, key, secret string) *sdk.KYC { return sdk.NewKYCWithAddr(addr, key, secret) } +func getBusiness(addr, key, secret, pubKey string) *sdk.Business { + b, _ := sdk.NewBusinessWithAddr(addr, key, secret, pubKey) + return b +} + func main() { usage := `JadePool SAAS control tool. Usage: - ctl [...] [-a ] + ctl [...] [-a ] [-p ] ctl -h | --help Options: -h --help Show this screen. - -a , --address Use custom SAAS server, e.g., http://127.0.0.1:8092` + -a , --address Use custom SAAS server, e.g., http://127.0.0.1:8092 + -p , --pubkey Use the public key pem file for verifying response` arguments, _ := docopt.ParseDoc(usage) @@ -408,6 +651,7 @@ Options: fmt.Println("code:", result.Code) fmt.Println("message:", result.Message) + fmt.Println("sign:", result.Sign) fmt.Println("data:") printMap(result.Data) } diff --git a/company.go b/company.go index 1e0fa90..51d0e20 100644 --- a/company.go +++ b/company.go @@ -207,6 +207,10 @@ func (c *Company) getSecret() string { return c.Secret } +func (c *Company) getPubKey() string { + return "" +} + func (c *Company) getAddr() string { return c.Addr } diff --git a/crypto.go b/crypto.go index 93fe43e..011b24f 100644 --- a/crypto.go +++ b/crypto.go @@ -6,10 +6,17 @@ import ( "crypto/cipher" "crypto/hmac" "crypto/sha256" + "crypto/x509/pkix" + "encoding/asn1" "encoding/base64" "encoding/hex" "encoding/json" + "encoding/pem" + "errors" "fmt" + "github.com/btcsuite/btcd/btcec" + "golang.org/x/crypto/sha3" + "math/big" "reflect" "sort" "strconv" @@ -157,8 +164,158 @@ func aesDecrypt(src []byte, key []byte, iv []byte) ([]byte, error) { return src, nil } +func parseEcdsaPrivateKeyFromPem(pemContent []byte) (*btcec.PrivateKey, error) { + block, _ := pem.Decode(pemContent) + if block == nil { + return nil, errors.New("invalid pem") + } + + var ecp ecPrivateKey + _, err := asn1.Unmarshal(block.Bytes, &ecp) + if err != nil { + return nil, err + } + + priKey, _ := btcec.PrivKeyFromBytes(btcec.S256(), ecp.PrivateKey) + return priKey, nil +} + +func signECCDataStr(priKeyPemBase64 string, msgStr string, sigEncode string) (*eccSig, error) { + priKeyPem, err := base64.StdEncoding.DecodeString(priKeyPemBase64) + if err != nil { + return nil, err + } + priKey, err := parseEcdsaPrivateKeyFromPem(priKeyPem) + if err != nil { + return nil, err + } + + sha3Hash := sha3.NewLegacyKeccak256() + _, err = sha3Hash.Write([]byte(msgStr)) + if err != nil { + return nil, err + } + msgBuf := sha3Hash.Sum(nil) + sig, err := priKey.Sign(msgBuf) + if err != nil { + return nil, err + } + + r := sig.R.Bytes() + s := sig.S.Bytes() + if len(r) < 32 { + preArr := []byte{} + for i := len(r) + 1; i <= 32; i++ { + preArr = append(preArr, 0) + } + r = append(preArr, r...) + } + if len(s) < 32 { + preArr := []byte{} + for i := len(s) + 1; i <= 32; i++ { + preArr = append(preArr, 0) + } + s = append(preArr, s...) + } + _sig := &eccSig{} + if sigEncode == "hex" { + _sig.R = hex.EncodeToString(r) + _sig.S = hex.EncodeToString(s) + } else if sigEncode == "base64" { + _sig.R = base64.StdEncoding.EncodeToString(r) + _sig.S = base64.StdEncoding.EncodeToString(s) + } + return _sig, nil +} + +func parseEcdsaPubKeyFromPem(pemContent []byte) (*btcec.PublicKey, error) { + block, _ := pem.Decode(pemContent) + if block == nil { + return nil, errors.New("invalid pem") + } + + var ecp ecPublicKey + _, err := asn1.Unmarshal(block.Bytes, &ecp) + if err != nil { + return nil, err + } + + return btcec.ParsePubKey(ecp.PublicKey.RightAlign(), btcec.S256()) +} + +func verifyECCSign(pubKeyPemBase64 string, obj map[string]interface{}, sign *eccSig, sigEncode string) (bool, error) { + pubKeyPem, err := base64.StdEncoding.DecodeString(pubKeyPemBase64) + if err != nil { + return false, err + } + + pubKey, err := parseEcdsaPubKeyFromPem(pubKeyPem) + if err != nil { + return false, err + } + + msgStr := buildMsg(obj, "", "") + return verifyECCSignStr(msgStr, sign, pubKey, sigEncode) +} + +func verifyECCSignStr(msgStr string, sign *eccSig, pubKey *btcec.PublicKey, sigEncode string) (bool, error) { + sha3Hash := sha3.NewLegacyKeccak256() + _, err := sha3Hash.Write([]byte(msgStr)) + if err != nil { + return false, err + } + msgBuf := sha3Hash.Sum(nil) + + var decodedR, decodedS []byte + if sigEncode == "hex" { + decodedR, err = hex.DecodeString(sign.R) + if err != nil { + return false, err + } + decodedS, err = hex.DecodeString(sign.S) + if err != nil { + return false, err + } + } else if sigEncode == "base64" { + decodedR, err = base64.StdEncoding.DecodeString(sign.R) + if err != nil { + return false, err + } + decodedS, err = base64.StdEncoding.DecodeString(sign.S) + if err != nil { + return false, err + } + } + + signature := btcec.Signature{ + R: new(big.Int).SetBytes(decodedR), + S: new(big.Int).SetBytes(decodedS), + } + return signature.Verify(msgBuf, pubKey), nil +} + func unpadding(src []byte) []byte { n := len(src) unPadNum := int(src[n-1]) return src[:n-unPadNum] } + +//This type provides compatibility with the btcec package +type ecPrivateKey struct { + Version int + PrivateKey []byte + NamedCurveOID asn1.ObjectIdentifier `asn1:"optional,explicit,tag:0"` + PublicKey asn1.BitString `asn1:"optional,explicit,tag:1"` +} + +//This type provides compatibility with the btcec package +type ecPublicKey struct { + Raw asn1.RawContent + Algorithm pkix.AlgorithmIdentifier + PublicKey asn1.BitString +} + +type eccSig struct { + R string `json:"r"` + S string `json:"s"` +} diff --git a/doc/business.md b/doc/business.md new file mode 100644 index 0000000..318767c --- /dev/null +++ b/doc/business.md @@ -0,0 +1,111 @@ +git clone git@github.com:nbltrust/hashkey-custody-sdk-go.git + +git checkout business + +go mod tidy + +prepare pri_hashkey-hub.pem and pub_xpert_238.pem + +go run cmd/ctl/main.go hashkey-hub pri_hashkey-hub.pem BusinessAssetsGet -a "https://develop-saas.nbltrust.com/saas-business" -p pub_xpert_238.pem + +```json +{ + "assets": [ + { + "decimal": 18, + "id": 1, + "name": "ETH", + "switch": true + }, + { + "decimal": 8, + "id": 2, + "name": "BTC", + "switch": true + } + ] +} +``` + +go run cmd/ctl/main.go hashkey-hub pri_hashkey-hub.pem BusinessClientGet 235 -a "https://develop-saas.nbltrust.com/saas-business" -p pub_xpert_238.pem + +```json +{ + "email": "cob63176@tuofs.com", + "id": 235, + "kycLevel": 1, + "name": "Christina", + "phone": "+86-13817572905" +} +``` + +go run cmd/ctl/main.go hashkey-hub pri_hashkey-hub.pem BusinessWalletBalancesGet 435 1 -a "https://develop-saas.nbltrust.com/saas-business" -p pub_xpert_238.pem + +```json +{ + "balances": [ + { + "assetID": 1, + "assetName": "ETH", + "available": "1000.000000000000000000", + "locked": "0.000000000000000000", + "total": "1000.000000000000000000" + } + ] +} +``` + +go run cmd/ctl/main.go hashkey-hub pri_hashkey-hub.pem BusinessBalanceLock 165045845511 435 1 11 -a "https://develop-saas.nbltrust.com/saas-business" -p pub_xpert_238.pem + +```json +{ + "available": "989.000000000000000000", + "total": "1000.000000000000000000" +} +``` + +go run cmd/ctl/main.go hashkey-hub pri_hashkey-hub.pem BusinessBalanceUnlock 165045845512 435 1 6 -a "https://develop-saas.nbltrust.com/saas-business" -p pub_xpert_238.pem + +```json +{ + "available": "995.000000000000000000", + "total": "1000.000000000000000000" +} +``` + +go run cmd/ctl/main.go hashkey-hub pri_hashkey-hub.pem BusinessTransfer 165045845513 1 4.1 435 235 -a "https://develop-saas.nbltrust.com/saas-business" -p pub_xpert_238.pem + +```json +{ + "id": 4 +} +``` + +go run cmd/ctl/main.go hashkey-hub pri_hashkey-hub.pem BusinessSwap 165045845515 435 2 0.11 1 1.22 -a "https://develop-saas.nbltrust.com/saas-business" -p pub_xpert_238.pem + +```json +{ + "id": 7 +} +``` + +go run cmd/ctl/main.go hashkey-hub pri_hashkey-hub.pem BusinessOrderGet 165045845515 -a "https://develop-saas.nbltrust.com/saas-business" -p pub_xpert_238.pem + +```json +{ + "detail": { + "from": 435, + "fromAmount": "0.11", + "fromAssetID": 2, + "fromWalletID": 1339, + "note": "", + "officialAmount": "1.22", + "officialAssetID": 1, + "officialWalletID": 1307, + "sequence": "165045845515" + }, + "id": 7, + "status": "DONE", + "type": "SWAP" +} +``` diff --git a/go.mod b/go.mod index a0403d3..2b5f200 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,10 @@ module github.com/nbltrust/hashkey-custody-sdk-go go 1.13 require ( + github.com/btcsuite/btcd v0.0.0-20190629003639-c26ffa870fd8 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 github.com/imroc/req v0.2.4 + github.com/labstack/echo/v4 v4.7.2 + github.com/sirupsen/logrus v1.8.1 + golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 ) diff --git a/kyc.go b/kyc.go index 92d9188..c7d221a 100644 --- a/kyc.go +++ b/kyc.go @@ -129,6 +129,10 @@ func (k *KYC) getSecret() string { return k.Secret } +func (k *KYC) getPubKey() string { + return "" +} + func (k *KYC) getAddr() string { return k.Addr } diff --git a/session.go b/session.go index 4047361..e7d329c 100644 --- a/session.go +++ b/session.go @@ -6,12 +6,15 @@ import ( "fmt" "io/ioutil" "math/rand" + "net/http" + "strconv" "time" "github.com/imroc/req" ) type params req.Param +type businessParams req.Param // Result request result. type Result struct { @@ -21,6 +24,18 @@ type Result struct { Sign string } +// BusinessResult request result. +type BusinessResult struct { + Code int + Data map[string]interface{} + Message string + Sign struct { + R string + S string + } + signVerified bool +} + type session struct { client client nonceCount int @@ -30,6 +45,10 @@ func (session *session) get(path string) (*Result, error) { return session.getWithParams(path, map[string]interface{}{}) } +func (session *session) businessGet(path string) (*BusinessResult, error) { + return session.businessGetWithParams(path, map[string]interface{}{}) +} + func (session *session) getWithParams(path string, params params) (*Result, error) { url := session.getURL(path) err := session.prepareParams(params) @@ -58,6 +77,34 @@ func (session *session) getWithParams(path string, params params) (*Result, erro return &result, err } +func (session *session) businessGetWithParams(path string, params businessParams) (*BusinessResult, error) { + url := session.getURL(path) + err := session.businessPrepareParams(http.MethodGet, path, params) + if err != nil { + return nil, err + } + + r, err := req.Get(url, session.commonHeaders(), req.Param(params)) + if err != nil { + return nil, err + } + if r.Response().StatusCode != 200 { + return nil, fmt.Errorf("http error code:%d", r.Response().StatusCode) + } + + var result BusinessResult + err = r.ToJSON(&result) + if err != nil { + return nil, fmt.Errorf("parse body to json failed: %v", err) + } + + if err = result.error(session.client.getPubKey()); err != nil { + return &result, err + } + + return &result, err +} + func (session *session) getFile(path string, filePath string) (*Result, error) { params := params{} @@ -135,6 +182,34 @@ func (session *session) post(path string, params params) (*Result, error) { return &result, err } +func (session *session) businessPost(path string, params businessParams) (*BusinessResult, error) { + url := session.getURL(path) + err := session.businessPrepareParams(http.MethodPost, path, params) + if err != nil { + return nil, err + } + + r, err := req.Post(url, session.commonHeaders(), req.BodyJSON(¶ms)) + if err != nil { + return nil, err + } + if r.Response().StatusCode != 200 { + return nil, fmt.Errorf("http error code:%d", r.Response().StatusCode) + } + + var result BusinessResult + err = r.ToJSON(&result) + if err != nil { + return nil, fmt.Errorf("parse body to json failed: %v", err) + } + + if err = result.error(session.client.getPubKey()); err != nil { + return nil, err + } + + return &result, err +} + func (session *session) patch(path string, params params) (*Result, error) { url := session.getURL(path) err := session.prepareParams(params) @@ -255,6 +330,34 @@ func (session *session) put(path string, params params) (*Result, error) { return &result, err } +func (session *session) businessPut(path string, params businessParams) (*BusinessResult, error) { + url := session.getURL(path) + err := session.businessPrepareParams(http.MethodPut, path, params) + if err != nil { + return nil, err + } + + r, err := req.Put(url, session.commonHeaders(), req.BodyJSON(¶ms)) + if err != nil { + return nil, err + } + if r.Response().StatusCode != 200 { + return nil, fmt.Errorf("http error code:%d", r.Response().StatusCode) + } + + var result BusinessResult + err = r.ToJSON(&result) + if err != nil { + return nil, fmt.Errorf("parse body to json failed: %v", err) + } + + if err = result.error(session.client.getPubKey()); err != nil { + return nil, err + } + + return &result, err +} + func (session *session) delete(path string) (*Result, error) { return session.deleteWithParams(path, map[string]interface{}{}) } @@ -298,6 +401,12 @@ func (session *session) prepareParams(params params) error { return params.sign(session.client.getSecret()) } +func (session *session) businessPrepareParams(method, path string, params businessParams) error { + timestamp := time.Now().Unix() + params["timestamp"] = timestamp + return params.sign(method, path, session.client.getSecret()) +} + func (session *session) commonHeaders() req.Header { keyName := session.client.getKeyHeaderName() return req.Header{ @@ -320,6 +429,62 @@ func (params *params) sign(secret string) error { return nil } +func (params *businessParams) sign(method, path string, priKeyPemBase64 string) error { + msgStr := method + path + msgStr += buildMsg(*params, "", "") + sign, err := signECCDataStr(priKeyPemBase64, msgStr, "base64") + if err != nil { + return err + } + if method == http.MethodGet { + (*params)["sigR"] = sign.R + (*params)["sigS"] = sign.S + } else { + (*params)["sig"] = map[string]interface{}{ + "r": sign.R, + "s": sign.S, + } + } + return nil +} + +func (result *BusinessResult) error(pubKey string) error { + if !result.success() { + return errors.New(result.Message) + } + + if len(pubKey) > 0 && !result.checkSign(pubKey) { + return errors.New("check sign failed") + } + return nil +} + +func (result *BusinessResult) success() bool { + return result.Code == 0 +} + +func (result *BusinessResult) checkSign(pubKeyPemBase64 string) bool { + verified, err := verifyECCSign(pubKeyPemBase64, result.Data, &eccSig{ + R: result.Sign.R, + S: result.Sign.S, + }, "base64") + if err != nil { + return false + } + + result.signVerified = true + return verified +} + +func (result *BusinessResult) ToResult() *Result { + return &Result{ + Code: result.Code, + Data: result.Data, + Message: result.Message, + Sign: strconv.FormatBool(result.signVerified), + } +} + func (result *Result) error(secret string) error { if !result.success() { return errors.New(result.Message) diff --git a/util.go b/util.go index 79eb41c..3dd3d98 100644 --- a/util.go +++ b/util.go @@ -8,5 +8,6 @@ type client interface { getKey() string getKeyHeaderName() string getSecret() string + getPubKey() string getAddr() string }