Skip to content

Commit

Permalink
Merge pull request #9 from eosio-enterprise/feature/maps
Browse files Browse the repository at this point in the history
Transitioned to map[string][]byte for all messages and blockchain tra…
  • Loading branch information
mgravitt authored Mar 16, 2020
2 parents 43598f8 + fcfe5f6 commit 91b14a7
Show file tree
Hide file tree
Showing 14 changed files with 175 additions and 219 deletions.
156 changes: 47 additions & 109 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,11 @@ Usage:
chappe [command]

Available Commands:
create Create chappe artifacts
get Get chappe artifacts
create Create chappe channel
get Get chappe message (via IPFS cid)
help Help about any command
publish Publish a private message to a channel
server Run a server
subscribe Subscribe to a channel
update Print update instructions
version Print the version

Flags:
Expand All @@ -79,19 +77,11 @@ Flags:
Use "chappe [command] --help" for more information about a command.
```

**PRs Welcome**

## Usage
### Dependencies
#### IPFS
It's simple to run your own IPFS node using Docker with only these 3 commands, or you may use ```ipfs.digscar.com``` for light testing.
I run go-ipfs:latest running in Docker.
``` bash
export ipfs_staging=</absolute/path/to/somewhere/>
export ipfs_data=</absolute/path/to/somewhere_else/>
### Configuration File
Chappe will locate a file named ```config.yaml``` by looking in the following folders: ```.```, ```configs```, ```/etc/chappe```, and ```$HOME/.chappe```.

docker run -d --name ipfs_host -v $ipfs_staging:/export -v $ipfs_data:/data/ipfs -p 4001:4001 -p 127.0.0.1:8080:8080 -p 127.0.0.1:5001:5001 ipfs/go-ipfs:latest
```
You can override any variable in the configuration file by setting an environment variable with a prefix of ```CHAPPE_```, followed by an all capital letter version of the variable that you want to override.

### Create a Key (Channel)
``` bash
Expand All @@ -100,21 +90,25 @@ docker run -d --name ipfs_host -v $ipfs_staging:/export -v $ipfs_data:/data/ipfs
The channel key is an assymetric RSA key. If you create a channel and you want another node to receive your messages, you would share the "chan4242.pem" file.

### Subscribe to the Channel
This runs a server, so fork your terminal shell to hold the ENV VARS intact.
This runs a server, so run it in a separate terminal.
``` bash
./chappe subscribe --channel-name chan4242
```

(After I publish, I will describe what happens with the subscribe process)
You can optionally request that the subscriber submit receipts/acknowledgements for each message. To prove that the receipient received and decrypted the message, the recipient's device key (unique to only that node) signs the decrypted message. This signature is posted to the blockchain, and the original sender may verify that the intended recipient(s) successfully received the message.

To send a receipt, pass the ```send-receipts``` or ```-r``` flag.
``` bash
./chappe subscribe --channel-name chan424 -r
```

### Publish to the Channel
On a separate tab, publish a message:
``` bash
./chappe publish --channel-name chan4242 --readable-memo "This is human-readable, unencrypted memo"
```

Currently, the publish command generates fake private data to be shared on the channel.

Currently, the publish command generates fake private data to be shared on the channel. More options will be added soon.
For example:
``` json
{
Expand All @@ -133,102 +127,46 @@ For example:
}
```

#### Hybrid Encryption
In order to support large messages (files), we use hybrid encryption as described in this paper (https://pdfs.semanticscholar.org/87ff/ea85fbf52e22e4808e1fcc9e40ead4ff7738.pdf).

We generate a random symmetric key to encrypt the message, then use the channel's (recipient's) assymetric public key to encrypt the symmetric key.

```chappe``` handles all of this but the purpose of this is for documentation of how it works
##### Step 1: Generate a Random AES Key (Symmetric)

Generate a one-time use key to encrypt the body of the message.
``` go
key := [32]byte{}
_, err := io.ReadFull(rand.Reader, key[:])
if err != nil {
panic(err)
}
```
##### Step 2: Encrypt the Message Data with the AES Key

``` go
block, err := aes.NewCipher(key[:])
if err != nil {
return nil, err
}

gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}

nonce := make([]byte, gcm.NonceSize())
_, err = io.ReadFull(rand.Reader, nonce)
if err != nil {
return nil, err
}

aesEncryptedData := gcm.Seal(nonce, nonce, plaintext, nil), nil
```

##### Step 3: Encrypt the AES Key with the Channel's Private Key
``` go
encryptedData, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, publicKey, key, label)
if err != nil {
fmt.Fprintf(os.Stderr, "Error from RSA encryption: %s\n", err)
return nil, err
}
```

##### Step 4: Publish Object to IPFS
The Encrypted Payload plus the Encrypted AES Key can be combined together, along with human-readable text to form the object. It is published to IPFS and a hash is returned.

