From 43598f86025eaf7a7d2094a65311e60d75ba4c30 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 15 Mar 2020 18:36:58 -0400 Subject: [PATCH] added support for receiver to publish ack/receipt back --- .gitignore | 6 +- README.md | 11 +- cmd/subscribe.go | 7 +- contracts/messenger/include/messenger.hpp | 1 + contracts/messenger/messenger/messenger.abi | 29 +++++ contracts/messenger/src/CMakeLists.txt | 1 - contracts/messenger/src/messenger.cpp | 3 +- internal/encryption/rsa.go | 115 ++++++++++---------- pkg/dfuse-graphql.go | 13 ++- pkg/dfuse-websocket.go | 17 +-- pkg/message.go | 18 ++- pkg/publish.go | 59 +++++++++- pkg/receipt.go | 31 ++++++ pkg/receiver.go | 60 ---------- 14 files changed, 231 insertions(+), 140 deletions(-) create mode 100644 pkg/receipt.go delete mode 100644 pkg/receiver.go diff --git a/.gitignore b/.gitignore index 5171e13..d9bbe08 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,8 @@ build node_modules package-lock.json **.DS_Store -.DS_Store \ No newline at end of file +.DS_Store +ipld +Dockerfile +demo.txt + diff --git a/README.md b/README.md index ce092d8..7278166 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,8 @@ Eosio: PublishAccount: messengerbus PublishPrivateKey: 5KAP1zytghuvowgprSPLNasajibZcxf4KMgdgNbrNj98xhcGAUa Dfuse: - WSEndpoint: wss://kylin.eos.dfuse.io/v1/stream + Protocol: GraphQL + GraphQLEndpoint: kylin.eos.dfuse.io:443 Origin: github.com/eosio-enterprise/chappe ApiKey: web_*** # Replace this, get one at dfuse.io KeyDirectory: channels/ @@ -43,6 +44,14 @@ Open New Shell, and run Publisher ``` ./chappe publish --channel-name chan4242 --readable-memo "This is human-readable, unencrypted memo" ``` +## Features +- Send/receive encrypted (or unencrypted) messages/documents using public or private EOSIO chains +- Messages are sent on channels, and all nodes with the channel key can read messages +- Optionally publish receipts (acknowledgements) signed with a node's key +- Support for large messages and files +- Optionally mask all metadata (publisher, type of message) +- (Coming Soon) Ability to set reveal parameters that automatically publish decrypted version after time elapses +- (Coming Soon) Hierarchies for data visibility ## Menu Run chappe diff --git a/cmd/subscribe.go b/cmd/subscribe.go index 5c98689..9e1d4fe 100644 --- a/cmd/subscribe.go +++ b/cmd/subscribe.go @@ -19,15 +19,18 @@ func MakeSubscribe() *cobra.Command { } var channelName string + var sendReceipts bool command.Flags().StringVarP(&channelName, "channel-name", "n", "", "channel name") + command.Flags().BoolVarP(&sendReceipts, "send-receipts", "r", false, "send device-specific receipts back to hub") + command.Run = func(cmd *cobra.Command, args []string) { go func() { if viper.GetString("Dfuse.Protocol") == "WebSocket" { - pkg.StreamWS(channelName) + pkg.StreamWS(channelName, sendReceipts) } else { - pkg.StreamMessages(context.TODO(), channelName) + pkg.StreamMessages(context.TODO(), channelName, sendReceipts) } }() diff --git a/contracts/messenger/include/messenger.hpp b/contracts/messenger/include/messenger.hpp index cc6451a..fa5ec2d 100644 --- a/contracts/messenger/include/messenger.hpp +++ b/contracts/messenger/include/messenger.hpp @@ -8,5 +8,6 @@ CONTRACT messenger : public contract { ACTION pub( string ipfs_hash, string memo ); + ACTION pubmap ( std::map payload); using pub_action = action_wrapper<"pub"_n, &messenger::pub>; }; \ No newline at end of file diff --git a/contracts/messenger/messenger/messenger.abi b/contracts/messenger/messenger/messenger.abi index 8a6875c..c700fa2 100644 --- a/contracts/messenger/messenger/messenger.abi +++ b/contracts/messenger/messenger/messenger.abi @@ -3,6 +3,20 @@ "version": "eosio::abi/1.1", "types": [], "structs": [ + { + "name": "pair_string_string", + "base": "", + "fields": [ + { + "name": "key", + "type": "string" + }, + { + "name": "value", + "type": "string" + } + ] + }, { "name": "pub", "base": "", @@ -16,6 +30,16 @@ "type": "string" } ] + }, + { + "name": "pubmap", + "base": "", + "fields": [ + { + "name": "payload", + "type": "pair_string_string[]" + } + ] } ], "actions": [ @@ -23,6 +47,11 @@ "name": "pub", "type": "pub", "ricardian_contract": "" + }, + { + "name": "pubmap", + "type": "pubmap", + "ricardian_contract": "" } ], "tables": [], diff --git a/contracts/messenger/src/CMakeLists.txt b/contracts/messenger/src/CMakeLists.txt index 6534d1b..a94029f 100644 --- a/contracts/messenger/src/CMakeLists.txt +++ b/contracts/messenger/src/CMakeLists.txt @@ -5,4 +5,3 @@ find_package(eosio.cdt) add_contract( messenger messenger messenger.cpp ) target_include_directories( messenger PUBLIC ${CMAKE_SOURCE_DIR}/../include ) -target_ricardian_directory( messenger ${CMAKE_SOURCE_DIR}/../ricardian ) \ No newline at end of file diff --git a/contracts/messenger/src/messenger.cpp b/contracts/messenger/src/messenger.cpp index 82fdf1f..6d05e16 100644 --- a/contracts/messenger/src/messenger.cpp +++ b/contracts/messenger/src/messenger.cpp @@ -1,2 +1,3 @@ #include -ACTION messenger::pub( string ipfs_hash, string memo) {} \ No newline at end of file +ACTION messenger::pub( string ipfs_hash, string memo) {} +ACTION messenger::pubmap ( std::map payload) {} \ No newline at end of file diff --git a/internal/encryption/rsa.go b/internal/encryption/rsa.go index 2853179..e4c9654 100644 --- a/internal/encryption/rsa.go +++ b/internal/encryption/rsa.go @@ -1,6 +1,7 @@ package encryption import ( + "crypto" "crypto/rand" "crypto/rsa" "crypto/sha256" @@ -9,62 +10,43 @@ import ( "errors" "fmt" "io/ioutil" + "log" "os" "github.com/spf13/viper" ) -// ParseRsaPublicKeyFromPem ... -func ParseRsaPublicKeyFromPem(pubPEM []byte) (*rsa.PublicKey, error) { - block, _ := pem.Decode(pubPEM) - if block == nil { - return nil, errors.New("failed to parse PEM block containing the key") - } - - pub, err := x509.ParsePKIXPublicKey(block.Bytes) - if err != nil { - return nil, err - } +// CreateChannel ... +func CreateChannel(keyname string) (*rsa.PrivateKey, *rsa.PublicKey) { - switch pub := pub.(type) { - case *rsa.PublicKey: - return pub, nil - default: - break // fall through - } - return nil, errors.New("Key type is not RSA") -} + priv, pub := generateRsaKeyPair() -// ParseRsaPrivateKeyFromPem ... -func ParseRsaPrivateKeyFromPem(privPEM []byte) (*rsa.PrivateKey, error) { - block, _ := pem.Decode(privPEM) - if block == nil { - return nil, errors.New("failed to parse PEM block containing the key") - } + // Export the keys to pem string + privPem := exportPrivateKey(priv) + pubPem, _ := exportPublicKey(pub) - priv, err := x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { - return nil, err - } + ioutil.WriteFile(viper.GetString("KeyDirectory")+keyname+".pub", pubPem, 0644) + ioutil.WriteFile(viper.GetString("KeyDirectory")+keyname+".pem", privPem, 0644) - return priv, nil + return priv, pub } -func loadPublicKey(keyname string) *rsa.PublicKey { +// CalcReceipt ... +func CalcReceipt(payload []byte) ([]byte, error) { - publicKeyPemStr, err := ioutil.ReadFile(viper.GetString("KeyDirectory") + keyname + ".pub") - if err != nil { - fmt.Fprintf(os.Stderr, "Error from reading key file: %s\n", err) - return nil - } + var signature []byte + rsaPrivateKey := load(viper.GetString("DeviceRSAPrivateKey")) + + // Only small messages can be signed directly; thus the hash of a + // message, rather than the message itself, is signed. + hashed := sha256.Sum256(payload) - publicKey, err := ParseRsaPublicKeyFromPem(publicKeyPemStr) + signature, err := rsa.SignPKCS1v15(rand.Reader, rsaPrivateKey, crypto.SHA256, hashed[:]) if err != nil { - fmt.Fprintf(os.Stderr, "Error from reading key file: %s\n", err) - return nil + log.Printf("Error from signing: %s\n", err) + return signature, err } - - return publicKey + return signature, nil } // RsaEncrypt ... @@ -94,6 +76,42 @@ func RsaDecrypt(channelName string, payload []byte) ([]byte, error) { return plaintext, nil } +func loadPublicKey(keyname string) *rsa.PublicKey { + publicKeyPemStr, err := ioutil.ReadFile(viper.GetString("KeyDirectory") + keyname + ".pub") + if err != nil { + fmt.Fprintf(os.Stderr, "Error from reading key file: %s\n", err) + return nil + } + + publicKey, err := parseRsaPublicKeyFromPem(publicKeyPemStr) + if err != nil { + fmt.Fprintf(os.Stderr, "Error from reading key file: %s\n", err) + return nil + } + + return publicKey +} + +func parseRsaPublicKeyFromPem(pubPEM []byte) (*rsa.PublicKey, error) { + block, _ := pem.Decode(pubPEM) + if block == nil { + return nil, errors.New("failed to parse PEM block containing the key") + } + + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, err + } + + switch pub := pub.(type) { + case *rsa.PublicKey: + return pub, nil + default: + break // fall through + } + return nil, errors.New("Key type is not RSA") +} + func parseRsaPrivateKeyFromPem(privPEM []byte) (*rsa.PrivateKey, error) { block, _ := pem.Decode(privPEM) if block == nil { @@ -153,18 +171,3 @@ func exportPublicKey(pubkey *rsa.PublicKey) ([]byte, error) { ) return pubkeyPem, nil } - -// CreateChannel ... -func CreateChannel(keyname string) (*rsa.PrivateKey, *rsa.PublicKey) { - - priv, pub := generateRsaKeyPair() - - // Export the keys to pem string - privPem := exportPrivateKey(priv) - pubPem, _ := exportPublicKey(pub) - - ioutil.WriteFile(viper.GetString("KeyDirectory")+keyname+".pub", pubPem, 0644) - ioutil.WriteFile(viper.GetString("KeyDirectory")+keyname+".pem", privPem, 0644) - - return priv, pub -} diff --git a/pkg/dfuse-graphql.go b/pkg/dfuse-graphql.go index 0256bfa..a7783a2 100644 --- a/pkg/dfuse-graphql.go +++ b/pkg/dfuse-graphql.go @@ -43,7 +43,7 @@ func getToken(apiKey string) (token string, expiration time.Time, err error) { func createClient(endpoint string) pb.GraphQLClient { dfuseAPIKey := viper.GetString("Dfuse.ApiKey") if dfuseAPIKey == "" { - panic("Dfuse.ApiKey is required in configuration") + log.Fatal("Dfuse.ApiKey is required in configuration") } token, _, err := getToken(dfuseAPIKey) @@ -86,7 +86,7 @@ type eosioDocument struct { } // StreamMessages ... -func StreamMessages(ctx context.Context, channelName string) { +func StreamMessages(ctx context.Context, channelName string, sendReceipts bool) { /* The client can be re-used for all requests, cache it at the appropriate level */ client := createClient(viper.GetString("Dfuse.GraphQLEndpoint")) executor, err := client.Execute(ctx, &pb.Request{Query: operationEOS}) @@ -121,7 +121,14 @@ func StreamMessages(ctx context.Context, channelName string) { } else { for _, action := range result.Trace.MatchingActions { data := action.JSON - receiveGQL(channelName, data) + if data["memo"] == "receipt" { + log.Println("Received receipt, ignoring.") + } else { + message, err := receiveGQL(channelName, data) + if err == nil && sendReceipts { + SendReceipt(channelName, message) + } + } } } } diff --git a/pkg/dfuse-websocket.go b/pkg/dfuse-websocket.go index 2416013..f3f4d37 100644 --- a/pkg/dfuse-websocket.go +++ b/pkg/dfuse-websocket.go @@ -10,9 +10,9 @@ import ( ) // StreamWS ... -func StreamWS(channelName string) { - client := GetClient() - err := client.Send(GetActionTraces()) +func StreamWS(channelName string, sendReceipts bool) { + client := getClient() + err := client.Send(getActionTraces()) if err != nil { log.Fatalf("Failed to send request to dfuse: %s", err) } @@ -25,8 +25,10 @@ func StreamWS(channelName string) { switch m := msg.(type) { case *eosws.ActionTrace: - // pkg.StreamMessages(context.TODO()) - receiveWS(channelName, m) + message, err := receiveWS(channelName, m) + if err == nil && sendReceipts { + SendReceipt(channelName, message) + } case *eosws.Progress: fmt.Print(".") // poor man's progress bar, using print not log case *eosws.Listening: @@ -48,11 +50,12 @@ func receiveWS(channelName string, dfuseMessage *eosws.ActionTrace) (Message, er log.Println("Error loading message: ", err) return msg, err } + return msg, nil } // GetClient ... -func GetClient() *eosws.Client { +func getClient() *eosws.Client { apiKey := viper.GetString("Dfuse.ApiKey") if apiKey == "" { log.Fatalf("Missing Dfuse.ApiKey in config") @@ -74,7 +77,7 @@ func GetClient() *eosws.Client { } // GetActionTraces ... -func GetActionTraces() *eosws.GetActionTraces { +func getActionTraces() *eosws.GetActionTraces { ga := &eosws.GetActionTraces{} ga.ReqID = "chappe" ga.StartBlock = -300 diff --git a/pkg/message.go b/pkg/message.go index 1ffeeb7..b9236f2 100644 --- a/pkg/message.go +++ b/pkg/message.go @@ -13,10 +13,20 @@ import ( // Message ... type Message struct { - EncryptedPayload []byte - EncryptedAESKey []byte - ReadableMemos []string - Payload map[string][]byte + // EncryptedPayload []byte + // EncryptedAESKey []byte + // ReadableMemos []string + Payload map[string][]byte +} + +// Bytes ... +func (m Message) Bytes() []byte { + jsonMessage, err := json.Marshal(m) + if err != nil { + log.Println("Cannot convert to message to bytes", err) + } + + return []byte(jsonMessage) } // NewMessage - creates a new Message object, provisions the Payload map diff --git a/pkg/publish.go b/pkg/publish.go index c920635..36046c6 100644 --- a/pkg/publish.go +++ b/pkg/publish.go @@ -14,10 +14,10 @@ import ( func newPub(ipfsHash, memo string) *eos.Action { return &eos.Action{ - Account: eos.AN("messengerbus"), + Account: eos.AN(viper.GetString("Eosio.PublishAccount")), Name: eos.ActN("pub"), Authorization: []eos.PermissionLevel{ - {Actor: "messengerbus", Permission: eos.PN("active")}, + {Actor: eos.AN(viper.GetString("Eosio.PublishAccount")), Permission: eos.PN("active")}, }, ActionData: eos.NewActionData(PubActionPayload{ IpfsHash: ipfsHash, @@ -26,13 +26,64 @@ func newPub(ipfsHash, memo string) *eos.Action { } } +// PubMapActionPayload ... +type PubMapActionPayload struct { + PayloadMap map[string]string `json:"payload"` +} + +func newPubMap(payload map[string]string) *eos.Action { + return &eos.Action{ + Account: eos.AN(viper.GetString("Eosio.PublishAccount")), + Name: eos.ActN("pubmap"), + Authorization: []eos.PermissionLevel{ + {Actor: eos.AN(viper.GetString("Eosio.PublishAccount")), Permission: eos.PN("active")}, + }, + ActionData: eos.NewActionData(PubMapActionPayload{ + PayloadMap: payload, + }), + } +} + // PubActionPayload ... type PubActionPayload struct { IpfsHash string `json:"ipfs_hash"` Memo string `json:"memo"` } -func addToEosio(cid string, readableMemo string) (string, error) { +// PublishMapToBlockchain ... +func PublishMapToBlockchain(payload map[string]string) (string, error) { + api := eos.New(viper.GetString("Eosio.Endpoint")) + + keyBag := &eos.KeyBag{} + err := keyBag.ImportPrivateKey(viper.GetString("Eosio.PublishPrivateKey")) + if err != nil { + log.Panicf("import private key: %s", err) + } + api.SetSigner(keyBag) + + txOpts := &eos.TxOptions{} + if err := txOpts.FillFromChain(api); err != nil { + log.Printf("Error filling tx opts: %s", err) + return "error", err + } + + tx := eos.NewTransaction([]*eos.Action{newPubMap(payload)}, txOpts) + _, packedTx, err := api.SignTransaction(tx, txOpts.ChainID, eos.CompressionNone) + if err != nil { + log.Printf("Error signing transaction: %s", err) + return "error", err + } + + response, err := api.PushTransaction(packedTx) + if err != nil { + log.Printf("Error pushing transaction: %s", err) + return "error", err + } + return hex.EncodeToString(response.Processed.ID), nil +} + +// AddToEosio ... +func AddToEosio(cid string, readableMemo string) (string, error) { api := eos.New(viper.GetString("Eosio.Endpoint")) keyBag := &eos.KeyBag{} @@ -87,5 +138,5 @@ func Publish(payload Message) (string, error) { } else { blockchainMemo = string(memoBytes) } - return addToEosio(addToIpfs(payload), blockchainMemo) + return AddToEosio(addToIpfs(payload), blockchainMemo) } diff --git a/pkg/receipt.go b/pkg/receipt.go new file mode 100644 index 0000000..57fa755 --- /dev/null +++ b/pkg/receipt.go @@ -0,0 +1,31 @@ +package pkg + +import ( + "encoding/hex" + "log" + + "github.com/eosio-enterprise/chappe/internal/encryption" +) + +// SendReceipt ... +func SendReceipt(channelName string, msg Message) error { + + receiptSignature, err := encryption.CalcReceipt(msg.Bytes()) + if err != nil { + // TODO: improve handling if the receipt cannot be generated and sent, perhaps should be fatal + log.Println("Cannot send receipt: ", err) + } + receiptStr := hex.EncodeToString(receiptSignature) + log.Println("Sending receipt: ", receiptStr) + + // receiptMap := make(map[string]string) + // receiptMap["receipt"] = receiptStr + + // trxID, _ := PublishMapToBlockchain(receiptMap) + // // currently, receipts will reveal other parties on the channel + // // TODO: mask metadata on receipts + trxID, _ := AddToEosio(receiptStr, "receipt") + + log.Println("Sent receipt, transaction ID: ", trxID) + return nil +} diff --git a/pkg/receiver.go b/pkg/receiver.go deleted file mode 100644 index db0de99..0000000 --- a/pkg/receiver.go +++ /dev/null @@ -1,60 +0,0 @@ -package pkg - -import ( - "bytes" - "encoding/json" - "fmt" - "log" - - "github.com/dfuse-io/eosws-go" - "github.com/eosio-enterprise/chappe/internal/encryption" - shell "github.com/ipfs/go-ipfs-api" - "github.com/spf13/viper" - "github.com/tidwall/gjson" -) - -// Receive ... -func Receive(channelName string, dfuseMessage *eosws.ActionTrace) error { - ipfsHash := gjson.Get(string(dfuseMessage.Data.Trace), "act.data.ipfs_hash") - memo := gjson.Get(string(dfuseMessage.Data.Trace), "act.data.memo") - fmt.Println() - log.Println("Received notification of new message: ", ipfsHash, "; memo: ", memo) - - sh := shell.NewShell(viper.GetString("IPFS.Endpoint")) - reader, err := sh.Cat(ipfsHash.String()) - if err != nil { - log.Println("Could not not find IPFS hash: ", ipfsHash, "; Error: ", err) - } - - buf := new(bytes.Buffer) - buf.ReadFrom(reader) - retrievedContents := buf.String() - - var msg Message - if err := json.Unmarshal([]byte(retrievedContents), &msg); err != nil { - log.Println("Cannot unmarshal object retrieved from IPFS: ", err) - } - - aesKey, err := encryption.RsaDecrypt(channelName, msg.EncryptedAESKey) - if err != nil { - trxID := string(dfuseMessage.Data.TransactionID) - log.Println("Cannot read contents of message, discarding. TrxId: ", trxID) - } else { - plaintext, err := encryption.AesDecrypt(msg.EncryptedPayload, &aesKey) - if err != nil { - log.Printf("Error from AES decryption: %s\n", err) - } - - log.Println("Decrypted message from channelName: \n", string(plaintext)) - - // We could also unmarshal the text (which is JSON) into an object - // var receivedPrivatePayload FakePrivatePayload - // err = json.Unmarshal(plaintext, &receivedPrivatePayload) - - // indentedPayload, err := json.MarshalIndent(receivedPrivatePayload, "", " ") - // if err != nil { - // log.Printf("Error from marshall indent: %s\n", err) - // } - } - return nil -}