``` go
type PersistedObject struct {
EncryptedPayload []byte
EncryptedAESKey []byte
UnencryptedPayload string
}

jsonPayloadNode, err := json.Marshal(payload)
if err != nil {
fmt.Fprintf(os.Stderr, "Could not marshal: %s", err)
}
The payload data is encrypted (see below) and constructed into an object and saved to IPFS. Here's an example of what one of the objects looks like: http://ipfs.digscar.com:8080/ipfs/QmNLuCqYR23RLzkE8fZvrnhsfaYJiawWAXcs2miLdeckND

hash, err := sh.Add(strings.NewReader(string(jsonPayloadNode)))
if err != nil {
fmt.Fprintf(os.Stderr, "Could not add data to IPFS: %s", err)
The blockchain transaction payload appears like this:
``` json
{
"payload": [
{
"key": "cid",
"value": "QmfBmT8CDSaRYQ6b7z1URNXZih7jWvgRdHw1oH5rQFAqoy"
},
{
"key": "memo",
"value": "foobars memo"
}
]
}
fmt.Println("IPFS Hash: ", hash)
```

##### Step 5: Publish IPFS Hash to EOSIO Blockchain
The user/node then publishes the IPFS hash to the appropriate blockchain, which records the event's existence, although elements of this metadata can be masked.
``` go
txOpts := &eos.TxOptions{}
if err := txOpts.FillFromChain(api); err != nil {
panic(fmt.Errorf("filling tx opts: %s", err))
}

tx := eos.NewTransaction([]*eos.Action{message.NewPub(hash, readableMemo)}, txOpts)
_, packedTx, err := api.SignTransaction(tx, txOpts.ChainID, eos.CompressionNone)
if err != nil {
panic(fmt.Errorf("sign transaction: %s", err))
}
#### Hybrid Encryption
In order to support large messages (files), we use hybrid encryption as described in this paper (https://pdfs.semanticscholar.org/87ff/ea85fbf52e22e4808e1fcc9e40ead4ff7738.pdf).

response, err := api.PushTransaction(packedTx)
if err != nil {
panic(fmt.Errorf("push transaction: %s", err))
}
```
We generate a random symmetric key for each message, then use the channel's (recipient's) assymetric public key to encrypt the symmetric key.

### Back to Subscription
```chappe``` handles all of this but the purpose of this is for documentation of how it works
1. Generate a Random AES Key (Symmetric)
2. Encrypt the Message Data with the AES Key
3. Encrypt the AES Key with the Channel's Private Key
4. Publish Message to IPFS
5. Publish IPFS CID (hash) to EOSIO Blockchain

The inverse happens on the subscription side:
- dfuse fires a websocket ( TODO: [ ] need to migrate to GraphQL)
- IPFS document is retrieved
- AES key is decrypted
- Message is decrypted
### Dependencies
#### Dfuse
You'll need a dfuse API key. You can register for a free one at dfuse.io

#### IPFS
It's simple to run your own IPFS node using Docker with only these 3 commands, or you may use ```ipfs.digscar.com``` for light testing.
I run go-ipfs:latest running in Docker.
``` bash
export ipfs_staging=</absolute/path/to/somewhere/>
export ipfs_data=</absolute/path/to/somewhere_else/>

(TODO: need to add an signed acknowledgement back to the sender)
docker run -d --name ipfs_host -v $ipfs_staging:/export -v $ipfs_data:/data/ipfs -p 4001:4001 -p 127.0.0.1:8080:8080 -p 127.0.0.1:5001:5001 ipfs/go-ipfs:latest
```
6 changes: 1 addition & 5 deletions cmd/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,7 @@ func MakeCreate() *cobra.Command {

encryption.CreateChannel(channelName)

log.Println(
`=======================================================================
key ` + channelName + ` created in files ` + channelName + `.pem (private) and ` + channelName + `.pub (public)
=======================================================================`)

log.Println(`Channel files created: ` + channelName + `.pem (private key) and ` + channelName + `.pub (public key)`)
return nil
}
return command
Expand Down
12 changes: 5 additions & 7 deletions cmd/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,17 @@ func MakePublish() *cobra.Command {
var command = &cobra.Command{
Use: "publish",
Short: "Publish a private message to a channel",
Long: `Create and share private messages with other nodes on a chappe network.`,
Example: ` chappe publish
chappe publish --channel-name MyChannel --readable-memo "This memo is not encrypted"
chappe publish --encrypt false`,

Long: `Create and share private messages with other nodes on a chappe network.
Currently, only a randomly generated JSON object is sent as the
message body. Other options will be added shortly.`,
Example: `chappe publish --channel-name MyChannel --memo "This memo is not encrypted"`,
SilenceUsage: false,
}

var channelName, readableMemo string
var encryptFlag bool

command.Flags().StringVarP(&channelName, "channel-name", "n", "", "channel name")
command.Flags().StringVarP(&readableMemo, "readable-memo", "m", "", "Human readable memo to attach to payload (never encrypted)")
command.Flags().StringVarP(&readableMemo, "memo", "m", "", "Human readable memo to attach to payload (never encrypted)")
command.Flags().BoolVarP(&encryptFlag, "encrypt", "", true, "Boolean flag whether to encrypt payload - defaults to true")

command.RunE = func(command *cobra.Command, args []string) error {
Expand Down
1 change: 0 additions & 1 deletion cmd/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ func MakeUpdate() *cobra.Command {
command.Run = func(cmd *cobra.Command, args []string) {
fmt.Println("Subscribe action is not yet implemented")
fmt.Println("Contribute: https://github.com/eosio-enterprise/chappe/blob/master/CONTRIBUTING.md")

}
return command
}
13 changes: 9 additions & 4 deletions configs/example-config.yaml
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
IPFS:
Endpoint: ec2-3-232-158-110.compute-1.amazonaws.com:5001
Endpoint: ipfs.digscar.com:5001
Eosio:
Endpoint: https://kylin.eosusa.news
PublishAccount: messengerbus
PublishPrivateKey: 5KAP1zytghuvowgprSPLNasajibZcxf4KMgdgNbrNj98xhcGAUa
Dfuse:
Protocol: GraphQL # WebSocket will use WSEndpoint; else it defaults to GraphQL
WSEndpoint: wss://kylin.eos.dfuse.io/v1/stream
Protocol: GraphQL # only GraphQL supported now # WebSocket will use WSEndpoint; else it defaults to GraphQL
WSEndpoint: wss://kylin.eos.dfuse.io/v1/stream # only GraphQL supported now
Origin: github.com/eosio-enterprise/chappe
ApiKey: <<insert dfuse api key here>>
GraphQLEndpoint: kylin.eos.dfuse.io:443
KeyDirectory: channels/
PublishInterval: 5s # Go Duration object
PublishInterval: 5s # Go Duration object
DeviceRSAPrivateKey: mykey # uniquely represents the device (used for receipts)

# coming soon
RevealMessage: true
RevealDelay: 60s # Time to wait before revealing the message
4 changes: 2 additions & 2 deletions contracts/messenger/include/messenger.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ CONTRACT messenger : public contract {
using contract::contract;

ACTION pub( string ipfs_hash, string memo );

ACTION pubmap ( std::map<string, string> payload);
using pub_action = action_wrapper<"pub"_n, &messenger::pub>;
// ACTION pubbytesmap ( std::map<string, byte[]> payload);

};
3 changes: 2 additions & 1 deletion contracts/messenger/src/messenger.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include <messenger.hpp>
ACTION messenger::pub( string ipfs_hash, string memo) {}
ACTION messenger::pubmap ( std::map<string, string> payload) {}
ACTION messenger::pubmap ( std::map<string, string> payload) {}
// ACTION messenger::pubbytesmap ( std::map<string, byte[]> payload) {}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.13
require (
github.com/bxcodec/faker v2.0.1+incompatible
github.com/dfuse-io/eosws-go v0.0.0-20191011181529-0eb3d4ce8743
github.com/eoscanada/eos-go v0.9.0
github.com/eoscanada/eos-go v0.9.1-0.20200316040626-bf09fb15dea8
github.com/golang/protobuf v1.3.3
github.com/ipfs/go-ipfs-api v0.0.3
github.com/multiformats/go-multihash v0.0.10 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/eoscanada/eos-go v0.8.5/go.mod h1:RKrm2XzZEZWxSMTRqH5QOyJ1fb/qKEjs2ix1aQl0sk4=
github.com/eoscanada/eos-go v0.9.0 h1:8Ko4/6lwn3KwbbDuaUB3p02OzbQVZVdxbp6noPChIkc=
github.com/eoscanada/eos-go v0.9.0/go.mod h1:6RuJFiRU1figWZ39M33o2cERU2MdL6VllElYLHTZNeo=
github.com/eoscanada/eos-go v0.9.1-0.20200316040626-bf09fb15dea8 h1:E79CwwnFq70bWq52l7qthwR2uY1EpVSUUojwUVbEgLs=
github.com/eoscanada/eos-go v0.9.1-0.20200316040626-bf09fb15dea8/go.mod h1:6RuJFiRU1figWZ39M33o2cERU2MdL6VllElYLHTZNeo=
github.com/ethereum/go-ethereum v1.9.9/go.mod h1:a9TqabFudpDu1nucId+k9S8R9whYaHnGBLKFouA5EAo=
github.com/fatih/color v1.3.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fjl/memsize v0.0.0-20180418122429-ca190fb6ffbc/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0=
Expand Down
17 changes: 12 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,18 @@ func main() {
},
}

var configFile string
rootCmd.Flags().StringVarP(&configFile, "config-file", "c", "configs/config.yaml", "Path/name of configuration file")
viper.SetConfigFile(configFile) //"configs/config.yaml")
if err := viper.ReadInConfig(); err != nil {
log.Fatalf("config file not found: %s", err)
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath("./configs")
viper.AddConfigPath("/etc/chappe/")
viper.AddConfigPath("$HOME/.chappe")
viper.AddConfigPath(".")
viper.SetEnvPrefix("CHAPPE")
viper.AutomaticEnv()

err := viper.ReadInConfig()
if err != nil {
log.Fatalf("Fatal error config file: %s \n", err)
}

rootCmd.AddCommand(cmdCreate)
Expand Down
Loading

0 comments on commit 91b14a7

Please sign in to comment.