integrate status-protocol-go

This commit is contained in:
Adam Babik 2019-07-18 00:25:42 +02:00 committed by Andrea Maria Piana
parent 664488faf9
commit e93d994460
215 changed files with 11057 additions and 6928 deletions

View File

@ -42,4 +42,3 @@ exclude_patterns:
- static/
- t/
- mailserver/migrations
- messaging/chat/migrations

View File

@ -47,7 +47,6 @@ linters:
- golint
- govet
- ineffassign
- interfacer
- megacheck
- misspell
- structcheck

View File

@ -172,7 +172,6 @@ setup: setup-build setup-dev tidy ##@other Prepare project for development and b
generate: ##@other Regenerate assets and other auto-generated stuff
go generate ./static ./static/chat_db_migrations ./static/mailserver_db_migrations ./t
$(shell cd ./messaging/chat/protobuf && exec protoc --go_out=. ./*.proto)
prepare-release: clean-release
mkdir -p $(RELEASE_DIR)

View File

@ -1 +1 @@
0.30.0-beta.3
0.30.1-beta.0

View File

@ -18,8 +18,8 @@ import (
gethnode "github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/crypto"
"github.com/status-im/status-go/mailserver/registry"
"github.com/status-im/status-go/messaging/crypto"
"github.com/status-im/status-go/node"
"github.com/status-im/status-go/notifications/push/fcm"
"github.com/status-im/status-go/params"

81
crypto/crypto.go Normal file
View File

@ -0,0 +1,81 @@
package crypto
import (
"crypto/ecdsa"
"encoding/hex"
"errors"
"fmt"
"github.com/ethereum/go-ethereum/crypto"
)
// Sign signs the hash of an arbitrary string
func Sign(content string, identity *ecdsa.PrivateKey) (string, error) {
signature, err := crypto.Sign(crypto.Keccak256([]byte(content)), identity)
if err != nil {
return "", err
}
return hex.EncodeToString(signature), nil
}
// VerifySignatures verifys tuples of signatures content/hash/public key
func VerifySignatures(signaturePairs [][3]string) error {
for _, signaturePair := range signaturePairs {
content := crypto.Keccak256([]byte(signaturePair[0]))
signature, err := hex.DecodeString(signaturePair[1])
if err != nil {
return err
}
publicKeyBytes, err := hex.DecodeString(signaturePair[2])
if err != nil {
return err
}
publicKey, err := crypto.UnmarshalPubkey(publicKeyBytes)
if err != nil {
return err
}
recoveredKey, err := crypto.SigToPub(
content,
signature,
)
if err != nil {
return err
}
if crypto.PubkeyToAddress(*recoveredKey) != crypto.PubkeyToAddress(*publicKey) {
return errors.New("identity key and signature mismatch")
}
}
return nil
}
// ExtractSignatures extract from tuples of signatures content a public key
func ExtractSignatures(signaturePairs [][2]string) ([]string, error) {
response := make([]string, len(signaturePairs))
for i, signaturePair := range signaturePairs {
content := crypto.Keccak256([]byte(signaturePair[0]))
signature, err := hex.DecodeString(signaturePair[1])
if err != nil {
return nil, err
}
recoveredKey, err := crypto.SigToPub(
content,
signature,
)
if err != nil {
return nil, err
}
response[i] = fmt.Sprintf("%x", crypto.FromECDSAPub(recoveredKey))
}
return response, nil
}

View File

@ -98,37 +98,3 @@ func TestVerifySignature(t *testing.T) {
err = VerifySignatures(signaturePairs)
require.Error(t, err)
}
func TestSymmetricEncryption(t *testing.T) {
const rawKey = "0000000000000000000000000000000000000000000000000000000000000000"
expectedPlaintext := []byte("test")
key, err := hex.DecodeString(rawKey)
require.Nil(t, err, "Key should be generated without errors")
cyphertext1, err := EncryptSymmetric(key, expectedPlaintext)
require.Nil(t, err, "Cyphertext should be generated without errors")
cyphertext2, err := EncryptSymmetric(key, expectedPlaintext)
require.Nil(t, err, "Cyphertext should be generated without errors")
require.Equalf(
t,
32,
len(cyphertext1),
"Cyphertext with the correct length should be generated")
require.NotEqualf(
t,
cyphertext1,
cyphertext2,
"Same plaintext should not be encrypted in the same way")
plaintext, err := DecryptSymmetric(key, cyphertext1)
require.Nil(t, err, "Cyphertext should be decrypted without errors")
require.Equalf(
t,
expectedPlaintext,
plaintext,
"Cypther text should be decrypted successfully")
}

16
go.mod
View File

@ -8,33 +8,25 @@ require (
github.com/btcsuite/btcd v0.0.0-20190523000118-16327141da8c
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d
github.com/ethereum/go-ethereum v1.8.27
github.com/go-playground/locales v0.12.1 // indirect
github.com/go-playground/universal-translator v0.16.0 // indirect
github.com/golang-migrate/migrate v3.5.4+incompatible // indirect
github.com/golang-migrate/migrate/v4 v4.4.0 // indirect
github.com/golang-migrate/migrate/v4 v4.5.0 // indirect
github.com/golang/mock v1.2.0
github.com/golang/protobuf v1.3.1
github.com/lib/pq v1.0.0
github.com/libp2p/go-libp2p-core v0.0.3
github.com/multiformats/go-multiaddr v0.0.4
github.com/mutecomm/go-sqlcipher v0.0.0-20170920224653-f799951b4ab2
github.com/pborman/uuid v0.0.0-20170112150404-1b00554d8222
github.com/russolsen/ohyeah v0.0.0-20160324131710-f4938c005315 // indirect
github.com/russolsen/same v0.0.0-20160222130632-f089df61f51d // indirect
github.com/russolsen/transit v0.0.0-20180705123435-0794b4c4505a
github.com/status-im/doubleratchet v2.0.0+incompatible
github.com/status-im/migrate/v4 v4.3.1-status
github.com/status-im/rendezvous v1.3.0
github.com/status-im/status-protocol-go v0.1.1
github.com/status-im/whisper v1.4.14
github.com/stretchr/testify v1.3.0
github.com/syndtr/goleveldb v1.0.0
go.uber.org/atomic v1.4.0 // indirect
go.uber.org/multierr v1.1.0 // indirect
go.uber.org/zap v1.10.0
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
golang.org/x/text v0.3.2
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/go-playground/validator.v9 v9.9.3
gopkg.in/go-playground/validator.v9 v9.29.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0
)

19
go.sum
View File

@ -27,6 +27,8 @@ github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb
github.com/aristanetworks/goarista v0.0.0-20170210015632-ea17b1a17847/go.mod h1:D/tb0zPVXnP7fmsLZjtdUhSsumbK/ij54UXjjVgMGxQ=
github.com/aristanetworks/goarista v0.0.0-20190502180301-283422fc1708 h1:tS7jSmwRqSxTnonTRlDD1oHo6Q9YOK4xHS9/v4L56eg=
github.com/aristanetworks/goarista v0.0.0-20190502180301-283422fc1708/go.mod h1:D/tb0zPVXnP7fmsLZjtdUhSsumbK/ij54UXjjVgMGxQ=
github.com/aristanetworks/goarista v0.0.0-20190704150520-f44d68189fd7 h1:fKnuvQ/O22ZpD7HaJjGQXn/GxOdDJOQFL8bpM8Xe3X8=
github.com/aristanetworks/goarista v0.0.0-20190704150520-f44d68189fd7/go.mod h1:D/tb0zPVXnP7fmsLZjtdUhSsumbK/ij54UXjjVgMGxQ=
github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/beevik/ntp v0.2.0 h1:sGsd+kAXzT0bfVfzJfce04g+dSRfrs+tbQW8lweuYgw=
github.com/beevik/ntp v0.2.0/go.mod h1:hIHWr+l3+/clUnF44zdK+CWW7fO8dR5cIylAQ76NRpg=
@ -75,6 +77,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deckarep/golang-set v0.0.0-20180603214616-504e848d77ea h1:j4317fAZh7X6GqbFowYdYdI0L9bwxL07jyPZIdepyZ0=
github.com/deckarep/golang-set v0.0.0-20180603214616-504e848d77ea/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ=
github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9rTHJQ=
github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ=
github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM=
github.com/dgraph-io/badger v1.5.5-0.20190226225317-8115aed38f8f/go.mod h1:VZxzAIRPHRVNRKRo6AXrX9BJegn6il06VMTZVJYCIjQ=
github.com/dgrijalva/jwt-go v0.0.0-20170201225849-2268707a8f08/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
@ -130,6 +134,8 @@ github.com/golang-migrate/migrate v3.5.4+incompatible h1:R7OzwvCJTCgwapPCiX6DyBi
github.com/golang-migrate/migrate v3.5.4+incompatible/go.mod h1:IsVUlFN5puWOmXrqjgGUfIRIbU7mr8oNBE2tyERd9Wk=
github.com/golang-migrate/migrate/v4 v4.4.0 h1:bQVSVUJCZkAf6ci1J4aUNFO0FyXVoncZvHEFCVD2ezE=
github.com/golang-migrate/migrate/v4 v4.4.0/go.mod h1:SzAcz2l+yDJVhQC7fwiF7T2MAFPMIkigJz98klRJ4OE=
github.com/golang-migrate/migrate/v4 v4.5.0 h1:ucd2qJu1BAKTtmjh7QlWYiq01DwlE/xXYQkOPE0ZTsI=
github.com/golang-migrate/migrate/v4 v4.5.0/go.mod h1:SzAcz2l+yDJVhQC7fwiF7T2MAFPMIkigJz98klRJ4OE=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
@ -234,6 +240,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kshvakov/clickhouse v1.3.5/go.mod h1:DMzX7FxRymoNkVgizH0DWAL8Cur7wHLgx3MUnGwJqpE=
github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/libp2p/go-addr-util v0.0.1 h1:TpTQm9cXVRVSKsYbgQ7GKc3KbbHVTnbostgGaDEP+88=
@ -405,6 +413,8 @@ github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRr
github.com/robertkrimen/otto v0.0.0-20170205013659-6a77b7cbc37d/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY=
github.com/rs/cors v0.0.0-20160617231935-a62a804a8a00 h1:8DPul/X0IT/1TNMIxoKLwdemEOBBHDC/K4EB16Cw5WE=
github.com/rs/cors v0.0.0-20160617231935-a62a804a8a00/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI=
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/rs/xhandler v0.0.0-20160618193221-ed27b6fd6521 h1:3hxavr+IHMsQBrYUPQM5v0CgENFktkkbg1sfpgM3h20=
github.com/rs/xhandler v0.0.0-20160618193221-ed27b6fd6521/go.mod h1:RvLn4FgxWubrpZHtQLnOf6EwhN2hEMusxZOhcW9H3UQ=
github.com/russolsen/ohyeah v0.0.0-20160324131710-f4938c005315 h1:H3hCXwP92pH/hSgNrCLtjxvsKJ50sq26nICbZuoR1tQ=
@ -438,6 +448,9 @@ github.com/status-im/migrate/v4 v4.3.1-status h1:tJwsEYLgbFkvlTSMk89APwRDfpr4yG8
github.com/status-im/migrate/v4 v4.3.1-status/go.mod h1:r8HggRBZ/k7TRwByq/Hp3P/ubFppIna0nvyavVK0pjA=
github.com/status-im/rendezvous v1.3.0 h1:7RK/MXXW+tlm0asKm1u7Qp7Yni6AO29a7j8+E4Lbjg4=
github.com/status-im/rendezvous v1.3.0/go.mod h1:+hzjuP+j/XzLPeF6E50b88pWOTLdTcwjvNYt+Gh1W1s=
github.com/status-im/status-go v0.29.0-beta.3/go.mod h1:8OHekmRoYTn9oZgMsBdhMEgeyfaQ4eI4ycOFHJCoX7M=
github.com/status-im/status-protocol-go v0.1.1 h1:PGKtMTgfk48LS2ZyNIEdJZJjQGVbkBfccf6EVYCjCpA=
github.com/status-im/status-protocol-go v0.1.1/go.mod h1:rqlsXFR7WENTB4HDJRByRg95UfRo8pjGWvrvVrC2Dm4=
github.com/status-im/whisper v1.4.14 h1:9VHqx4+PUYfhDnYYtDxHkg/3cfVvkHjPNciY4LO83yc=
github.com/status-im/whisper v1.4.14/go.mod h1:WS6z39YJQ8WJa9s+DmTuEM/s2nVF6Iz3B1SZYw5cYf0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -487,6 +500,8 @@ golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 h1:8dUaAV7K4uHsF56JQWkprecIQKdPHtR9jCHF5nB8uzc=
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@ -510,6 +525,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6 h1:FP8hkuE6yUEaJnK7O2eTuejKWwW+Rhfj80dQ2JcKxCU=
golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -580,6 +597,8 @@ gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXa
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v9 v9.9.3 h1:Jp2VeUxCViEo9Gst3entIFWiSkZH29YU9iRgl6ePzl4=
gopkg.in/go-playground/validator.v9 v9.9.3/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/go-playground/validator.v9 v9.29.0 h1:5ofssLNYgAA/inWn6rTZ4juWpRJUwEnXc1LG2IeXwgQ=
gopkg.in/go-playground/validator.v9 v9.29.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=

10
logutils/logger.go Normal file
View File

@ -0,0 +1,10 @@
package logutils
import (
"github.com/ethereum/go-ethereum/log"
)
// Logger returns the main logger instance used by status-go.
func Logger() log.Logger {
return log.Root()
}

View File

@ -3,9 +3,13 @@ package logutils
import (
"fmt"
"github.com/ethereum/go-ethereum/log"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"math"
"sync"
"time"
protocol "github.com/status-im/status-protocol-go"
)
type gethLoggerCore struct {
@ -113,3 +117,30 @@ func NewZapAdapter(logger log.Logger, enab zapcore.LevelEnabler) zapcore.Core {
logger: logger,
}
}
var registerOnce sync.Once
// NewZapLoggerWithAdapter returns a logger forwarding all logs with level info and above.
func NewZapLoggerWithAdapter(logger log.Logger) (*zap.Logger, error) {
registerOnce.Do(func() {
if err := protocol.RegisterJSONHexEncoder(); err != nil {
panic(err)
}
})
cfg := zap.Config{
Level: zap.NewAtomicLevelAt(zapcore.DebugLevel),
Development: false,
Sampling: nil,
Encoding: "json-hex",
EncoderConfig: zap.NewProductionEncoderConfig(),
OutputPaths: []string{"stderr"},
ErrorOutputPaths: []string{"stderr"},
}
adapter := zap.WrapCore(
func(zapcore.Core) zapcore.Core {
return NewZapAdapter(logger, cfg.Level)
},
)
return cfg.Build(adapter)
}

View File

@ -11,7 +11,6 @@ import (
func TestNewZapAdapter(t *testing.T) {
buf := bytes.NewBuffer(nil)
logger := log.New()
handler := log.StreamHandler(buf, log.LogfmtFormat())
logger.SetHandler(handler)
@ -40,3 +39,19 @@ func TestNewZapAdapter(t *testing.T) {
Info("some message with param")
require.Contains(t, buf.String(), `lvl=info msg="some message with param" namespace=some-namespace site=SomeSite`)
}
func TestNewZapLoggerWithAdapter(t *testing.T) {
buf := bytes.NewBuffer(nil)
logger := log.New()
handler := log.StreamHandler(buf, log.LogfmtFormat())
logger.SetHandler(handler)
zapLogger, err := NewZapLoggerWithAdapter(logger)
require.NoError(t, err)
buf.Reset()
zapLogger.
With(zap.Error(errors.New("some error"))).
Error("some message with error level")
require.Contains(t, buf.String(), `lvl=eror msg="some message with error level" error="some error`)
}

View File

@ -1,278 +0,0 @@
package chat
import (
"crypto/ecdsa"
"fmt"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/suite"
"os"
"testing"
"github.com/status-im/status-go/messaging/multidevice"
"github.com/status-im/status-go/messaging/sharedsecret"
)
const (
aliceUser = "alice"
bobUser = "bob"
)
func TestEncryptionServiceMultiDeviceSuite(t *testing.T) {
suite.Run(t, new(EncryptionServiceMultiDeviceSuite))
}
type serviceAndKey struct {
services []*ProtocolService
key *ecdsa.PrivateKey
}
type EncryptionServiceMultiDeviceSuite struct {
suite.Suite
services map[string]*serviceAndKey
}
func setupUser(user string, s *EncryptionServiceMultiDeviceSuite, n int) error {
key, err := crypto.GenerateKey()
if err != nil {
return err
}
s.services[user] = &serviceAndKey{
key: key,
services: make([]*ProtocolService, n),
}
for i := 0; i < n; i++ {
installationID := fmt.Sprintf("%s%d", user, i+1)
dbPath := fmt.Sprintf("/tmp/%s.db", installationID)
os.Remove(dbPath)
persistence, err := NewSQLLitePersistence(dbPath, "key")
if err != nil {
return err
}
// Initialize sharedsecret
multideviceConfig := &multidevice.Config{
MaxInstallations: n - 1,
InstallationID: installationID,
ProtocolVersion: 1,
}
sharedSecretService := sharedsecret.NewService(persistence.GetSharedSecretStorage())
multideviceService := multidevice.New(multideviceConfig, persistence.GetMultideviceStorage())
protocol := NewProtocolService(
NewEncryptionService(
persistence,
DefaultEncryptionServiceConfig(installationID)),
sharedSecretService,
multideviceService,
func(s []*multidevice.Installation) {},
func(s []*sharedsecret.Secret) {},
)
s.services[user].services[i] = protocol
}
return nil
}
func (s *EncryptionServiceMultiDeviceSuite) SetupTest() {
s.services = make(map[string]*serviceAndKey)
err := setupUser(aliceUser, s, 4)
s.Require().NoError(err)
err = setupUser(bobUser, s, 4)
s.Require().NoError(err)
}
func (s *EncryptionServiceMultiDeviceSuite) TestProcessPublicBundle() {
aliceKey := s.services[aliceUser].key
alice2Bundle, err := s.services[aliceUser].services[1].GetBundle(aliceKey)
s.Require().NoError(err)
alice2IdentityPK, err := ExtractIdentity(alice2Bundle)
s.Require().NoError(err)
alice2Identity := fmt.Sprintf("0x%x", crypto.FromECDSAPub(alice2IdentityPK))
alice3Bundle, err := s.services[aliceUser].services[2].GetBundle(aliceKey)
s.Require().NoError(err)
alice3IdentityPK, err := ExtractIdentity(alice2Bundle)
s.Require().NoError(err)
alice3Identity := fmt.Sprintf("0x%x", crypto.FromECDSAPub(alice3IdentityPK))
// Add alice2 bundle
response, err := s.services[aliceUser].services[0].ProcessPublicBundle(aliceKey, alice2Bundle)
s.Require().NoError(err)
s.Require().Equal(multidevice.Installation{
Identity: alice2Identity,
Version: 1,
ID: "alice2",
}, *response[0])
// Add alice3 bundle
response, err = s.services[aliceUser].services[0].ProcessPublicBundle(aliceKey, alice3Bundle)
s.Require().NoError(err)
s.Require().Equal(multidevice.Installation{
Identity: alice3Identity,
Version: 1,
ID: "alice3",
}, *response[0])
// No installation is enabled
alice1MergedBundle1, err := s.services[aliceUser].services[0].GetBundle(aliceKey)
s.Require().NoError(err)
s.Require().Equal(1, len(alice1MergedBundle1.GetSignedPreKeys()))
s.Require().NotNil(alice1MergedBundle1.GetSignedPreKeys()["alice1"])
// We enable the installations
err = s.services[aliceUser].services[0].EnableInstallation(&aliceKey.PublicKey, "alice2")
s.Require().NoError(err)
err = s.services[aliceUser].services[0].EnableInstallation(&aliceKey.PublicKey, "alice3")
s.Require().NoError(err)
alice1MergedBundle2, err := s.services[aliceUser].services[0].GetBundle(aliceKey)
s.Require().NoError(err)
// We get back a bundle with all the installations
s.Require().Equal(3, len(alice1MergedBundle2.GetSignedPreKeys()))
s.Require().NotNil(alice1MergedBundle2.GetSignedPreKeys()["alice1"])
s.Require().NotNil(alice1MergedBundle2.GetSignedPreKeys()["alice2"])
s.Require().NotNil(alice1MergedBundle2.GetSignedPreKeys()["alice3"])
// We disable the installations
err = s.services[aliceUser].services[0].DisableInstallation(&aliceKey.PublicKey, "alice2")
s.Require().NoError(err)
alice1MergedBundle3, err := s.services[aliceUser].services[0].GetBundle(aliceKey)
s.Require().NoError(err)
// We get back a bundle with all the installations
s.Require().Equal(2, len(alice1MergedBundle3.GetSignedPreKeys()))
s.Require().NotNil(alice1MergedBundle3.GetSignedPreKeys()["alice1"])
s.Require().NotNil(alice1MergedBundle3.GetSignedPreKeys()["alice3"])
}
func (s *EncryptionServiceMultiDeviceSuite) TestProcessPublicBundleOutOfOrder() {
aliceKey, err := crypto.GenerateKey()
s.Require().NoError(err)
// Alice1 creates a bundle
alice1Bundle, err := s.services[aliceUser].services[0].GetBundle(aliceKey)
s.Require().NoError(err)
// Alice2 Receives the bundle
_, err = s.services[aliceUser].services[1].ProcessPublicBundle(aliceKey, alice1Bundle)
s.Require().NoError(err)
// Alice2 Creates a Bundle
_, err = s.services[aliceUser].services[1].GetBundle(aliceKey)
s.Require().NoError(err)
// We enable the installation
err = s.services[aliceUser].services[1].EnableInstallation(&aliceKey.PublicKey, "alice1")
s.Require().NoError(err)
// It should contain both bundles
alice2MergedBundle1, err := s.services[aliceUser].services[1].GetBundle(aliceKey)
s.Require().NoError(err)
s.Require().NotNil(alice2MergedBundle1.GetSignedPreKeys()["alice1"])
s.Require().NotNil(alice2MergedBundle1.GetSignedPreKeys()["alice2"])
}
func pairDevices(s *serviceAndKey, target int) error {
device := s.services[target]
for i := 0; i < len(s.services); i++ {
b, err := s.services[i].GetBundle(s.key)
if err != nil {
return err
}
_, err = device.ProcessPublicBundle(s.key, b)
if err != nil {
return err
}
err = device.EnableInstallation(&s.key.PublicKey, s.services[i].encryption.config.InstallationID)
if err != nil {
return nil
}
}
return nil
}
func (s *EncryptionServiceMultiDeviceSuite) TestMaxDevices() {
err := pairDevices(s.services[aliceUser], 0)
s.Require().NoError(err)
alice1 := s.services[aliceUser].services[0]
bob1 := s.services[bobUser].services[0]
aliceKey := s.services[aliceUser].key
bobKey := s.services[bobUser].key
// Check bundle is ok
// No installation is enabled
aliceBundle, err := alice1.GetBundle(aliceKey)
s.Require().NoError(err)
// Check all installations are correctly working, and that the oldest device is not there
preKeys := aliceBundle.GetSignedPreKeys()
s.Require().Equal(3, len(preKeys))
s.Require().NotNil(preKeys["alice1"])
// alice2 being the oldest device is rotated out, as we reached the maximum
s.Require().Nil(preKeys["alice2"])
s.Require().NotNil(preKeys["alice3"])
s.Require().NotNil(preKeys["alice4"])
// We propagate this to bob
_, err = bob1.ProcessPublicBundle(bobKey, aliceBundle)
s.Require().NoError(err)
// Bob sends a message to alice
msg, err := bob1.BuildDirectMessage(bobKey, &aliceKey.PublicKey, []byte("test"))
s.Require().NoError(err)
payload := msg.Message.GetDirectMessage()
s.Require().Equal(3, len(payload))
s.Require().NotNil(payload["alice1"])
s.Require().NotNil(payload["alice3"])
s.Require().NotNil(payload["alice4"])
// We disable the last installation
err = s.services[aliceUser].services[0].DisableInstallation(&aliceKey.PublicKey, "alice4")
s.Require().NoError(err)
// We check the bundle is updated
aliceBundle, err = alice1.GetBundle(aliceKey)
s.Require().NoError(err)
// Check all installations are there
preKeys = aliceBundle.GetSignedPreKeys()
s.Require().Equal(3, len(preKeys))
s.Require().NotNil(preKeys["alice1"])
s.Require().NotNil(preKeys["alice2"])
s.Require().NotNil(preKeys["alice3"])
// alice4 is disabled at this point, alice2 is back in
s.Require().Nil(preKeys["alice4"])
// We propagate this to bob
_, err = bob1.ProcessPublicBundle(bobKey, aliceBundle)
s.Require().NoError(err)
// Bob sends a message to alice
msg, err = bob1.BuildDirectMessage(bobKey, &aliceKey.PublicKey, []byte("test"))
s.Require().NoError(err)
payload = msg.Message.GetDirectMessage()
s.Require().Equal(3, len(payload))
s.Require().NotNil(payload["alice1"])
s.Require().NotNil(payload["alice2"])
s.Require().NotNil(payload["alice3"])
}

File diff suppressed because it is too large Load Diff

View File

@ -1,53 +0,0 @@
package chat
import (
"crypto/ecdsa"
dr "github.com/status-im/doubleratchet"
"github.com/status-im/status-go/messaging/chat/protobuf"
"github.com/status-im/status-go/messaging/multidevice"
)
// RatchetInfo holds the current ratchet state
type RatchetInfo struct {
ID []byte
Sk []byte
PrivateKey []byte
PublicKey []byte
Identity []byte
BundleID []byte
EphemeralKey []byte
InstallationID string
}
// Persistence defines the interface for a storage service
type Persistence interface {
// GetKeysStorage returns the associated double ratchet KeysStorage object.
GetKeysStorage() dr.KeysStorage
// GetSessionStorage returns the associated double ratchet SessionStorage object.
GetSessionStorage() dr.SessionStorage
// GetPublicBundle retrieves an existing Bundle for the specified public key & installations
GetPublicBundle(*ecdsa.PublicKey, []*multidevice.Installation) (*protobuf.Bundle, error)
// AddPublicBundle persists a specified Bundle
AddPublicBundle(*protobuf.Bundle) error
// GetAnyPrivateBundle retrieves any bundle for our identity & installations
GetAnyPrivateBundle([]byte, []*multidevice.Installation) (*protobuf.BundleContainer, error)
// GetPrivateKeyBundle retrieves a BundleContainer with the specified signed prekey.
GetPrivateKeyBundle([]byte) ([]byte, error)
// AddPrivateBundle persists a BundleContainer.
AddPrivateBundle(*protobuf.BundleContainer) error
// MarkBundleExpired marks a private bundle as expired, not to be used for encryption anymore.
MarkBundleExpired([]byte) error
// AddRatchetInfo persists the specified ratchet info
AddRatchetInfo([]byte, []byte, []byte, []byte, string) error
// GetRatchetInfo retrieves the existing RatchetInfo for a specified bundle ID and interlocutor public key.
GetRatchetInfo([]byte, []byte, string) (*RatchetInfo, error)
// GetAnyRatchetInfo retrieves any existing RatchetInfo for a specified interlocutor public key.
GetAnyRatchetInfo([]byte, string) (*RatchetInfo, error)
// RatchetInfoConfirmed clears the ephemeral key in the RatchetInfo
// associated with the specified bundle ID and interlocutor identity public key.
RatchetInfoConfirmed([]byte, []byte, string) error
}

View File

@ -1,386 +0,0 @@
package chat
import (
"crypto/ecdsa"
"errors"
"fmt"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/messaging/chat/protobuf"
"github.com/status-im/status-go/messaging/multidevice"
"github.com/status-im/status-go/messaging/sharedsecret"
)
const ProtocolVersion = 1
const sharedSecretNegotiationVersion = 1
const partitionedTopicMinVersion = 1
const defaultMinVersion = 0
type PartitionTopic int
const (
PartitionTopicNoSupport PartitionTopic = iota
PartitionTopicV1
)
type ProtocolService struct {
log log.Logger
encryption *EncryptionService
secret *sharedsecret.Service
multidevice *multidevice.Service
addedBundlesHandler func([]*multidevice.Installation)
onNewSharedSecretHandler func([]*sharedsecret.Secret)
Enabled bool
}
var (
ErrNotProtocolMessage = errors.New("not a protocol message")
ErrNoPayload = errors.New("no payload")
)
// NewProtocolService creates a new ProtocolService instance
func NewProtocolService(encryption *EncryptionService, secret *sharedsecret.Service, multidevice *multidevice.Service, addedBundlesHandler func([]*multidevice.Installation), onNewSharedSecretHandler func([]*sharedsecret.Secret)) *ProtocolService {
return &ProtocolService{
log: log.New("package", "status-go/services/sshext.chat"),
encryption: encryption,
secret: secret,
multidevice: multidevice,
addedBundlesHandler: addedBundlesHandler,
onNewSharedSecretHandler: onNewSharedSecretHandler,
}
}
func (p *ProtocolService) addBundle(myIdentityKey *ecdsa.PrivateKey, msg *protobuf.ProtocolMessage, sendSingle bool) (*protobuf.ProtocolMessage, error) {
// Get a bundle
installations, err := p.multidevice.GetOurActiveInstallations(&myIdentityKey.PublicKey)
if err != nil {
return nil, err
}
bundle, err := p.encryption.CreateBundle(myIdentityKey, installations)
if err != nil {
p.log.Error("encryption-service", "error creating bundle", err)
return nil, err
}
if sendSingle {
// DEPRECATED: This is only for backward compatibility, remove once not
// an issue anymore
msg.Bundle = bundle
} else {
msg.Bundles = []*protobuf.Bundle{bundle}
}
return msg, nil
}
// BuildPublicMessage marshals a public chat message given the user identity private key and a payload
func (p *ProtocolService) BuildPublicMessage(myIdentityKey *ecdsa.PrivateKey, payload []byte) (*protobuf.ProtocolMessage, error) {
// Build message not encrypted
protocolMessage := &protobuf.ProtocolMessage{
InstallationId: p.encryption.config.InstallationID,
PublicMessage: payload,
}
return p.addBundle(myIdentityKey, protocolMessage, false)
}
type ProtocolMessageSpec struct {
Message *protobuf.ProtocolMessage
// Installations is the targeted devices
Installations []*multidevice.Installation
// SharedSecret is a shared secret established among the installations
SharedSecret []byte
}
func (p *ProtocolMessageSpec) MinVersion() uint32 {
if len(p.Installations) == 0 {
return defaultMinVersion
}
version := p.Installations[0].Version
for _, installation := range p.Installations[1:] {
if installation.Version < version {
version = installation.Version
}
}
return version
}
func (p *ProtocolMessageSpec) PartitionedTopic() PartitionTopic {
if p.MinVersion() >= partitionedTopicMinVersion {
return PartitionTopicV1
}
return PartitionTopicNoSupport
}
// BuildDirectMessage returns a 1:1 chat message and optionally a negotiated topic given the user identity private key, the recipient's public key, and a payload
func (p *ProtocolService) BuildDirectMessage(myIdentityKey *ecdsa.PrivateKey, publicKey *ecdsa.PublicKey, payload []byte) (*ProtocolMessageSpec, error) {
activeInstallations, err := p.multidevice.GetActiveInstallations(publicKey)
if err != nil {
return nil, err
}
// Encrypt payload
encryptionResponse, installations, err := p.encryption.EncryptPayload(publicKey, myIdentityKey, activeInstallations, payload)
if err != nil {
p.log.Error("encryption-service", "error encrypting payload", err)
return nil, err
}
// Build message
protocolMessage := &protobuf.ProtocolMessage{
InstallationId: p.encryption.config.InstallationID,
DirectMessage: encryptionResponse,
}
msg, err := p.addBundle(myIdentityKey, protocolMessage, true)
if err != nil {
return nil, err
}
// Check who we are sending the message to, and see if we have a shared secret
// across devices
var installationIDs []string
var sharedSecret *sharedsecret.Secret
var agreed bool
for installationID := range protocolMessage.GetDirectMessage() {
if installationID != noInstallationID {
installationIDs = append(installationIDs, installationID)
}
}
sharedSecret, agreed, err = p.secret.Send(myIdentityKey, p.encryption.config.InstallationID, publicKey, installationIDs)
if err != nil {
return nil, err
}
// Call handler
if sharedSecret != nil {
p.onNewSharedSecretHandler([]*sharedsecret.Secret{sharedSecret})
}
response := &ProtocolMessageSpec{
Message: msg,
Installations: installations,
}
if agreed {
response.SharedSecret = sharedSecret.Key
}
return response, nil
}
// BuildDHMessage builds a message with DH encryption so that it can be decrypted by any other device.
func (p *ProtocolService) BuildDHMessage(myIdentityKey *ecdsa.PrivateKey, destination *ecdsa.PublicKey, payload []byte) (*ProtocolMessageSpec, error) {
// Encrypt payload
encryptionResponse, err := p.encryption.EncryptPayloadWithDH(destination, payload)
if err != nil {
p.log.Error("encryption-service", "error encrypting payload", err)
return nil, err
}
// Build message
protocolMessage := &protobuf.ProtocolMessage{
InstallationId: p.encryption.config.InstallationID,
DirectMessage: encryptionResponse,
}
msg, err := p.addBundle(myIdentityKey, protocolMessage, true)
if err != nil {
return nil, err
}
return &ProtocolMessageSpec{Message: msg}, nil
}
// ProcessPublicBundle processes a received X3DH bundle.
func (p *ProtocolService) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey, bundle *protobuf.Bundle) ([]*multidevice.Installation, error) {
p.log.Debug("Processing bundle", "bundle", bundle)
if err := p.encryption.ProcessPublicBundle(myIdentityKey, bundle); err != nil {
return nil, err
}
installations, fromOurs, err := p.recoverInstallationsFromBundle(myIdentityKey, bundle)
if err != nil {
return nil, err
}
// TODO(adam): why do we add installations using identity obtained from GetIdentity()
// instead of the output of crypto.CompressPubkey()? I tried the second option
// and the unit tests TestTopic and TestMaxDevices fail.
return p.multidevice.AddInstallations(bundle.GetIdentity(), bundle.GetTimestamp(), installations, fromOurs)
}
// recoverInstallationsFromBundle extracts installations from the bundle.
// It returns extracted installations and true if the installations
// are ours, i.e. the bundle was created by our identity key.
func (p *ProtocolService) recoverInstallationsFromBundle(myIdentityKey *ecdsa.PrivateKey, bundle *protobuf.Bundle) ([]*multidevice.Installation, bool, error) {
var installations []*multidevice.Installation
theirIdentity, err := ExtractIdentity(bundle)
if err != nil {
return nil, false, err
}
myIdentityStr := fmt.Sprintf("0x%x", crypto.FromECDSAPub(&myIdentityKey.PublicKey))
theirIdentityStr := fmt.Sprintf("0x%x", crypto.FromECDSAPub(theirIdentity))
// Any device from other peers will be considered enabled, ours needs to
// be explicitly enabled
fromOurIdentity := theirIdentityStr != myIdentityStr
signedPreKeys := bundle.GetSignedPreKeys()
for installationID, signedPreKey := range signedPreKeys {
if installationID != p.multidevice.InstallationID() {
installations = append(installations, &multidevice.Installation{
Identity: theirIdentityStr,
ID: installationID,
Version: signedPreKey.GetProtocolVersion(),
})
}
}
return installations, fromOurIdentity, nil
}
// GetBundle retrieves or creates a X3DH bundle, given a private identity key.
func (p *ProtocolService) GetBundle(myIdentityKey *ecdsa.PrivateKey) (*protobuf.Bundle, error) {
installations, err := p.multidevice.GetOurActiveInstallations(&myIdentityKey.PublicKey)
if err != nil {
return nil, err
}
return p.encryption.CreateBundle(myIdentityKey, installations)
}
// EnableInstallation enables an installation for multi-device sync.
func (p *ProtocolService) EnableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error {
return p.multidevice.EnableInstallation(myIdentityKey, installationID)
}
// DisableInstallation disables an installation for multi-device sync.
func (p *ProtocolService) DisableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error {
return p.multidevice.DisableInstallation(myIdentityKey, installationID)
}
// GetOurInstallations returns all the installations available given an identity
func (p *ProtocolService) GetOurInstallations(myIdentityKey *ecdsa.PublicKey) ([]*multidevice.Installation, error) {
return p.multidevice.GetOurInstallations(myIdentityKey)
}
// SetInstallationMetadata sets the metadata for our own installation
func (p *ProtocolService) SetInstallationMetadata(myIdentityKey *ecdsa.PublicKey, installationID string, data *multidevice.InstallationMetadata) error {
return p.multidevice.SetInstallationMetadata(myIdentityKey, installationID, data)
}
// GetPublicBundle retrieves a public bundle given an identity
func (p *ProtocolService) GetPublicBundle(theirIdentityKey *ecdsa.PublicKey) (*protobuf.Bundle, error) {
installations, err := p.multidevice.GetActiveInstallations(theirIdentityKey)
if err != nil {
return nil, err
}
return p.encryption.GetPublicBundle(theirIdentityKey, installations)
}
// ConfirmMessagesProcessed confirms and deletes message keys for the given messages
func (p *ProtocolService) ConfirmMessagesProcessed(messageIDs [][]byte) error {
return p.encryption.ConfirmMessagesProcessed(messageIDs)
}
func (p *ProtocolService) GetSharedSecretService() *sharedsecret.Service {
return p.secret
}
// HandleMessage unmarshals a message and processes it, decrypting it if it is a 1:1 message.
func (p *ProtocolService) HandleMessage(myIdentityKey *ecdsa.PrivateKey, theirPublicKey *ecdsa.PublicKey, protocolMessage *protobuf.ProtocolMessage, messageID []byte) ([]byte, error) {
p.log.Debug("Received message from", "public-key", theirPublicKey)
if p.encryption == nil {
return nil, errors.New("encryption service not initialized")
}
// Process bundle, deprecated, here for backward compatibility
if bundle := protocolMessage.GetBundle(); bundle != nil {
// Should we stop processing if the bundle cannot be verified?
addedBundles, err := p.ProcessPublicBundle(myIdentityKey, bundle)
if err != nil {
return nil, err
}
p.addedBundlesHandler(addedBundles)
}
// Process bundles
for _, bundle := range protocolMessage.GetBundles() {
// Should we stop processing if the bundle cannot be verified?
addedBundles, err := p.ProcessPublicBundle(myIdentityKey, bundle)
if err != nil {
return nil, err
}
p.addedBundlesHandler(addedBundles)
}
// Check if it's a public message
if publicMessage := protocolMessage.GetPublicMessage(); publicMessage != nil {
p.log.Debug("Public message, nothing to do")
// Nothing to do, as already in cleartext
return publicMessage, nil
}
// Decrypt message
if directMessage := protocolMessage.GetDirectMessage(); directMessage != nil {
p.log.Debug("Processing direct message")
message, err := p.encryption.DecryptPayload(myIdentityKey, theirPublicKey, protocolMessage.GetInstallationId(), directMessage, messageID)
if err != nil {
return nil, err
}
// Handle protocol negotiation for compatible clients
bundles := append(protocolMessage.GetBundles(), protocolMessage.GetBundle())
version := getProtocolVersion(bundles, protocolMessage.GetInstallationId())
p.log.Debug("Message version is", "version", version)
if version >= sharedSecretNegotiationVersion {
p.log.Debug("Negotiating shared secret")
sharedSecret, err := p.secret.Receive(myIdentityKey, theirPublicKey, protocolMessage.GetInstallationId())
if err != nil {
return nil, err
}
p.onNewSharedSecretHandler([]*sharedsecret.Secret{sharedSecret})
}
return message, nil
}
// Return error
return nil, ErrNoPayload
}
func getProtocolVersion(bundles []*protobuf.Bundle, installationID string) uint32 {
if installationID == "" {
return defaultMinVersion
}
for _, bundle := range bundles {
if bundle != nil {
signedPreKeys := bundle.GetSignedPreKeys()
if signedPreKeys == nil {
continue
}
signedPreKey := signedPreKeys[installationID]
if signedPreKey == nil {
return defaultMinVersion
}
return signedPreKey.GetProtocolVersion()
}
}
return defaultMinVersion
}

View File

@ -1,171 +0,0 @@
package chat
import (
"os"
"testing"
"github.com/ethereum/go-ethereum/crypto"
"github.com/status-im/status-go/messaging/multidevice"
"github.com/status-im/status-go/messaging/sharedsecret"
"github.com/stretchr/testify/suite"
)
func TestProtocolServiceTestSuite(t *testing.T) {
suite.Run(t, new(ProtocolServiceTestSuite))
}
type ProtocolServiceTestSuite struct {
suite.Suite
alice *ProtocolService
bob *ProtocolService
}
func (s *ProtocolServiceTestSuite) SetupTest() {
aliceDBPath := "/tmp/alice.db"
aliceDBKey := "alice"
bobDBPath := "/tmp/bob.db"
bobDBKey := "bob"
os.Remove(aliceDBPath)
os.Remove(bobDBPath)
alicePersistence, err := NewSQLLitePersistence(aliceDBPath, aliceDBKey)
if err != nil {
panic(err)
}
bobPersistence, err := NewSQLLitePersistence(bobDBPath, bobDBKey)
if err != nil {
panic(err)
}
addedBundlesHandler := func(addedBundles []*multidevice.Installation) {}
onNewSharedSecretHandler := func(secret []*sharedsecret.Secret) {}
aliceMultideviceConfig := &multidevice.Config{
MaxInstallations: 3,
InstallationID: "1",
ProtocolVersion: ProtocolVersion,
}
s.alice = NewProtocolService(
NewEncryptionService(alicePersistence, DefaultEncryptionServiceConfig("1")),
sharedsecret.NewService(alicePersistence.GetSharedSecretStorage()),
multidevice.New(aliceMultideviceConfig, alicePersistence.GetMultideviceStorage()),
addedBundlesHandler,
onNewSharedSecretHandler,
)
bobMultideviceConfig := &multidevice.Config{
MaxInstallations: 3,
InstallationID: "2",
ProtocolVersion: ProtocolVersion,
}
s.bob = NewProtocolService(
NewEncryptionService(bobPersistence, DefaultEncryptionServiceConfig("2")),
sharedsecret.NewService(bobPersistence.GetSharedSecretStorage()),
multidevice.New(bobMultideviceConfig, bobPersistence.GetMultideviceStorage()),
addedBundlesHandler,
onNewSharedSecretHandler,
)
}
func (s *ProtocolServiceTestSuite) TestBuildPublicMessage() {
aliceKey, err := crypto.GenerateKey()
s.NoError(err)
payload := []byte("test")
s.NoError(err)
msg, err := s.alice.BuildPublicMessage(aliceKey, payload)
s.NoError(err)
s.NotNil(msg, "It creates a message")
s.NotNilf(msg.GetBundles(), "It adds a bundle to the message")
}
func (s *ProtocolServiceTestSuite) TestBuildDirectMessage() {
bobKey, err := crypto.GenerateKey()
s.NoError(err)
aliceKey, err := crypto.GenerateKey()
s.NoError(err)
payload := []byte("test")
msgSpec, err := s.alice.BuildDirectMessage(aliceKey, &bobKey.PublicKey, payload)
s.NoError(err)
s.NotNil(msgSpec, "It creates a message spec")
msg := msgSpec.Message
s.NotNil(msg, "It creates a messages")
s.NotNilf(msg.GetBundle(), "It adds a bundle to the message")
directMessage := msg.GetDirectMessage()
s.NotNilf(directMessage, "It sets the direct message")
encryptedPayload := directMessage["none"].GetPayload()
s.NotNilf(encryptedPayload, "It sets the payload of the message")
s.NotEqualf(payload, encryptedPayload, "It encrypts the payload")
}
func (s *ProtocolServiceTestSuite) TestBuildAndReadDirectMessage() {
bobKey, err := crypto.GenerateKey()
s.Require().NoError(err)
aliceKey, err := crypto.GenerateKey()
s.Require().NoError(err)
payload := []byte("test")
// Message is sent with DH
msgSpec, err := s.alice.BuildDirectMessage(aliceKey, &bobKey.PublicKey, payload)
s.Require().NoError(err)
s.Require().NotNil(msgSpec)
msg := msgSpec.Message
s.Require().NotNil(msg)
// Bob is able to decrypt the message
unmarshaledMsg, err := s.bob.HandleMessage(bobKey, &aliceKey.PublicKey, msg, []byte("message-id"))
s.NoError(err)
s.NotNil(unmarshaledMsg)
recoveredPayload := []byte("test")
s.Equalf(payload, recoveredPayload, "It successfully unmarshal the decrypted message")
}
func (s *ProtocolServiceTestSuite) TestSecretNegotiation() {
var secretResponse []*sharedsecret.Secret
bobKey, err := crypto.GenerateKey()
s.NoError(err)
aliceKey, err := crypto.GenerateKey()
s.NoError(err)
payload := []byte("test")
s.bob.onNewSharedSecretHandler = func(secret []*sharedsecret.Secret) {
secretResponse = secret
}
msgSpec, err := s.alice.BuildDirectMessage(aliceKey, &bobKey.PublicKey, payload)
s.NoError(err)
s.NotNil(msgSpec, "It creates a message spec")
bundle := msgSpec.Message.GetBundle()
s.Require().NotNil(bundle)
signedPreKeys := bundle.GetSignedPreKeys()
s.Require().NotNil(signedPreKeys)
signedPreKey := signedPreKeys["1"]
s.Require().NotNil(signedPreKey)
s.Require().Equal(uint32(1), signedPreKey.GetProtocolVersion())
_, err = s.bob.HandleMessage(bobKey, &aliceKey.PublicKey, msgSpec.Message, []byte("message-id"))
s.NoError(err)
s.Require().NotNil(secretResponse)
}

View File

@ -1,200 +0,0 @@
package chat
import (
dr "github.com/status-im/doubleratchet"
"github.com/stretchr/testify/suite"
"os"
"testing"
)
var (
pubKey1 = dr.Key{0xe3, 0xbe, 0xb9, 0x4e, 0x70, 0x17, 0x37, 0xc, 0x1, 0x8f, 0xa9, 0x7e, 0xef, 0x4, 0xfb, 0x23, 0xac, 0xea, 0x28, 0xf7, 0xa9, 0x56, 0xcc, 0x1d, 0x46, 0xf3, 0xb5, 0x1d, 0x7d, 0x7d, 0x5e, 0x2c}
pubKey2 = dr.Key{0xec, 0x8, 0x10, 0x7c, 0x33, 0x54, 0x0, 0x20, 0xe9, 0x4f, 0x6c, 0x84, 0xe4, 0x39, 0x50, 0x5a, 0x2f, 0x60, 0xbe, 0x81, 0xa, 0x78, 0x8b, 0xeb, 0x1e, 0x2c, 0x9, 0x8d, 0x4b, 0x4d, 0xc1, 0x40}
mk1 = dr.Key{0x00, 0x8, 0x10, 0x7c, 0x33, 0x54, 0x0, 0x20, 0xe9, 0x4f, 0x6c, 0x84, 0xe4, 0x39, 0x50, 0x5a, 0x2f, 0x60, 0xbe, 0x81, 0xa, 0x78, 0x8b, 0xeb, 0x1e, 0x2c, 0x9, 0x8d, 0x4b, 0x4d, 0xc1, 0x40}
mk2 = dr.Key{0x01, 0x8, 0x10, 0x7c, 0x33, 0x54, 0x0, 0x20, 0xe9, 0x4f, 0x6c, 0x84, 0xe4, 0x39, 0x50, 0x5a, 0x2f, 0x60, 0xbe, 0x81, 0xa, 0x78, 0x8b, 0xeb, 0x1e, 0x2c, 0x9, 0x8d, 0x4b, 0x4d, 0xc1, 0x40}
mk3 = dr.Key{0x02, 0x8, 0x10, 0x7c, 0x33, 0x54, 0x0, 0x20, 0xe9, 0x4f, 0x6c, 0x84, 0xe4, 0x39, 0x50, 0x5a, 0x2f, 0x60, 0xbe, 0x81, 0xa, 0x78, 0x8b, 0xeb, 0x1e, 0x2c, 0x9, 0x8d, 0x4b, 0x4d, 0xc1, 0x40}
mk4 = dr.Key{0x03, 0x8, 0x10, 0x7c, 0x33, 0x54, 0x0, 0x20, 0xe9, 0x4f, 0x6c, 0x84, 0xe4, 0x39, 0x50, 0x5a, 0x2f, 0x60, 0xbe, 0x81, 0xa, 0x78, 0x8b, 0xeb, 0x1e, 0x2c, 0x9, 0x8d, 0x4b, 0x4d, 0xc1, 0x40}
mk5 = dr.Key{0x04, 0x8, 0x10, 0x7c, 0x33, 0x54, 0x0, 0x20, 0xe9, 0x4f, 0x6c, 0x84, 0xe4, 0x39, 0x50, 0x5a, 0x2f, 0x60, 0xbe, 0x81, 0xa, 0x78, 0x8b, 0xeb, 0x1e, 0x2c, 0x9, 0x8d, 0x4b, 0x4d, 0xc1, 0x40}
)
func TestSQLLitePersistenceKeysStorageTestSuite(t *testing.T) {
suite.Run(t, new(SQLLitePersistenceKeysStorageTestSuite))
}
type SQLLitePersistenceKeysStorageTestSuite struct {
suite.Suite
service dr.KeysStorage
}
func (s *SQLLitePersistenceKeysStorageTestSuite) SetupTest() {
dbPath := "/tmp/status-key-store.db"
key := "blahblahblah"
os.Remove(dbPath)
p, err := NewSQLLitePersistence(dbPath, key)
if err != nil {
panic(err)
}
s.service = p.GetKeysStorage()
}
func (s *SQLLitePersistenceKeysStorageTestSuite) TestKeysStorageSqlLiteGetMissing() {
// Act.
_, ok, err := s.service.Get(pubKey1, 0)
// Assert.
s.NoError(err)
s.False(ok, "It returns false")
}
func (s *SQLLitePersistenceKeysStorageTestSuite) TestKeysStorageSqlLite_Put() {
// Act and assert.
err := s.service.Put([]byte("session-id"), pubKey1, 0, mk1, 1)
s.NoError(err)
}
func (s *SQLLitePersistenceKeysStorageTestSuite) TestKeysStorageSqlLite_DeleteOldMks() {
// Insert keys out-of-order
err := s.service.Put([]byte("session-id"), pubKey1, 0, mk1, 1)
s.NoError(err)
err = s.service.Put([]byte("session-id"), pubKey1, 1, mk2, 2)
s.NoError(err)
err = s.service.Put([]byte("session-id"), pubKey1, 2, mk3, 20)
s.NoError(err)
err = s.service.Put([]byte("session-id"), pubKey1, 3, mk4, 21)
s.NoError(err)
err = s.service.Put([]byte("session-id"), pubKey1, 4, mk5, 22)
s.NoError(err)
err = s.service.DeleteOldMks([]byte("session-id"), 20)
s.NoError(err)
_, ok, err := s.service.Get(pubKey1, 0)
s.NoError(err)
s.False(ok)
_, ok, err = s.service.Get(pubKey1, 1)
s.NoError(err)
s.False(ok)
_, ok, err = s.service.Get(pubKey1, 2)
s.NoError(err)
s.False(ok)
_, ok, err = s.service.Get(pubKey1, 3)
s.NoError(err)
s.True(ok)
_, ok, err = s.service.Get(pubKey1, 4)
s.NoError(err)
s.True(ok)
}
func (s *SQLLitePersistenceKeysStorageTestSuite) TestKeysStorageSqlLite_TruncateMks() {
// Insert keys out-of-order
err := s.service.Put([]byte("session-id"), pubKey2, 2, mk5, 5)
s.NoError(err)
err = s.service.Put([]byte("session-id"), pubKey2, 0, mk3, 3)
s.NoError(err)
err = s.service.Put([]byte("session-id"), pubKey1, 1, mk2, 2)
s.NoError(err)
err = s.service.Put([]byte("session-id"), pubKey2, 1, mk4, 4)
s.NoError(err)
err = s.service.Put([]byte("session-id"), pubKey1, 0, mk1, 1)
s.NoError(err)
err = s.service.TruncateMks([]byte("session-id"), 2)
s.NoError(err)
_, ok, err := s.service.Get(pubKey1, 0)
s.NoError(err)
s.False(ok)
_, ok, err = s.service.Get(pubKey1, 1)
s.NoError(err)
s.False(ok)
_, ok, err = s.service.Get(pubKey2, 0)
s.NoError(err)
s.False(ok)
_, ok, err = s.service.Get(pubKey2, 1)
s.NoError(err)
s.True(ok)
_, ok, err = s.service.Get(pubKey2, 2)
s.NoError(err)
s.True(ok)
}
func (s *SQLLitePersistenceKeysStorageTestSuite) TestKeysStorageSqlLite_Count() {
// Act.
cnt, err := s.service.Count(pubKey1)
// Assert.
s.NoError(err)
s.EqualValues(0, cnt, "It returns 0 when no keys are in the database")
}
func (s *SQLLitePersistenceKeysStorageTestSuite) TestKeysStorageSqlLite_Delete() {
// Arrange.
// Act and assert.
err := s.service.DeleteMk(pubKey1, 0)
s.NoError(err)
}
func (s *SQLLitePersistenceKeysStorageTestSuite) TestKeysStorageSqlLite_Flow() {
// Act.
err := s.service.Put([]byte("session-id"), pubKey1, 0, mk1, 1)
s.NoError(err)
k, ok, err := s.service.Get(pubKey1, 0)
// Assert.
s.NoError(err)
s.True(ok, "It returns true")
s.Equal(mk1, k, "It returns the message key")
// Act.
_, ok, err = s.service.Get(pubKey2, 0)
// Assert.
s.NoError(err)
s.False(ok, "It returns false when querying non existing public key")
// Act.
_, ok, err = s.service.Get(pubKey1, 1)
// Assert.
s.NoError(err)
s.False(ok, "It returns false when querying the wrong msg number")
// Act.
cnt, err := s.service.Count(pubKey1)
// Assert.
s.NoError(err)
s.EqualValues(1, cnt)
// Act and assert.
err = s.service.DeleteMk(pubKey1, 1)
s.NoError(err)
// Act and assert.
err = s.service.DeleteMk(pubKey2, 0)
s.NoError(err)
// Act.
err = s.service.DeleteMk(pubKey1, 0)
s.NoError(err)
cnt, err = s.service.Count(pubKey1)
// Assert.
s.NoError(err)
s.EqualValues(0, cnt)
}

View File

@ -1,351 +0,0 @@
package chat
import (
"database/sql"
"os"
"testing"
"github.com/ethereum/go-ethereum/crypto"
"github.com/status-im/status-go/messaging/multidevice"
"github.com/stretchr/testify/suite"
)
const (
dbPath = "/tmp/status-key-store.db"
key = "blahblahblah"
)
func TestSQLLitePersistenceTestSuite(t *testing.T) {
suite.Run(t, new(SQLLitePersistenceTestSuite))
}
type SQLLitePersistenceTestSuite struct {
suite.Suite
// nolint: structcheck, megacheck
db *sql.DB
service Persistence
}
func (s *SQLLitePersistenceTestSuite) SetupTest() {
os.Remove(dbPath)
p, err := NewSQLLitePersistence(dbPath, key)
s.Require().NoError(err)
s.service = p
}
func (s *SQLLitePersistenceTestSuite) TestMultipleInit() {
os.Remove(dbPath)
_, err := NewSQLLitePersistence(dbPath, key)
s.Require().NoError(err)
_, err = NewSQLLitePersistence(dbPath, key)
s.Require().NoError(err)
}
func (s *SQLLitePersistenceTestSuite) TestPrivateBundle() {
installationID := "1"
key, err := crypto.GenerateKey()
s.Require().NoError(err)
actualKey, err := s.service.GetPrivateKeyBundle([]byte("non-existing"))
s.Require().NoError(err, "Error was not returned even though bundle is not there")
s.Nil(actualKey)
anyPrivateBundle, err := s.service.GetAnyPrivateBundle([]byte("non-existing-id"), []*multidevice.Installation{{ID: installationID, Version: 1}})
s.Require().NoError(err)
s.Nil(anyPrivateBundle)
bundle, err := NewBundleContainer(key, installationID)
s.Require().NoError(err)
err = s.service.AddPrivateBundle(bundle)
s.Require().NoError(err)
bundleID := bundle.GetBundle().GetSignedPreKeys()[installationID].GetSignedPreKey()
actualKey, err = s.service.GetPrivateKeyBundle(bundleID)
s.Require().NoError(err)
s.Equal(bundle.GetPrivateSignedPreKey(), actualKey, "It returns the same key")
identity := crypto.CompressPubkey(&key.PublicKey)
anyPrivateBundle, err = s.service.GetAnyPrivateBundle(identity, []*multidevice.Installation{{ID: installationID, Version: 1}})
s.Require().NoError(err)
s.NotNil(anyPrivateBundle)
s.Equal(bundle.GetBundle().GetSignedPreKeys()[installationID].SignedPreKey, anyPrivateBundle.GetBundle().GetSignedPreKeys()[installationID].SignedPreKey, "It returns the same bundle")
}
func (s *SQLLitePersistenceTestSuite) TestPublicBundle() {
key, err := crypto.GenerateKey()
s.Require().NoError(err)
actualBundle, err := s.service.GetPublicBundle(&key.PublicKey, []*multidevice.Installation{{ID: "1", Version: 1}})
s.Require().NoError(err, "Error was not returned even though bundle is not there")
s.Nil(actualBundle)
bundleContainer, err := NewBundleContainer(key, "1")
s.Require().NoError(err)
bundle := bundleContainer.GetBundle()
err = s.service.AddPublicBundle(bundle)
s.Require().NoError(err)
actualBundle, err = s.service.GetPublicBundle(&key.PublicKey, []*multidevice.Installation{{ID: "1", Version: 1}})
s.Require().NoError(err)
s.Equal(bundle.GetIdentity(), actualBundle.GetIdentity(), "It sets the right identity")
s.Equal(bundle.GetSignedPreKeys(), actualBundle.GetSignedPreKeys(), "It sets the right prekeys")
}
func (s *SQLLitePersistenceTestSuite) TestUpdatedBundle() {
key, err := crypto.GenerateKey()
s.Require().NoError(err)
actualBundle, err := s.service.GetPublicBundle(&key.PublicKey, []*multidevice.Installation{{ID: "1", Version: 1}})
s.Require().NoError(err, "Error was not returned even though bundle is not there")
s.Nil(actualBundle)
// Create & add initial bundle
bundleContainer, err := NewBundleContainer(key, "1")
s.Require().NoError(err)
bundle := bundleContainer.GetBundle()
err = s.service.AddPublicBundle(bundle)
s.Require().NoError(err)
// Create & add a new bundle
bundleContainer, err = NewBundleContainer(key, "1")
s.Require().NoError(err)
bundle = bundleContainer.GetBundle()
// We set the version
bundle.GetSignedPreKeys()["1"].Version = 1
err = s.service.AddPublicBundle(bundle)
s.Require().NoError(err)
actualBundle, err = s.service.GetPublicBundle(&key.PublicKey, []*multidevice.Installation{{ID: "1", Version: 1}})
s.Require().NoError(err)
s.Equal(bundle.GetIdentity(), actualBundle.GetIdentity(), "It sets the right identity")
s.Equal(bundle.GetSignedPreKeys(), actualBundle.GetSignedPreKeys(), "It sets the right prekeys")
}
func (s *SQLLitePersistenceTestSuite) TestOutOfOrderBundles() {
key, err := crypto.GenerateKey()
s.Require().NoError(err)
actualBundle, err := s.service.GetPublicBundle(&key.PublicKey, []*multidevice.Installation{{ID: "1", Version: 1}})
s.Require().NoError(err, "Error was not returned even though bundle is not there")
s.Nil(actualBundle)
// Create & add initial bundle
bundleContainer, err := NewBundleContainer(key, "1")
s.Require().NoError(err)
bundle1 := bundleContainer.GetBundle()
err = s.service.AddPublicBundle(bundle1)
s.Require().NoError(err)
// Create & add a new bundle
bundleContainer, err = NewBundleContainer(key, "1")
s.Require().NoError(err)
bundle2 := bundleContainer.GetBundle()
// We set the version
bundle2.GetSignedPreKeys()["1"].Version = 1
err = s.service.AddPublicBundle(bundle2)
s.Require().NoError(err)
// Add again the initial bundle
err = s.service.AddPublicBundle(bundle1)
s.Require().NoError(err)
actualBundle, err = s.service.GetPublicBundle(&key.PublicKey, []*multidevice.Installation{{ID: "1", Version: 1}})
s.Require().NoError(err)
s.Equal(bundle2.GetIdentity(), actualBundle.GetIdentity(), "It sets the right identity")
s.Equal(bundle2.GetSignedPreKeys()["1"].GetVersion(), uint32(1))
s.Equal(bundle2.GetSignedPreKeys()["1"].GetSignedPreKey(), actualBundle.GetSignedPreKeys()["1"].GetSignedPreKey(), "It sets the right prekeys")
}
func (s *SQLLitePersistenceTestSuite) TestMultiplePublicBundle() {
key, err := crypto.GenerateKey()
s.Require().NoError(err)
actualBundle, err := s.service.GetPublicBundle(&key.PublicKey, []*multidevice.Installation{{ID: "1", Version: 1}})
s.Require().NoError(err, "Error was not returned even though bundle is not there")
s.Nil(actualBundle)
bundleContainer, err := NewBundleContainer(key, "1")
s.Require().NoError(err)
bundle := bundleContainer.GetBundle()
err = s.service.AddPublicBundle(bundle)
s.Require().NoError(err)
// Adding it again does not throw an error
err = s.service.AddPublicBundle(bundle)
s.Require().NoError(err)
// Adding a different bundle
bundleContainer, err = NewBundleContainer(key, "1")
s.Require().NoError(err)
// We set the version
bundle = bundleContainer.GetBundle()
bundle.GetSignedPreKeys()["1"].Version = 1
err = s.service.AddPublicBundle(bundle)
s.Require().NoError(err)
// Returns the most recent bundle
actualBundle, err = s.service.GetPublicBundle(&key.PublicKey, []*multidevice.Installation{{ID: "1", Version: 1}})
s.Require().NoError(err)
s.Equal(bundle.GetIdentity(), actualBundle.GetIdentity(), "It sets the identity")
s.Equal(bundle.GetSignedPreKeys(), actualBundle.GetSignedPreKeys(), "It sets the signed pre keys")
}
func (s *SQLLitePersistenceTestSuite) TestMultiDevicePublicBundle() {
key, err := crypto.GenerateKey()
s.Require().NoError(err)
actualBundle, err := s.service.GetPublicBundle(&key.PublicKey, []*multidevice.Installation{{ID: "1", Version: 1}})
s.Require().NoError(err, "Error was not returned even though bundle is not there")
s.Nil(actualBundle)
bundleContainer, err := NewBundleContainer(key, "1")
s.Require().NoError(err)
bundle := bundleContainer.GetBundle()
err = s.service.AddPublicBundle(bundle)
s.Require().NoError(err)
// Adding it again does not throw an error
err = s.service.AddPublicBundle(bundle)
s.Require().NoError(err)
// Adding a different bundle from a different instlation id
bundleContainer, err = NewBundleContainer(key, "2")
s.Require().NoError(err)
bundle = bundleContainer.GetBundle()
err = s.service.AddPublicBundle(bundle)
s.Require().NoError(err)
// Returns the most recent bundle
actualBundle, err = s.service.GetPublicBundle(&key.PublicKey,
[]*multidevice.Installation{
{ID: "1", Version: 1},
{ID: "2", Version: 1},
})
s.Require().NoError(err)
s.Equal(bundle.GetIdentity(), actualBundle.GetIdentity(), "It sets the identity")
s.NotNil(actualBundle.GetSignedPreKeys()["1"])
s.NotNil(actualBundle.GetSignedPreKeys()["2"])
}
func (s *SQLLitePersistenceTestSuite) TestRatchetInfoPrivateBundle() {
key, err := crypto.GenerateKey()
s.Require().NoError(err)
// Add a private bundle
bundle, err := NewBundleContainer(key, "2")
s.Require().NoError(err)
err = s.service.AddPrivateBundle(bundle)
s.Require().NoError(err)
err = s.service.AddRatchetInfo(
[]byte("symmetric-key"),
[]byte("their-public-key"),
bundle.GetBundle().GetSignedPreKeys()["2"].GetSignedPreKey(),
[]byte("ephemeral-public-key"),
"1",
)
s.Require().NoError(err)
ratchetInfo, err := s.service.GetRatchetInfo(bundle.GetBundle().GetSignedPreKeys()["2"].GetSignedPreKey(), []byte("their-public-key"), "1")
s.Require().NoError(err)
s.Require().NotNil(ratchetInfo)
s.NotNil(ratchetInfo.ID, "It adds an id")
s.Equal(ratchetInfo.PrivateKey, bundle.GetPrivateSignedPreKey(), "It returns the private key")
s.Equal(ratchetInfo.Sk, []byte("symmetric-key"), "It returns the symmetric key")
s.Equal(ratchetInfo.Identity, []byte("their-public-key"), "It returns the identity of the contact")
s.Equal(ratchetInfo.PublicKey, bundle.GetBundle().GetSignedPreKeys()["2"].GetSignedPreKey(), "It returns the public key of the bundle")
s.Equal(bundle.GetBundle().GetSignedPreKeys()["2"].GetSignedPreKey(), ratchetInfo.BundleID, "It returns the bundle id")
s.Equal([]byte("ephemeral-public-key"), ratchetInfo.EphemeralKey, "It returns the ratchet ephemeral key")
s.Equal("1", ratchetInfo.InstallationID, "It returns the right installation id")
}
func (s *SQLLitePersistenceTestSuite) TestRatchetInfoPublicBundle() {
installationID := "1"
theirPublicKey := []byte("their-public-key")
key, err := crypto.GenerateKey()
s.Require().NoError(err)
// Add a private bundle
bundle, err := NewBundleContainer(key, installationID)
s.Require().NoError(err)
err = s.service.AddPublicBundle(bundle.GetBundle())
s.Require().NoError(err)
signedPreKey := bundle.GetBundle().GetSignedPreKeys()[installationID].GetSignedPreKey()
err = s.service.AddRatchetInfo(
[]byte("symmetric-key"),
theirPublicKey,
signedPreKey,
[]byte("public-ephemeral-key"),
installationID,
)
s.Require().NoError(err)
ratchetInfo, err := s.service.GetRatchetInfo(signedPreKey, theirPublicKey, installationID)
s.Require().NoError(err)
s.Require().NotNil(ratchetInfo, "It returns the ratchet info")
s.NotNil(ratchetInfo.ID, "It adds an id")
s.Nil(ratchetInfo.PrivateKey, "It does not return the private key")
s.Equal(ratchetInfo.Sk, []byte("symmetric-key"), "It returns the symmetric key")
s.Equal(ratchetInfo.Identity, theirPublicKey, "It returns the identity of the contact")
s.Equal(ratchetInfo.PublicKey, signedPreKey, "It returns the public key of the bundle")
s.Equal(installationID, ratchetInfo.InstallationID, "It returns the right installationID")
s.Nilf(ratchetInfo.PrivateKey, "It does not return the private key")
ratchetInfo, err = s.service.GetAnyRatchetInfo(theirPublicKey, installationID)
s.Require().NoError(err)
s.Require().NotNil(ratchetInfo, "It returns the ratchet info")
s.NotNil(ratchetInfo.ID, "It adds an id")
s.Nil(ratchetInfo.PrivateKey, "It does not return the private key")
s.Equal(ratchetInfo.Sk, []byte("symmetric-key"), "It returns the symmetric key")
s.Equal(ratchetInfo.Identity, theirPublicKey, "It returns the identity of the contact")
s.Equal(ratchetInfo.PublicKey, signedPreKey, "It returns the public key of the bundle")
s.Equal(signedPreKey, ratchetInfo.BundleID, "It returns the bundle id")
s.Equal(installationID, ratchetInfo.InstallationID, "It saves the right installation ID")
}
func (s *SQLLitePersistenceTestSuite) TestRatchetInfoNoBundle() {
err := s.service.AddRatchetInfo(
[]byte("symmetric-key"),
[]byte("their-public-key"),
[]byte("non-existing-bundle"),
[]byte("non-existing-ephemeral-key"),
"none",
)
s.Error(err, "It returns an error")
_, err = s.service.GetRatchetInfo([]byte("non-existing-bundle"), []byte("their-public-key"), "none")
s.Require().NoError(err)
ratchetInfo, err := s.service.GetAnyRatchetInfo([]byte("their-public-key"), "4")
s.Require().NoError(err)
s.Nil(ratchetInfo, "It returns nil when no bundle is there")
}
// TODO: Add test for MarkBundleExpired

View File

@ -1,210 +0,0 @@
package chat
import (
"fmt"
"testing"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/ecies"
"github.com/status-im/status-go/messaging/chat/protobuf"
"github.com/stretchr/testify/require"
)
const (
alicePrivateKey = "00000000000000000000000000000000"
aliceEphemeralKey = "11111111111111111111111111111111"
bobPrivateKey = "22222222222222222222222222222222"
bobSignedPreKey = "33333333333333333333333333333333"
)
var sharedKey = []byte{0xa4, 0xe9, 0x23, 0xd0, 0xaf, 0x8f, 0xe7, 0x8a, 0x5, 0x63, 0x63, 0xbe, 0x20, 0xe7, 0x1c, 0xa, 0x58, 0xe5, 0x69, 0xea, 0x8f, 0xc1, 0xf7, 0x92, 0x89, 0xec, 0xa1, 0xd, 0x9f, 0x68, 0x13, 0x3a}
func bobBundle() (*protobuf.Bundle, error) {
privateKey, err := crypto.ToECDSA([]byte(bobPrivateKey))
if err != nil {
return nil, err
}
signedPreKey, err := crypto.ToECDSA([]byte(bobSignedPreKey))
if err != nil {
return nil, err
}
compressedPreKey := crypto.CompressPubkey(&signedPreKey.PublicKey)
signature, err := crypto.Sign(crypto.Keccak256(compressedPreKey), privateKey)
if err != nil {
return nil, err
}
signedPreKeys := make(map[string]*protobuf.SignedPreKey)
signedPreKeys[bobInstallationID] = &protobuf.SignedPreKey{SignedPreKey: compressedPreKey}
bundle := protobuf.Bundle{
Identity: crypto.CompressPubkey(&privateKey.PublicKey),
SignedPreKeys: signedPreKeys,
Signature: signature,
}
return &bundle, nil
}
func TestNewBundleContainer(t *testing.T) {
privateKey, err := crypto.ToECDSA([]byte(alicePrivateKey))
require.NoError(t, err, "Private key should be generated without errors")
bundleContainer, err := NewBundleContainer(privateKey, bobInstallationID)
require.NoError(t, err, "Bundle container should be created successfully")
err = SignBundle(privateKey, bundleContainer)
require.NoError(t, err, "Bundle container should be signed successfully")
require.NoError(t, err, "Bundle container should be created successfully")
bundle := bundleContainer.Bundle
require.NotNil(t, bundle, "Bundle should be generated without errors")
signatureMaterial := append([]byte(bobInstallationID), bundle.GetSignedPreKeys()[bobInstallationID].GetSignedPreKey()...)
signatureMaterial = append(signatureMaterial, []byte("0")...)
signatureMaterial = append(signatureMaterial, []byte(fmt.Sprint(bundle.GetTimestamp()))...)
recoveredPublicKey, err := crypto.SigToPub(
crypto.Keccak256(signatureMaterial),
bundle.Signature,
)
require.NoError(t, err, "Public key should be recovered from the bundle successfully")
require.Equal(
t,
privateKey.PublicKey,
*recoveredPublicKey,
"The correct public key should be recovered",
)
}
func TestSignBundle(t *testing.T) {
privateKey, err := crypto.ToECDSA([]byte(alicePrivateKey))
require.NoError(t, err, "Private key should be generated without errors")
bundleContainer1, err := NewBundleContainer(privateKey, "1")
require.NoError(t, err, "Bundle container should be created successfully")
bundle1 := bundleContainer1.Bundle
require.NotNil(t, bundle1, "Bundle should be generated without errors")
// We add a signed pre key
signedPreKeys := bundle1.GetSignedPreKeys()
signedPreKeys["2"] = &protobuf.SignedPreKey{SignedPreKey: []byte("key")}
err = SignBundle(privateKey, bundleContainer1)
require.NoError(t, err)
signatureMaterial := append([]byte("1"), bundle1.GetSignedPreKeys()["1"].GetSignedPreKey()...)
signatureMaterial = append(signatureMaterial, []byte("0")...)
signatureMaterial = append(signatureMaterial, []byte("2")...)
signatureMaterial = append(signatureMaterial, []byte("key")...)
signatureMaterial = append(signatureMaterial, []byte("0")...)
signatureMaterial = append(signatureMaterial, []byte(fmt.Sprint(bundle1.GetTimestamp()))...)
recoveredPublicKey, err := crypto.SigToPub(
crypto.Keccak256(signatureMaterial),
bundleContainer1.GetBundle().Signature,
)
require.NoError(t, err, "Public key should be recovered from the bundle successfully")
require.Equal(
t,
privateKey.PublicKey,
*recoveredPublicKey,
"The correct public key should be recovered",
)
}
func TestExtractIdentity(t *testing.T) {
privateKey, err := crypto.ToECDSA([]byte(alicePrivateKey))
require.NoError(t, err, "Private key should be generated without errors")
bundleContainer, err := NewBundleContainer(privateKey, "1")
require.NoError(t, err, "Bundle container should be created successfully")
err = SignBundle(privateKey, bundleContainer)
require.NoError(t, err, "Bundle container should be signed successfully")
bundle := bundleContainer.Bundle
require.NotNil(t, bundle, "Bundle should be generated without errors")
recoveredPublicKey, err := ExtractIdentity(bundle)
require.NoError(t, err, "Public key should be recovered from the bundle successfully")
require.Equal(
t,
privateKey.PublicKey,
*recoveredPublicKey,
"The correct public key should be recovered",
)
}
// Alice wants to send a message to Bob
func TestX3dhActive(t *testing.T) {
bobIdentityKey, err := crypto.ToECDSA([]byte(bobPrivateKey))
require.NoError(t, err, "Bundle identity key should be generated without errors")
bobSignedPreKey, err := crypto.ToECDSA([]byte(bobSignedPreKey))
require.NoError(t, err, "Bundle signed pre key should be generated without errors")
aliceIdentityKey, err := crypto.ToECDSA([]byte(alicePrivateKey))
require.NoError(t, err, "Private key should be generated without errors")
aliceEphemeralKey, err := crypto.ToECDSA([]byte(aliceEphemeralKey))
require.NoError(t, err, "Ephemeral key should be generated without errors")
x3dh, err := x3dhActive(
ecies.ImportECDSA(aliceIdentityKey),
ecies.ImportECDSAPublic(&bobSignedPreKey.PublicKey),
ecies.ImportECDSA(aliceEphemeralKey),
ecies.ImportECDSAPublic(&bobIdentityKey.PublicKey),
)
require.NoError(t, err, "Shared key should be generated without errors")
require.Equal(t, sharedKey, x3dh, "Should generate the correct key")
}
// Bob receives a message from Alice
func TestPerformPassiveX3DH(t *testing.T) {
alicePrivateKey, err := crypto.ToECDSA([]byte(alicePrivateKey))
require.NoError(t, err, "Private key should be generated without errors")
bobSignedPreKey, err := crypto.ToECDSA([]byte(bobSignedPreKey))
require.NoError(t, err, "Private key should be generated without errors")
aliceEphemeralKey, err := crypto.ToECDSA([]byte(aliceEphemeralKey))
require.NoError(t, err, "Ephemeral key should be generated without errors")
bobPrivateKey, err := crypto.ToECDSA([]byte(bobPrivateKey))
require.NoError(t, err, "Private key should be generated without errors")
x3dh, err := PerformPassiveX3DH(
&alicePrivateKey.PublicKey,
bobSignedPreKey,
&aliceEphemeralKey.PublicKey,
bobPrivateKey,
)
require.NoError(t, err, "Shared key should be generated without errors")
require.Equal(t, sharedKey, x3dh, "Should generate the correct key")
}
func TestPerformActiveX3DH(t *testing.T) {
bundle, err := bobBundle()
require.NoError(t, err, "Test bundle should be generated without errors")
privateKey, err := crypto.ToECDSA([]byte(bobPrivateKey))
require.NoError(t, err, "Private key should be imported without errors")
signedPreKey := bundle.GetSignedPreKeys()[bobInstallationID].GetSignedPreKey()
actualSharedSecret, actualEphemeralKey, err := PerformActiveX3DH(bundle.GetIdentity(), signedPreKey, privateKey)
require.NoError(t, err, "No error should be reported")
require.NotNil(t, actualEphemeralKey, "An ephemeral key-pair should be generated")
require.NotNil(t, actualSharedSecret, "A shared key should be generated")
}

View File

@ -1,594 +0,0 @@
package filter
import (
"crypto/ecdsa"
"encoding/hex"
"errors"
"fmt"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/messaging/sharedsecret"
whisper "github.com/status-im/whisper/whisperv6"
"math/big"
"sync"
"time"
)
const (
discoveryTopic = "contact-discovery"
)
// The number of partitions
var nPartitions = big.NewInt(5000)
var minPow = 0.0
type Filter struct {
FilterID string
Topic whisper.TopicType
SymKeyID string
}
type Chat struct {
// ChatID is the identifier of the chat
ChatID string `json:"chatId"`
// SymKeyID is the symmetric key id used for symmetric chats
SymKeyID string `json:"symKeyId"`
// OneToOne tells us if we need to use asymmetric encryption for this chat
OneToOne bool `json:"oneToOne"`
// Listen is whether we are actually listening for messages on this chat, or the filter is only created in order to be able to post on the topic
Listen bool `json:"listen"`
// FilterID the whisper filter id generated
FilterID string `json:"filterId"`
// Identity is the public key of the other recipient for non-public chats
Identity string `json:"identity"`
// Topic is the whisper topic
Topic whisper.TopicType `json:"topic"`
// Discovery is whether this is a discovery topic
Discovery bool `json:"discovery"`
// Negotiated tells us whether is a negotiated topic
Negotiated bool `json:"negotiated"`
}
type Messages struct {
Chat *Chat `json:"chat"`
Messages []*whisper.Message `json:"messages"`
Error error `json:"error"`
}
type Service struct {
whisper *whisper.Whisper
secret *sharedsecret.Service
chats map[string]*Chat
persistence Persistence
mutex sync.Mutex
keys map[string][]byte
quit chan struct{}
onNewMessages func([]*Messages)
}
// New returns a new filter service
func New(w *whisper.Whisper, p Persistence, s *sharedsecret.Service, onNewMessages func([]*Messages)) *Service {
return &Service{
whisper: w,
secret: s,
mutex: sync.Mutex{},
persistence: p,
chats: make(map[string]*Chat),
quit: make(chan struct{}),
onNewMessages: onNewMessages,
}
}
// LoadChat should return a list of newly chats loaded
func (s *Service) Init(chats []*Chat) ([]*Chat, error) {
log.Debug("Initializing filter service", "chats", chats)
keys, err := s.persistence.All()
if err != nil {
return nil, err
}
s.keys = keys
keyID := s.whisper.SelectedKeyPairID()
if keyID == "" {
return nil, errors.New("no key selected")
}
myKey, err := s.whisper.GetPrivateKey(keyID)
if err != nil {
return nil, err
}
// Add our own topic
log.Debug("Loading one to one chats")
identityStr := fmt.Sprintf("%x", crypto.FromECDSAPub(&myKey.PublicKey))
_, err = s.loadOneToOne(myKey, identityStr, true)
if err != nil {
log.Error("Error loading one to one chats", "err", err)
return nil, err
}
// Add discovery topic
log.Debug("Loading discovery topics")
err = s.loadDiscovery(myKey)
if err != nil {
return nil, err
}
// Add the various one to one and public chats
log.Debug("Loading chats")
for _, chat := range chats {
_, err = s.load(myKey, chat)
if err != nil {
return nil, err
}
}
// Add the negotiated secrets
log.Debug("Loading negotiated topics")
secrets, err := s.secret.All()
if err != nil {
return nil, err
}
for _, secret := range secrets {
if _, err := s.ProcessNegotiatedSecret(secret); err != nil {
return nil, err
}
}
s.mutex.Lock()
defer s.mutex.Unlock()
var allChats []*Chat
for _, chat := range s.chats {
allChats = append(allChats, chat)
}
log.Debug("Loaded chats")
return allChats, nil
}
func (s *Service) Start(checkPeriod time.Duration) {
ticker := time.NewTicker(checkPeriod)
defer ticker.Stop()
for {
select {
case <-ticker.C:
messages := s.getMessages()
if len(messages) != 0 {
s.onNewMessages(messages)
}
case <-s.quit:
return
}
}
}
// Stop removes all the filters
func (s *Service) Stop() error {
close(s.quit)
var chats []*Chat
s.mutex.Lock()
for _, chat := range s.chats {
chats = append(chats, chat)
}
s.mutex.Unlock()
return s.Remove(chats)
}
// Remove remove all the filters associated with a chat/identity
func (s *Service) Remove(chats []*Chat) error {
s.mutex.Lock()
defer s.mutex.Unlock()
log.Debug("Removing chats", "chats", chats)
for _, chat := range chats {
log.Debug("Removing chat", "chat", chat)
if err := s.whisper.Unsubscribe(chat.FilterID); err != nil {
return err
}
if chat.SymKeyID != "" {
s.whisper.DeleteSymKey(chat.SymKeyID)
}
delete(s.chats, chat.ChatID)
}
return nil
}
// LoadPartitioned creates a filter for a partitioned topic
func (s *Service) LoadPartitioned(myKey *ecdsa.PrivateKey, theirPublicKey *ecdsa.PublicKey, listen bool) (*Chat, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
chatID := PublicKeyToPartitionedTopic(theirPublicKey)
if _, ok := s.chats[chatID]; ok {
return s.chats[chatID], nil
}
// We set up a filter so we can publish, but we discard envelopes if listen is false
filter, err := s.addAsymmetricFilter(myKey, chatID, listen)
if err != nil {
return nil, err
}
identityStr := fmt.Sprintf("%x", crypto.FromECDSAPub(theirPublicKey))
chat := &Chat{
ChatID: chatID,
FilterID: filter.FilterID,
Topic: filter.Topic,
Listen: listen,
Identity: identityStr,
Discovery: true,
}
s.chats[chatID] = chat
return chat, nil
}
// Load creates filters for a given chat, and returns all the created filters
func (s *Service) Load(chat *Chat) ([]*Chat, error) {
keyID := s.whisper.SelectedKeyPairID()
if keyID == "" {
return nil, errors.New("no key selected")
}
myKey, err := s.whisper.GetPrivateKey(keyID)
if err != nil {
return nil, err
}
return s.load(myKey, chat)
}
func ContactCodeTopic(identity string) string {
return "0x" + identity + "-contact-code"
}
// Get returns a negotiated chat given an identity
func (s *Service) GetNegotiated(identity *ecdsa.PublicKey) *Chat {
s.mutex.Lock()
defer s.mutex.Unlock()
return s.chats[negotiatedID(identity)]
}
// GetByID returns a chat by chatID
func (s *Service) GetByID(chatID string) *Chat {
s.mutex.Lock()
defer s.mutex.Unlock()
return s.chats[chatID]
}
// ProcessNegotiatedSecret adds a filter based on the agreed secret
func (s *Service) ProcessNegotiatedSecret(secret *sharedsecret.Secret) (*Chat, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
chatID := negotiatedID(secret.Identity)
// If we already have a chat do nothing
if _, ok := s.chats[chatID]; ok {
return s.chats[chatID], nil
}
keyString := fmt.Sprintf("%x", secret.Key)
filter, err := s.addSymmetric(keyString)
if err != nil {
return nil, err
}
identityStr := fmt.Sprintf("%x", crypto.FromECDSAPub(secret.Identity))
chat := &Chat{
ChatID: chatID,
Topic: filter.Topic,
SymKeyID: filter.SymKeyID,
FilterID: filter.FilterID,
Identity: identityStr,
Listen: true,
Negotiated: true,
}
log.Debug("Processing negotiated secret", "chat-id", chatID, "topic", filter.Topic)
s.chats[chat.ChatID] = chat
return chat, nil
}
// ToTopic converts a string to a whisper topic
func ToTopic(s string) []byte {
return crypto.Keccak256([]byte(s))[:whisper.TopicLength]
}
// PublicKeyToPartitionedTopic returns the associated partitioned topic string
// with the given public key
func PublicKeyToPartitionedTopic(publicKey *ecdsa.PublicKey) string {
partition := big.NewInt(0)
partition.Mod(publicKey.X, nPartitions)
return fmt.Sprintf("contact-discovery-%d", partition.Int64())
}
// PublicKeyToPartitionedTopicBytes returns the bytes of the partitioned topic
// associated with the given public key
func PublicKeyToPartitionedTopicBytes(publicKey *ecdsa.PublicKey) []byte {
return ToTopic(PublicKeyToPartitionedTopic(publicKey))
}
// loadDiscovery adds the discovery filter
func (s *Service) loadDiscovery(myKey *ecdsa.PrivateKey) error {
s.mutex.Lock()
defer s.mutex.Unlock()
if _, ok := s.chats[discoveryTopic]; ok {
return nil
}
identityStr := fmt.Sprintf("%x", crypto.FromECDSAPub(&myKey.PublicKey))
discoveryChat := &Chat{
ChatID: discoveryTopic,
Listen: true,
Identity: identityStr,
Discovery: true,
}
discoveryResponse, err := s.addAsymmetricFilter(myKey, discoveryChat.ChatID, true)
if err != nil {
return err
}
discoveryChat.Topic = discoveryResponse.Topic
discoveryChat.FilterID = discoveryResponse.FilterID
s.chats[discoveryChat.ChatID] = discoveryChat
// Load personal discovery
personalDiscoveryTopic := fmt.Sprintf("contact-discovery-%s", identityStr)
personalDiscoveryChat := &Chat{
ChatID: personalDiscoveryTopic,
Listen: true,
Identity: identityStr,
Discovery: true,
}
discoveryResponse, err = s.addAsymmetricFilter(myKey, personalDiscoveryChat.ChatID, true)
if err != nil {
return err
}
personalDiscoveryChat.Topic = discoveryResponse.Topic
personalDiscoveryChat.FilterID = discoveryResponse.FilterID
s.chats[personalDiscoveryChat.ChatID] = personalDiscoveryChat
return nil
}
// loadPublic adds a filter for a public chat
func (s *Service) loadPublic(chat *Chat) error {
s.mutex.Lock()
defer s.mutex.Unlock()
if _, ok := s.chats[chat.ChatID]; ok {
return nil
}
filterAndTopic, err := s.addSymmetric(chat.ChatID)
if err != nil {
return err
}
chat.FilterID = filterAndTopic.FilterID
chat.SymKeyID = filterAndTopic.SymKeyID
chat.Topic = filterAndTopic.Topic
chat.Listen = true
s.chats[chat.ChatID] = chat
return nil
}
// loadOneToOne creates two filters for a given chat, one listening to the contact codes
// and another on the partitioned topic, if listen is specified.
func (s *Service) loadOneToOne(myKey *ecdsa.PrivateKey, identity string, listen bool) ([]*Chat, error) {
var chats []*Chat
contactCodeChat, err := s.loadContactCode(identity)
if err != nil {
return nil, err
}
chats = append(chats, contactCodeChat)
if listen {
publicKeyBytes, err := hex.DecodeString(identity)
if err != nil {
return nil, err
}
publicKey, err := crypto.UnmarshalPubkey(publicKeyBytes)
if err != nil {
return nil, err
}
partitionedChat, err := s.LoadPartitioned(myKey, publicKey, listen)
if err != nil {
return nil, err
}
chats = append(chats, partitionedChat)
}
return chats, nil
}
// loadContactCode creates a filter for the topic are advertised for a given identity
func (s *Service) loadContactCode(identity string) (*Chat, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
chatID := ContactCodeTopic(identity)
if _, ok := s.chats[chatID]; ok {
return s.chats[chatID], nil
}
contactCodeFilter, err := s.addSymmetric(chatID)
if err != nil {
return nil, err
}
chat := &Chat{
ChatID: chatID,
FilterID: contactCodeFilter.FilterID,
Topic: contactCodeFilter.Topic,
SymKeyID: contactCodeFilter.SymKeyID,
Identity: identity,
Listen: true,
}
s.chats[chatID] = chat
return chat, nil
}
// addSymmetric adds a symmetric key filter
func (s *Service) addSymmetric(chatID string) (*Filter, error) {
var symKeyID string
var err error
topic := ToTopic(chatID)
topics := [][]byte{topic}
symKey, ok := s.keys[chatID]
if ok {
log.Debug("Loading from cache", "chat-id", chatID)
symKeyID, err = s.whisper.AddSymKeyDirect(symKey)
if err != nil {
log.Error("Adding symkey failed", "err", err)
return nil, err
}
} else {
log.Debug("Generating symkey", "chat-id", chatID)
symKeyID, err = s.whisper.AddSymKeyFromPassword(chatID)
if err != nil {
log.Error("Adding symkey from password failed", "err", err)
return nil, err
}
if symKey, err = s.whisper.GetSymKey(symKeyID); err != nil {
return nil, err
}
s.keys[chatID] = symKey
err = s.persistence.Add(chatID, symKey)
if err != nil {
return nil, err
}
}
f := &whisper.Filter{
KeySym: symKey,
PoW: minPow,
AllowP2P: true,
Topics: topics,
Messages: s.whisper.NewMessageStore(),
}
id, err := s.whisper.Subscribe(f)
if err != nil {
return nil, err
}
return &Filter{
FilterID: id,
SymKeyID: symKeyID,
Topic: whisper.BytesToTopic(topic),
}, nil
}
// addAsymmetricFilter adds a filter with our privatekey, and set minPow according to the listen parameter
func (s *Service) addAsymmetricFilter(keyAsym *ecdsa.PrivateKey, chatID string, listen bool) (*Filter, error) {
var err error
var pow float64
if listen {
pow = minPow
} else {
// Set high pow so we discard messages
pow = 1
}
topic := ToTopic(chatID)
topics := [][]byte{topic}
f := &whisper.Filter{
KeyAsym: keyAsym,
PoW: pow,
AllowP2P: true,
Topics: topics,
Messages: s.whisper.NewMessageStore(),
}
id, err := s.whisper.Subscribe(f)
if err != nil {
return nil, err
}
return &Filter{FilterID: id, Topic: whisper.BytesToTopic(topic)}, nil
}
func (s *Service) getMessages() []*Messages {
var response []*Messages
s.mutex.Lock()
defer s.mutex.Unlock()
for chatID := range s.chats {
messages := s.getMessagesForChat(chatID)
if messages.Error != nil || len(messages.Messages) != 0 {
response = append(response, messages)
}
}
return response
}
func (s *Service) getMessagesForChat(chatID string) *Messages {
response := &Messages{}
response.Chat = s.chats[chatID]
if response.Chat == nil {
response.Error = errors.New("Chat not found")
return response
}
filter := s.whisper.GetFilter(response.Chat.FilterID)
if filter == nil {
response.Error = errors.New("Filter not found")
return response
}
receivedMessages := filter.Retrieve()
response.Messages = make([]*whisper.Message, 0, len(receivedMessages))
for _, msg := range receivedMessages {
response.Messages = append(response.Messages, whisper.ToWhisperMessage(msg))
}
return response
}
func negotiatedID(identity *ecdsa.PublicKey) string {
return fmt.Sprintf("0x%x-negotiated", crypto.FromECDSAPub(identity))
}
func (s *Service) load(myKey *ecdsa.PrivateKey, chat *Chat) ([]*Chat, error) {
log.Debug("Loading chat", "chatID", chat.ChatID)
if chat.OneToOne {
return s.loadOneToOne(myKey, chat.Identity, false)
}
return []*Chat{chat}, s.loadPublic(chat)
}

View File

@ -1,233 +0,0 @@
package filter
import (
"crypto/ecdsa"
"fmt"
"io/ioutil"
"os"
"testing"
"github.com/ethereum/go-ethereum/crypto"
whisper "github.com/status-im/whisper/whisperv6"
"github.com/stretchr/testify/suite"
msgdb "github.com/status-im/status-go/messaging/db"
"github.com/status-im/status-go/messaging/sharedsecret"
)
func TestServiceTestSuite(t *testing.T) {
suite.Run(t, new(ServiceTestSuite))
}
type TestKey struct {
privateKey *ecdsa.PrivateKey
partitionedTopic int
}
func NewTestKey(privateKey string, partitionedTopic int) (*TestKey, error) {
key, err := crypto.HexToECDSA(privateKey)
if err != nil {
return nil, err
}
return &TestKey{
privateKey: key,
partitionedTopic: partitionedTopic,
}, nil
}
func (t *TestKey) PublicKeyString() string {
return fmt.Sprintf("%x", crypto.FromECDSAPub(&t.privateKey.PublicKey))
}
type ServiceTestSuite struct {
suite.Suite
service *Service
path string
keys []*TestKey
}
func (s *ServiceTestSuite) SetupTest() {
keyStrs := []string{"c6cbd7d76bc5baca530c875663711b947efa6a86a900a9e8645ce32e5821484e", "d51dd64ad19ea84968a308dca246012c00d2b2101d41bce740acd1c650acc509"}
keyTopics := []int{4490, 3991}
dbFile, err := ioutil.TempFile(os.TempDir(), "filter")
s.Require().NoError(err)
s.path = dbFile.Name()
for i, k := range keyStrs {
testKey, err := NewTestKey(k, keyTopics[i])
s.Require().NoError(err)
s.keys = append(s.keys, testKey)
}
db, err := msgdb.Open(s.path, "", 0)
s.Require().NoError(err)
// Build services
sharedSecretService := sharedsecret.NewService(sharedsecret.NewSQLLitePersistence(db))
whisper := whisper.New(nil)
_, err = whisper.AddKeyPair(s.keys[0].privateKey)
s.Require().NoError(err)
persistence := NewSQLLitePersistence(db)
s.service = New(whisper, persistence, sharedSecretService, func([]*Messages) {})
}
func (s *ServiceTestSuite) TearDownTest() {
os.Remove(s.path)
}
func (s *ServiceTestSuite) TestDiscoveryAndPartitionedTopic() {
chats := []*Chat{}
partitionedTopic := fmt.Sprintf("contact-discovery-%d", s.keys[0].partitionedTopic)
personalDiscoveryTopic := fmt.Sprintf("contact-discovery-%s", s.keys[0].PublicKeyString())
contactCodeTopic := "0x" + s.keys[0].PublicKeyString() + "-contact-code"
_, err := s.service.Init(chats)
s.Require().NoError(err)
s.Require().Equal(4, len(s.service.chats), "It creates four filters")
discoveryFilter := s.service.chats[discoveryTopic]
s.Require().NotNil(discoveryFilter, "It adds the discovery filter")
s.Require().True(discoveryFilter.Listen)
personalDiscoveryFilter := s.service.chats[personalDiscoveryTopic]
s.Require().NotNil(personalDiscoveryFilter, "It adds the discovery filter")
s.Require().True(personalDiscoveryFilter.Listen)
contactCodeFilter := s.service.chats[contactCodeTopic]
s.Require().NotNil(contactCodeFilter, "It adds the contact code filter")
s.Require().True(contactCodeFilter.Listen)
partitionedFilter := s.service.chats[partitionedTopic]
s.Require().NotNil(partitionedFilter, "It adds the partitioned filter")
s.Require().True(partitionedFilter.Listen)
}
func (s *ServiceTestSuite) TestPublicAndOneToOneChats() {
chats := []*Chat{
{
ChatID: "status",
},
{
ChatID: s.keys[1].PublicKeyString(),
Identity: s.keys[1].PublicKeyString(),
OneToOne: true,
},
}
contactCodeTopic := "0x" + s.keys[1].PublicKeyString() + "-contact-code"
response, err := s.service.Init(chats)
s.Require().NoError(err)
actualChats := make(map[string]*Chat)
for _, chat := range response {
actualChats[chat.ChatID] = chat
}
s.Require().Equal(6, len(actualChats), "It creates two additional filters for the one to one and one for the public chat")
statusFilter := actualChats["status"]
s.Require().NotNil(statusFilter, "It creates a filter for the public chat")
s.Require().NotNil(statusFilter.SymKeyID, "It returns a sym key id")
s.Require().True(statusFilter.Listen)
contactCodeFilter := actualChats[contactCodeTopic]
s.Require().NotNil(contactCodeFilter, "It adds the contact code filter")
s.Require().True(contactCodeFilter.Listen)
}
func (s *ServiceTestSuite) TestLoadFromCache() {
chats := []*Chat{
{
ChatID: "status",
},
{
ChatID: "status-1",
},
}
_, err := s.service.Init(chats)
s.Require().NoError(err)
// We create another service using the same persistence
service2 := New(s.service.whisper, s.service.persistence, s.service.secret, func([]*Messages) {})
_, err = service2.Init(chats)
s.Require().NoError(err)
}
func (s *ServiceTestSuite) TestNegotiatedTopic() {
chats := []*Chat{}
negotiatedTopic1 := "0x" + s.keys[0].PublicKeyString() + "-negotiated"
negotiatedTopic2 := "0x" + s.keys[1].PublicKeyString() + "-negotiated"
// We send a message to ourselves
_, _, err := s.service.secret.Send(s.keys[0].privateKey, "0-1", &s.keys[0].privateKey.PublicKey, []string{"0-2"})
s.Require().NoError(err)
// We send a message to someone else
_, _, err = s.service.secret.Send(s.keys[0].privateKey, "0-1", &s.keys[1].privateKey.PublicKey, []string{"0-2"})
s.Require().NoError(err)
response, err := s.service.Init(chats)
s.Require().NoError(err)
actualChats := make(map[string]*Chat)
for _, chat := range response {
actualChats[chat.ChatID] = chat
}
s.Require().Equal(6, len(actualChats), "It creates two additional filters for the negotiated topics")
negotiatedFilter1 := actualChats[negotiatedTopic1]
s.Require().NotNil(negotiatedFilter1, "It adds the negotiated filter")
negotiatedFilter2 := actualChats[negotiatedTopic2]
s.Require().NotNil(negotiatedFilter2, "It adds the negotiated filter")
}
func (s *ServiceTestSuite) TestLoadChat() {
chats := []*Chat{}
_, err := s.service.Init(chats)
s.Require().NoError(err)
// We add a public chat
response1, err := s.service.Load(&Chat{ChatID: "status"})
s.Require().NoError(err)
s.Require().Equal(1, len(response1))
s.Require().Equal("status", response1[0].ChatID)
s.Require().True(response1[0].Listen)
}
func (s *ServiceTestSuite) TestNoInstallationIDs() {
chats := []*Chat{}
negotiatedTopic1 := "0x" + s.keys[1].PublicKeyString() + "-negotiated"
// We send a message to someone else, but without any installation ID
_, _, err := s.service.secret.Send(s.keys[0].privateKey, "0-1", &s.keys[1].privateKey.PublicKey, []string{})
s.Require().NoError(err)
response, err := s.service.Init(chats)
s.Require().NoError(err)
actualChats := make(map[string]*Chat)
for _, chat := range response {
actualChats[chat.ChatID] = chat
}
s.Require().Equal(5, len(actualChats), "It creates two additional filters for the negotiated topics")
negotiatedFilter1 := actualChats[negotiatedTopic1]
s.Require().NotNil(negotiatedFilter1, "It adds the negotiated filter")
}

View File

@ -1,16 +0,0 @@
package multidevice
type Persistence interface {
// GetActiveInstallations returns the active installations for a given identity.
GetActiveInstallations(maxInstallations int, identity []byte) ([]*Installation, error)
// EnableInstallation enables the installation.
EnableInstallation(identity []byte, installationID string) error
// DisableInstallation disable the installation.
DisableInstallation(identity []byte, installationID string) error
// AddInstallations adds the installations for a given identity, maintaining the enabled flag and returns the newly inserted installations
AddInstallations(identity []byte, timestamp int64, installations []*Installation, defaultEnabled bool) ([]*Installation, error)
// GetInstallations returns all the installations for a given identity
GetInstallations(identity []byte) ([]*Installation, error)
// SetInstallationMetadata sets the metadata for a given installation
SetInstallationMetadata(identity []byte, installationID string, data *InstallationMetadata) error
}

View File

@ -1,312 +0,0 @@
package multidevice
import (
"database/sql"
"os"
"testing"
msgdb "github.com/status-im/status-go/messaging/db"
"github.com/stretchr/testify/suite"
)
const (
dbPath = "/tmp/status-key-store.db"
)
func TestSQLLitePersistenceTestSuite(t *testing.T) {
suite.Run(t, new(SQLLitePersistenceTestSuite))
}
type SQLLitePersistenceTestSuite struct {
suite.Suite
// nolint: structcheck, megacheck
db *sql.DB
service Persistence
}
func (s *SQLLitePersistenceTestSuite) SetupTest() {
os.Remove(dbPath)
db, err := msgdb.Open(dbPath, "", 0)
s.Require().NoError(err)
s.service = NewSQLLitePersistence(db)
}
func (s *SQLLitePersistenceTestSuite) TestAddInstallations() {
identity := []byte("alice")
installations := []*Installation{
{ID: "alice-1", Version: 1, Enabled: true},
{ID: "alice-2", Version: 2, Enabled: true},
}
addedInstallations, err := s.service.AddInstallations(
identity,
1,
installations,
true,
)
s.Require().NoError(err)
enabledInstallations, err := s.service.GetActiveInstallations(5, identity)
s.Require().NoError(err)
s.Require().Equal(installations, enabledInstallations)
s.Require().Equal(installations, addedInstallations)
}
func (s *SQLLitePersistenceTestSuite) TestAddInstallationVersions() {
identity := []byte("alice")
installations := []*Installation{
{ID: "alice-1", Version: 1, Enabled: true},
}
_, err := s.service.AddInstallations(
identity,
1,
installations,
true,
)
s.Require().NoError(err)
enabledInstallations, err := s.service.GetActiveInstallations(5, identity)
s.Require().NoError(err)
s.Require().Equal(installations, enabledInstallations)
installationsWithDowngradedVersion := []*Installation{
{ID: "alice-1", Version: 0},
}
_, err = s.service.AddInstallations(
identity,
3,
installationsWithDowngradedVersion,
true,
)
s.Require().NoError(err)
enabledInstallations, err = s.service.GetActiveInstallations(5, identity)
s.Require().NoError(err)
s.Require().Equal(installations, enabledInstallations)
}
func (s *SQLLitePersistenceTestSuite) TestAddInstallationsLimit() {
identity := []byte("alice")
installations := []*Installation{
{ID: "alice-1", Version: 1},
{ID: "alice-2", Version: 2},
}
_, err := s.service.AddInstallations(
identity,
1,
installations,
true,
)
s.Require().NoError(err)
installations = []*Installation{
{ID: "alice-1", Version: 1},
{ID: "alice-3", Version: 3},
}
_, err = s.service.AddInstallations(
identity,
2,
installations,
true,
)
s.Require().NoError(err)
installations = []*Installation{
{ID: "alice-2", Version: 2, Enabled: true},
{ID: "alice-3", Version: 3, Enabled: true},
{ID: "alice-4", Version: 4, Enabled: true},
}
_, err = s.service.AddInstallations(
identity,
3,
installations,
true,
)
s.Require().NoError(err)
enabledInstallations, err := s.service.GetActiveInstallations(3, identity)
s.Require().NoError(err)
s.Require().Equal(installations, enabledInstallations)
}
func (s *SQLLitePersistenceTestSuite) TestAddInstallationsDisabled() {
identity := []byte("alice")
installations := []*Installation{
{ID: "alice-1", Version: 1},
{ID: "alice-2", Version: 2},
}
_, err := s.service.AddInstallations(
identity,
1,
installations,
false,
)
s.Require().NoError(err)
actualInstallations, err := s.service.GetActiveInstallations(3, identity)
s.Require().NoError(err)
s.Require().Nil(actualInstallations)
}
func (s *SQLLitePersistenceTestSuite) TestDisableInstallation() {
identity := []byte("alice")
installations := []*Installation{
{ID: "alice-1", Version: 1},
{ID: "alice-2", Version: 2},
}
_, err := s.service.AddInstallations(
identity,
1,
installations,
true,
)
s.Require().NoError(err)
err = s.service.DisableInstallation(identity, "alice-1")
s.Require().NoError(err)
// We add the installations again
installations = []*Installation{
{ID: "alice-1", Version: 1},
{ID: "alice-2", Version: 2},
}
addedInstallations, err := s.service.AddInstallations(
identity,
1,
installations,
true,
)
s.Require().NoError(err)
s.Require().Equal(0, len(addedInstallations))
actualInstallations, err := s.service.GetActiveInstallations(3, identity)
s.Require().NoError(err)
expected := []*Installation{{ID: "alice-2", Version: 2, Enabled: true}}
s.Require().Equal(expected, actualInstallations)
}
func (s *SQLLitePersistenceTestSuite) TestEnableInstallation() {
identity := []byte("alice")
installations := []*Installation{
{ID: "alice-1", Version: 1},
{ID: "alice-2", Version: 2},
}
_, err := s.service.AddInstallations(
identity,
1,
installations,
true,
)
s.Require().NoError(err)
err = s.service.DisableInstallation(identity, "alice-1")
s.Require().NoError(err)
actualInstallations, err := s.service.GetActiveInstallations(3, identity)
s.Require().NoError(err)
expected := []*Installation{{ID: "alice-2", Version: 2, Enabled: true}}
s.Require().Equal(expected, actualInstallations)
err = s.service.EnableInstallation(identity, "alice-1")
s.Require().NoError(err)
actualInstallations, err = s.service.GetActiveInstallations(3, identity)
s.Require().NoError(err)
expected = []*Installation{
{ID: "alice-1", Version: 1, Enabled: true},
{ID: "alice-2", Version: 2, Enabled: true},
}
s.Require().Equal(expected, actualInstallations)
}
func (s *SQLLitePersistenceTestSuite) TestGetInstallations() {
identity := []byte("alice")
installations := []*Installation{
{ID: "alice-1", Version: 1},
{ID: "alice-2", Version: 2},
}
_, err := s.service.AddInstallations(
identity,
1,
installations,
true,
)
s.Require().NoError(err)
err = s.service.DisableInstallation(identity, "alice-1")
s.Require().NoError(err)
actualInstallations, err := s.service.GetInstallations(identity)
s.Require().NoError(err)
emptyMetadata := &InstallationMetadata{}
expected := []*Installation{
{ID: "alice-1", Version: 1, Timestamp: 1, Enabled: false, InstallationMetadata: emptyMetadata},
{ID: "alice-2", Version: 2, Timestamp: 1, Enabled: true, InstallationMetadata: emptyMetadata},
}
s.Require().Equal(2, len(actualInstallations))
s.Require().ElementsMatch(expected, actualInstallations)
}
func (s *SQLLitePersistenceTestSuite) TestSetMetadata() {
identity := []byte("alice")
installations := []*Installation{
{ID: "alice-1", Version: 1},
{ID: "alice-2", Version: 2},
}
_, err := s.service.AddInstallations(
identity,
1,
installations,
true,
)
s.Require().NoError(err)
err = s.service.DisableInstallation(identity, "alice-1")
s.Require().NoError(err)
emptyMetadata := &InstallationMetadata{}
setMetadata := &InstallationMetadata{
Name: "a",
FCMToken: "b",
DeviceType: "c",
}
err = s.service.SetInstallationMetadata(identity, "alice-2", setMetadata)
s.Require().NoError(err)
actualInstallations, err := s.service.GetInstallations(identity)
s.Require().NoError(err)
expected := []*Installation{
{ID: "alice-1", Version: 1, Timestamp: 1, Enabled: false, InstallationMetadata: emptyMetadata},
{ID: "alice-2", Version: 2, Timestamp: 1, Enabled: true, InstallationMetadata: setMetadata},
}
s.Require().ElementsMatch(expected, actualInstallations)
}

View File

@ -1,65 +0,0 @@
package publisher
import (
"database/sql"
"fmt"
"sync"
)
type Persistence interface {
GetLastPublished() (int64, error)
SetLastPublished(int64) error
GetLastAcked(identity []byte) (int64, error)
SetLastAcked(identity []byte, lastAcked int64) error
}
type SQLLitePersistence struct {
db *sql.DB
lastAcked map[string]int64
lastAckedMutex sync.Mutex
}
func NewSQLLitePersistence(db *sql.DB) *SQLLitePersistence {
return &SQLLitePersistence{
db: db,
lastAcked: make(map[string]int64),
lastAckedMutex: sync.Mutex{},
}
}
func (s *SQLLitePersistence) GetLastPublished() (int64, error) {
var lastPublished int64
statement := "SELECT last_published FROM contact_code_config LIMIT 1"
err := s.db.QueryRow(statement).Scan(&lastPublished)
if err != nil {
return 0, err
}
return lastPublished, nil
}
func (s *SQLLitePersistence) SetLastPublished(lastPublished int64) error {
statement := "UPDATE contact_code_config SET last_published = ?"
stmt, err := s.db.Prepare(statement)
if err != nil {
return err
}
defer stmt.Close()
_, err = stmt.Exec(lastPublished)
return err
}
func (s *SQLLitePersistence) GetLastAcked(identity []byte) (int64, error) {
s.lastAckedMutex.Lock()
defer s.lastAckedMutex.Unlock()
return s.lastAcked[fmt.Sprintf("%x", identity)], nil
}
func (s *SQLLitePersistence) SetLastAcked(identity []byte, lastAcked int64) error {
s.lastAckedMutex.Lock()
defer s.lastAckedMutex.Unlock()
s.lastAcked[fmt.Sprintf("%x", identity)] = lastAcked
return nil
}

View File

@ -1,57 +0,0 @@
package publisher
import (
"io/ioutil"
"path/filepath"
"testing"
"github.com/status-im/status-go/messaging/chat"
"github.com/stretchr/testify/suite"
)
func TestPersistenceTestSuite(t *testing.T) {
suite.Run(t, new(PersistenceTestSuite))
}
type PersistenceTestSuite struct {
suite.Suite
persistence Persistence
}
func (s *PersistenceTestSuite) SetupTest() {
dir, err := ioutil.TempDir("", "publisher-persistence-test")
s.Require().NoError(err)
p, err := chat.NewSQLLitePersistence(filepath.Join(dir, "db1.sql"), "pass")
s.Require().NoError(err)
s.persistence = NewSQLLitePersistence(p.DB)
}
func (s *PersistenceTestSuite) TestLastAcked() {
identity := []byte("identity")
// Nothing in the database
lastAcked1, err := s.persistence.GetLastAcked(identity)
s.Require().NoError(err)
s.Require().Equal(int64(0), lastAcked1)
err = s.persistence.SetLastAcked(identity, 3)
s.Require().NoError(err)
lastAcked2, err := s.persistence.GetLastAcked(identity)
s.Require().NoError(err)
s.Require().Equal(int64(3), lastAcked2)
}
func (s *PersistenceTestSuite) TestLastPublished() {
lastPublished1, err := s.persistence.GetLastPublished()
s.Require().NoError(err)
s.Require().Equal(int64(0), lastPublished1)
err = s.persistence.SetLastPublished(3)
s.Require().NoError(err)
lastPublished2, err := s.persistence.GetLastPublished()
s.Require().NoError(err)
s.Require().Equal(int64(3), lastPublished2)
}

View File

@ -1,518 +0,0 @@
package publisher
import (
"context"
"crypto/ecdsa"
"database/sql"
"errors"
"fmt"
"time"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/messaging/chat"
"github.com/status-im/status-go/messaging/chat/protobuf"
"github.com/status-im/status-go/messaging/filter"
"github.com/status-im/status-go/messaging/multidevice"
"github.com/status-im/status-go/messaging/sharedsecret"
"github.com/status-im/status-go/services/shhext/whisperutils"
"github.com/golang/protobuf/proto"
whisper "github.com/status-im/whisper/whisperv6"
)
const (
tickerInterval = 120
// How often we should publish a contact code in seconds
publishInterval = 21600
// How often we should check for new messages
pollIntervalMs = 300
// Cooldown period on acking messages when not targeting our device
deviceNotFoundAckInterval = 7200
)
var (
errProtocolNotInitialized = errors.New("protocol is not initialized")
// ErrPFSNotEnabled is returned when an endpoint PFS only is called but
// PFS is disabled.
ErrPFSNotEnabled = errors.New("pfs not enabled")
errNoKeySelected = errors.New("no key selected")
// ErrNoProtocolMessage means that a message was not a protocol message,
// that is it could not be unmarshaled.
ErrNoProtocolMessage = errors.New("not a protocol message")
)
type Publisher struct {
config Config
whisper *whisper.Whisper
online func() bool
whisperAPI *whisper.PublicWhisperAPI
protocol *chat.ProtocolService
persistence Persistence
log log.Logger
filter *filter.Service
quit chan struct{}
ticker *time.Ticker
}
type Config struct {
PFSEnabled bool
}
func New(w *whisper.Whisper, c Config) *Publisher {
return &Publisher{
config: c,
whisper: w,
whisperAPI: whisper.NewPublicWhisperAPI(w),
log: log.New("package", "status-go/messaging/publisher.Publisher"),
}
}
func (p *Publisher) Init(db *sql.DB, protocol *chat.ProtocolService, onNewMessagesHandler func([]*filter.Messages)) {
filterService := filter.New(p.whisper, filter.NewSQLLitePersistence(db), protocol.GetSharedSecretService(), onNewMessagesHandler)
p.persistence = NewSQLLitePersistence(db)
p.protocol = protocol
p.filter = filterService
}
func (p *Publisher) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey, bundle *protobuf.Bundle) ([]*multidevice.Installation, error) {
if p.protocol == nil {
return nil, errProtocolNotInitialized
}
return p.protocol.ProcessPublicBundle(myIdentityKey, bundle)
}
func (p *Publisher) GetBundle(myIdentityKey *ecdsa.PrivateKey) (*protobuf.Bundle, error) {
if p.protocol == nil {
return nil, errProtocolNotInitialized
}
return p.protocol.GetBundle(myIdentityKey)
}
// EnableInstallation enables an installation for multi-device sync.
func (p *Publisher) EnableInstallation(installationID string) error {
if p.protocol == nil {
return errProtocolNotInitialized
}
privateKeyID := p.whisper.SelectedKeyPairID()
if privateKeyID == "" {
return errNoKeySelected
}
privateKey, err := p.whisper.GetPrivateKey(privateKeyID)
if err != nil {
return err
}
return p.protocol.EnableInstallation(&privateKey.PublicKey, installationID)
}
// DisableInstallation disables an installation for multi-device sync.
func (p *Publisher) DisableInstallation(installationID string) error {
if p.protocol == nil {
return errProtocolNotInitialized
}
privateKeyID := p.whisper.SelectedKeyPairID()
if privateKeyID == "" {
return errNoKeySelected
}
privateKey, err := p.whisper.GetPrivateKey(privateKeyID)
if err != nil {
return err
}
return p.protocol.DisableInstallation(&privateKey.PublicKey, installationID)
}
// GetOurInstallations returns all the installations available given an identity
func (p *Publisher) GetOurInstallations() ([]*multidevice.Installation, error) {
if p.protocol == nil {
return nil, errProtocolNotInitialized
}
privateKeyID := p.whisper.SelectedKeyPairID()
if privateKeyID == "" {
return nil, errNoKeySelected
}
privateKey, err := p.whisper.GetPrivateKey(privateKeyID)
if err != nil {
return nil, err
}
return p.protocol.GetOurInstallations(&privateKey.PublicKey)
}
// SetInstallationMetadata sets the metadata for our own installation
func (p *Publisher) SetInstallationMetadata(installationID string, data *multidevice.InstallationMetadata) error {
if p.protocol == nil {
return errProtocolNotInitialized
}
privateKeyID := p.whisper.SelectedKeyPairID()
if privateKeyID == "" {
return errNoKeySelected
}
privateKey, err := p.whisper.GetPrivateKey(privateKeyID)
if err != nil {
return err
}
return p.protocol.SetInstallationMetadata(&privateKey.PublicKey, installationID, data)
}
func (p *Publisher) GetPublicBundle(identityKey *ecdsa.PublicKey) (*protobuf.Bundle, error) {
if p.protocol == nil {
return nil, errProtocolNotInitialized
}
return p.protocol.GetPublicBundle(identityKey)
}
func (p *Publisher) Start(online func() bool, startTicker bool) error {
if p.protocol == nil {
return errProtocolNotInitialized
}
p.online = online
if startTicker {
p.startTicker()
}
go p.filter.Start(pollIntervalMs * time.Millisecond)
return nil
}
func (p *Publisher) Stop() error {
if p.filter != nil {
if err := p.filter.Stop(); err != nil {
log.Error("Failed to stop filter service with error", "err", err)
}
}
return nil
}
func (p *Publisher) getNegotiatedChat(identity *ecdsa.PublicKey) *filter.Chat {
return p.filter.GetNegotiated(identity)
}
func (p *Publisher) LoadFilters(chats []*filter.Chat) ([]*filter.Chat, error) {
return p.filter.Init(chats)
}
func (p *Publisher) LoadFilter(chat *filter.Chat) ([]*filter.Chat, error) {
return p.filter.Load(chat)
}
func (p *Publisher) RemoveFilters(chats []*filter.Chat) error {
return p.filter.Remove(chats)
}
func (p *Publisher) ProcessNegotiatedSecret(secrets []*sharedsecret.Secret) {
for _, secret := range secrets {
_, err := p.filter.ProcessNegotiatedSecret(secret)
if err != nil {
log.Error("could not process negotiated filter", "err", err)
}
}
}
func (p *Publisher) ProcessMessage(msg *whisper.Message, msgID []byte) error {
if !p.config.PFSEnabled {
return ErrPFSNotEnabled
}
privateKeyID := p.whisper.SelectedKeyPairID()
if privateKeyID == "" {
return errNoKeySelected
}
privateKey, err := p.whisper.GetPrivateKey(privateKeyID)
if err != nil {
return err
}
publicKey, err := crypto.UnmarshalPubkey(msg.Sig)
if err != nil {
return err
}
// Unmarshal message
protocolMessage := &protobuf.ProtocolMessage{}
if err := proto.Unmarshal(msg.Payload, protocolMessage); err != nil {
p.log.Debug("Not a protocol message", "err", err)
return ErrNoProtocolMessage
}
response, err := p.protocol.HandleMessage(privateKey, publicKey, protocolMessage, msgID)
if err == nil {
msg.Payload = response
} else if err == chat.ErrDeviceNotFound {
if err := p.handleDeviceNotFound(privateKey, publicKey); err != nil {
p.log.Error("Failed to handle DeviceNotFound", "err", err)
}
// Return the original error
return err
}
return err
}
// CreateDirectMessage creates a 1:1 chat message
func (p *Publisher) CreateDirectMessage(privateKey *ecdsa.PrivateKey, publicKey *ecdsa.PublicKey, DH bool, payload []byte) (*whisper.NewMessage, error) {
if !p.config.PFSEnabled {
return nil, ErrPFSNotEnabled
}
var (
msgSpec *chat.ProtocolMessageSpec
err error
)
if DH {
p.log.Debug("Building dh message")
msgSpec, err = p.protocol.BuildDHMessage(privateKey, publicKey, payload)
} else {
p.log.Debug("Building direct message")
msgSpec, err = p.protocol.BuildDirectMessage(privateKey, publicKey, payload)
}
if err != nil {
return nil, err
}
whisperMessage, err := p.directMessageToWhisper(privateKey, publicKey, msgSpec)
if err != nil {
p.log.Error("sshext-service", "error building whisper message", err)
return nil, err
}
return whisperMessage, nil
}
func (p *Publisher) directMessageToWhisper(myPrivateKey *ecdsa.PrivateKey, theirPublicKey *ecdsa.PublicKey, spec *chat.ProtocolMessageSpec) (*whisper.NewMessage, error) {
// marshal for sending to wire
marshaledMessage, err := proto.Marshal(spec.Message)
if err != nil {
p.log.Error("encryption-service", "error marshaling message", err)
return nil, err
}
// We rely on the fact that a deterministic ID is created for the same keys.
sigID, err := p.whisper.AddKeyPair(myPrivateKey)
if err != nil {
p.log.Error("failed to add key pair in order to get signature ID", "err", err)
return nil, err
}
destination := hexutil.Bytes(crypto.FromECDSAPub(theirPublicKey))
whisperMessage := whisperutils.DefaultWhisperMessage()
whisperMessage.Payload = marshaledMessage
whisperMessage.Sig = sigID
if spec.SharedSecret != nil {
chat := p.getNegotiatedChat(theirPublicKey)
if chat != nil {
p.log.Debug("Sending on negotiated topic", "public-key", destination)
whisperMessage.SymKeyID = chat.SymKeyID
whisperMessage.Topic = chat.Topic
whisperMessage.PublicKey = nil
return &whisperMessage, nil
}
} else if spec.PartitionedTopic() == chat.PartitionTopicV1 {
p.log.Debug("Sending on partitioned topic", "public-key", destination)
// Create filter on demand
if _, err := p.filter.LoadPartitioned(myPrivateKey, theirPublicKey, false); err != nil {
return nil, err
}
t := filter.PublicKeyToPartitionedTopicBytes(theirPublicKey)
whisperMessage.Topic = whisper.BytesToTopic(t)
whisperMessage.PublicKey = destination
return &whisperMessage, nil
}
p.log.Debug("Sending on old discovery topic", "public-key", destination)
whisperMessage.Topic = whisperutils.DiscoveryTopicBytes
whisperMessage.PublicKey = destination
return &whisperMessage, nil
}
// CreatePublicMessage sends a public chat message to the underlying transport
func (p *Publisher) CreatePublicMessage(privateKey *ecdsa.PrivateKey, chatID string, payload []byte, wrap bool) (*whisper.NewMessage, error) {
if !p.config.PFSEnabled {
return nil, ErrPFSNotEnabled
}
filter := p.filter.GetByID(chatID)
if filter == nil {
return nil, errors.New("not subscribed to chat")
}
sigID, err := p.whisper.AddKeyPair(privateKey)
if err != nil {
return nil, fmt.Errorf("failed to get a signature ID: %v", err)
}
p.log.Info("signature ID", sigID)
// Enrich with transport layer info
whisperMessage := whisperutils.DefaultWhisperMessage()
whisperMessage.Sig = sigID
whisperMessage.Topic = whisperutils.ToTopic(chatID)
whisperMessage.SymKeyID = filter.SymKeyID
if wrap {
privateKeyID := p.whisper.SelectedKeyPairID()
if privateKeyID == "" {
return nil, errNoKeySelected
}
privateKey, err := p.whisper.GetPrivateKey(privateKeyID)
if err != nil {
return nil, err
}
message, err := p.protocol.BuildPublicMessage(privateKey, payload)
if err != nil {
return nil, err
}
marshaledMessage, err := proto.Marshal(message)
if err != nil {
p.log.Error("encryption-service", "error marshaling message", err)
return nil, err
}
whisperMessage.Payload = marshaledMessage
} else {
whisperMessage.Payload = payload
}
return &whisperMessage, nil
}
func (p *Publisher) ConfirmMessagesProcessed(ids [][]byte) error {
return p.protocol.ConfirmMessagesProcessed(ids)
}
func (p *Publisher) startTicker() {
p.ticker = time.NewTicker(tickerInterval * time.Second)
p.quit = make(chan struct{})
go func() {
for {
select {
case <-p.ticker.C:
_, err := p.sendContactCode()
if err != nil {
p.log.Error("could not execute tick", "err", err)
}
case <-p.quit:
p.ticker.Stop()
return
}
}
}()
}
func (p *Publisher) sendContactCode() (*whisper.NewMessage, error) {
p.log.Info("publishing bundle")
if !p.config.PFSEnabled {
return nil, nil
}
if p.persistence == nil {
p.log.Info("not initialized, skipping")
return nil, nil
}
lastPublished, err := p.persistence.GetLastPublished()
if err != nil {
p.log.Error("could not fetch config from db", "err", err)
return nil, err
}
now := time.Now().Unix()
if now-lastPublished < publishInterval {
p.log.Debug("nothing to do")
return nil, nil
}
if !p.online() {
p.log.Debug("not connected")
return nil, nil
}
privateKeyID := p.whisper.SelectedKeyPairID()
if privateKeyID == "" {
return nil, errNoKeySelected
}
privateKey, err := p.whisper.GetPrivateKey(privateKeyID)
if err != nil {
return nil, err
}
identity := fmt.Sprintf("%x", crypto.FromECDSAPub(&privateKey.PublicKey))
message, err := p.CreatePublicMessage(privateKey, filter.ContactCodeTopic(identity), nil, true)
if err != nil {
p.log.Error("could not build contact code", "identity", identity, "err", err)
return nil, err
}
_, err = p.whisperAPI.Post(context.TODO(), *message)
if err != nil {
p.log.Error("could not publish contact code on whisper", "identity", identity, "err", err)
return nil, err
}
err = p.persistence.SetLastPublished(now)
if err != nil {
p.log.Error("could not set last published", "err", err)
return nil, err
}
return message, nil
}
// handleDeviceNotFound sends an empty message to publicKey containing our bundle information
// so it's notified of our devices
func (p *Publisher) handleDeviceNotFound(privateKey *ecdsa.PrivateKey, publicKey *ecdsa.PublicKey) error {
now := time.Now().Unix()
identity := crypto.CompressPubkey(publicKey)
lastAcked, err := p.persistence.GetLastAcked(identity)
if err != nil {
return err
}
if now-lastAcked < deviceNotFoundAckInterval {
p.log.Debug("already acked identity", "identity", identity, "lastAcked", lastAcked)
return nil
}
message, err := p.CreateDirectMessage(privateKey, publicKey, true, nil)
if err != nil {
return err
}
_, err = p.whisperAPI.Post(context.TODO(), *message)
if err != nil {
return err
}
return p.persistence.SetLastAcked(identity, now)
}

View File

@ -1,207 +0,0 @@
package publisher
import (
"crypto/ecdsa"
"fmt"
"io/ioutil"
"path/filepath"
"testing"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/ecies"
"github.com/status-im/status-go/messaging/chat"
"github.com/status-im/status-go/messaging/filter"
"github.com/status-im/status-go/messaging/multidevice"
"github.com/status-im/status-go/messaging/sharedsecret"
"github.com/status-im/status-go/services/shhext/whisperutils"
whisper "github.com/status-im/whisper/whisperv6"
"github.com/stretchr/testify/suite"
)
func TestServiceTestSuite(t *testing.T) {
suite.Run(t, new(ServiceTestSuite))
}
type TestKey struct {
privateKey *ecdsa.PrivateKey
keyID string
publicKeyBytes hexutil.Bytes
}
type ServiceTestSuite struct {
suite.Suite
alice *Publisher
bob *Publisher
aliceKey *TestKey
bobKey *TestKey
}
func (s *ServiceTestSuite) createPublisher(installationID string) (*Publisher, *TestKey) {
dir, err := ioutil.TempDir("", "publisher-test")
s.Require().NoError(err)
config := Config{PFSEnabled: true}
whisper := whisper.New(nil)
err = whisper.SetMinimumPoW(0)
s.Require().NoError(err)
publisher := New(whisper, config)
pk, err := crypto.GenerateKey()
s.Require().NoError(err)
keyID, err := whisper.AddKeyPair(pk)
s.Require().NoError(err)
testKey := TestKey{
privateKey: pk,
keyID: keyID,
publicKeyBytes: crypto.FromECDSAPub(&pk.PublicKey),
}
persistence, err := chat.NewSQLLitePersistence(filepath.Join(dir, "db1.sql"), "pass")
s.Require().NoError(err)
sharedSecretService := sharedsecret.NewService(persistence.GetSharedSecretStorage())
multideviceConfig := &multidevice.Config{
InstallationID: installationID,
ProtocolVersion: chat.ProtocolVersion,
MaxInstallations: 3,
}
multideviceService := multidevice.New(multideviceConfig, persistence.GetMultideviceStorage())
protocolService := chat.NewProtocolService(
chat.NewEncryptionService(
persistence,
chat.DefaultEncryptionServiceConfig(installationID)),
sharedSecretService,
multideviceService,
func(addedBundles []*multidevice.Installation) {},
publisher.ProcessNegotiatedSecret,
)
publisher.Init(persistence.DB, protocolService, func(msg []*filter.Messages) {})
err = publisher.Start(func() bool { return true }, false)
s.Require().NoError(err)
return publisher, &testKey
}
func (s *ServiceTestSuite) SetupTest() {
s.alice, s.aliceKey = s.createPublisher("installation-1")
_, err := s.alice.LoadFilters([]*filter.Chat{})
s.Require().NoError(err)
s.bob, s.bobKey = s.createPublisher("installation-2")
_, err = s.bob.LoadFilters([]*filter.Chat{})
s.Require().NoError(err)
}
func (s *ServiceTestSuite) TestCreateDirectMessage() {
newMessage, err := s.alice.CreateDirectMessage(s.aliceKey.privateKey, &s.bobKey.privateKey.PublicKey, false, []byte("hello"))
s.Require().NoError(err)
message := &whisper.Message{
Sig: s.aliceKey.publicKeyBytes,
Topic: newMessage.Topic,
Payload: newMessage.Payload,
Dst: newMessage.PublicKey,
}
err = s.bob.ProcessMessage(message, []byte("1"))
s.Require().NoError(err)
s.Require().Equal([]byte("hello"), message.Payload)
}
func (s *ServiceTestSuite) TestTopic() {
// We build an initial message
newMessage1, err := s.alice.CreateDirectMessage(s.aliceKey.privateKey, &s.bobKey.privateKey.PublicKey, false, []byte("hello"))
s.Require().NoError(err)
message1 := &whisper.Message{
Sig: s.aliceKey.publicKeyBytes,
Topic: newMessage1.Topic,
Payload: newMessage1.Payload,
Dst: newMessage1.PublicKey,
}
// We have no information, it should use the discovery topic
s.Require().Equal(whisperutils.DiscoveryTopicBytes, message1.Topic)
// We build a contact code from user 2
newMessage2, err := s.bob.sendContactCode()
s.Require().NoError(err)
s.Require().NotNil(newMessage2)
message2 := &whisper.Message{
Sig: s.bobKey.publicKeyBytes,
Topic: newMessage2.Topic,
Payload: newMessage2.Payload,
Dst: newMessage2.PublicKey,
}
// We receive the contact code
err = s.alice.ProcessMessage(message2, []byte("1"))
s.Require().EqualError(err, chat.ErrNoPayload.Error())
// We build another message, this time it should use the partitioned topic
newMessage3, err := s.alice.CreateDirectMessage(s.aliceKey.privateKey, &s.bobKey.privateKey.PublicKey, false, []byte("hello"))
s.Require().NoError(err)
message3 := &whisper.Message{
Sig: s.aliceKey.publicKeyBytes,
Topic: newMessage3.Topic,
Payload: newMessage3.Payload,
Dst: newMessage3.PublicKey,
}
expectedTopic3 := whisper.BytesToTopic(filter.PublicKeyToPartitionedTopicBytes(&s.bobKey.privateKey.PublicKey))
s.Require().Equal(expectedTopic3, message3.Topic)
// We receive the message
err = s.bob.ProcessMessage(message3, []byte("1"))
s.Require().NoError(err)
// We build another message, this time it should use the negotiated topic
newMessage4, err := s.bob.CreateDirectMessage(s.bobKey.privateKey, &s.aliceKey.privateKey.PublicKey, false, []byte("hello"))
s.Require().NoError(err)
message4 := &whisper.Message{
Sig: s.bobKey.publicKeyBytes,
Topic: newMessage4.Topic,
Payload: newMessage4.Payload,
Dst: newMessage4.PublicKey,
}
sharedSecret, err := ecies.ImportECDSA(s.bobKey.privateKey).GenerateShared(
ecies.ImportECDSAPublic(&s.aliceKey.privateKey.PublicKey),
16,
16)
s.Require().NoError(err)
keyString := fmt.Sprintf("%x", sharedSecret)
negotiatedTopic := whisper.BytesToTopic(filter.ToTopic(keyString))
s.Require().Equal(negotiatedTopic, message4.Topic)
// We receive the message
err = s.alice.ProcessMessage(message4, []byte("1"))
s.Require().NoError(err)
// Alice sends another message to Bob, this time it should use the negotiated topic
newMessage5, err := s.alice.CreateDirectMessage(s.aliceKey.privateKey, &s.bobKey.privateKey.PublicKey, false, []byte("hello"))
s.Require().NoError(err)
message5 := &whisper.Message{
Sig: s.aliceKey.publicKeyBytes,
Topic: newMessage5.Topic,
Payload: newMessage5.Payload,
Dst: newMessage5.PublicKey,
}
s.Require().NoError(err)
s.Require().Equal(negotiatedTopic, message5.Topic)
}

View File

@ -1,104 +0,0 @@
package sharedsecret
import (
"crypto/ecdsa"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/ecies"
"github.com/ethereum/go-ethereum/log"
)
const sskLen = 16
type Service struct {
log log.Logger
persistence Persistence
}
func NewService(persistence Persistence) *Service {
return &Service{
log: log.New("package", "status-go/messaging/sharedsecret.Service"),
persistence: persistence,
}
}
func (s *Service) setup(myPrivateKey *ecdsa.PrivateKey, theirPublicKey *ecdsa.PublicKey, installationID string) (*Secret, error) {
s.log.Debug("Setup called for", "installationID", installationID)
sharedKey, err := ecies.ImportECDSA(myPrivateKey).GenerateShared(
ecies.ImportECDSAPublic(theirPublicKey),
sskLen,
sskLen,
)
if err != nil {
return nil, err
}
theirIdentity := crypto.CompressPubkey(theirPublicKey)
if err = s.persistence.Add(theirIdentity, sharedKey, installationID); err != nil {
return nil, err
}
return &Secret{Key: sharedKey, Identity: theirPublicKey}, err
}
// Receive will generate a shared secret for a given identity, and return it
func (s *Service) Receive(myPrivateKey *ecdsa.PrivateKey, theirPublicKey *ecdsa.PublicKey, installationID string) (*Secret, error) {
s.log.Debug("Received message, setting up topic", "public-key", theirPublicKey, "installation-id", installationID)
return s.setup(myPrivateKey, theirPublicKey, installationID)
}
// Send returns a shared key and whether it has been acknowledged from all the installationIDs
func (s *Service) Send(myPrivateKey *ecdsa.PrivateKey, myInstallationID string, theirPublicKey *ecdsa.PublicKey, theirInstallationIDs []string) (*Secret, bool, error) {
s.log.Debug("Checking against:", "installation-ids", theirInstallationIDs)
secret, err := s.setup(myPrivateKey, theirPublicKey, myInstallationID)
if err != nil {
return nil, false, err
}
if len(theirInstallationIDs) == 0 {
return secret, false, nil
}
theirIdentity := crypto.CompressPubkey(theirPublicKey)
response, err := s.persistence.Get(theirIdentity, theirInstallationIDs)
if err != nil {
return nil, false, err
}
for _, installationID := range theirInstallationIDs {
if !response.installationIDs[installationID] {
s.log.Debug("no shared secret with:", "installation-id", installationID)
return secret, false, nil
}
}
s.log.Debug("shared secret found")
return secret, true, nil
}
type Secret struct {
Identity *ecdsa.PublicKey
Key []byte
}
func (s *Service) All() ([]*Secret, error) {
var secrets []*Secret
tuples, err := s.persistence.All()
if err != nil {
return nil, err
}
for _, tuple := range tuples {
key, err := crypto.DecompressPubkey(tuple[0])
if err != nil {
return nil, err
}
secrets = append(secrets, &Secret{Identity: key, Key: tuple[1]})
}
return secrets, nil
}

View File

@ -1,114 +0,0 @@
package sharedsecret
import (
"io/ioutil"
"os"
"testing"
"github.com/ethereum/go-ethereum/crypto"
msgdb "github.com/status-im/status-go/messaging/db"
"github.com/stretchr/testify/suite"
)
func TestServiceTestSuite(t *testing.T) {
suite.Run(t, new(ServiceTestSuite))
}
type ServiceTestSuite struct {
suite.Suite
service *Service
path string
}
func (s *ServiceTestSuite) SetupTest() {
dbFile, err := ioutil.TempFile(os.TempDir(), "sharedsecret")
s.Require().NoError(err)
s.path = dbFile.Name()
db, err := msgdb.Open(s.path, "", 0)
s.Require().NoError(err)
s.service = NewService(NewSQLLitePersistence(db))
}
func (s *ServiceTestSuite) TearDownTest() {
os.Remove(s.path)
}
func (s *ServiceTestSuite) TestSingleInstallationID() {
ourInstallationID := "our"
installationID1 := "1"
installationID2 := "2"
myKey, err := crypto.GenerateKey()
s.Require().NoError(err)
theirKey, err := crypto.GenerateKey()
s.Require().NoError(err)
// We receive a message from installationID1
sharedKey1, err := s.service.Receive(myKey, &theirKey.PublicKey, installationID1)
s.Require().NoError(err)
s.Require().NotNil(sharedKey1, "it generates a shared key")
// We want to send a message to installationID1
sharedKey2, agreed2, err := s.service.Send(myKey, ourInstallationID, &theirKey.PublicKey, []string{installationID1})
s.Require().NoError(err)
s.Require().True(agreed2)
s.Require().NotNil(sharedKey2, "We can retrieve a shared secret")
s.Require().Equal(sharedKey1, sharedKey2, "The shared secret is the same as the one stored")
// We want to send a message to multiple installationIDs, one of which we haven't never communicated with
sharedKey3, agreed3, err := s.service.Send(myKey, ourInstallationID, &theirKey.PublicKey, []string{installationID1, installationID2})
s.Require().NoError(err)
s.Require().NotNil(sharedKey3, "A shared key is returned")
s.Require().False(agreed3)
// We receive a message from installationID2
sharedKey4, err := s.service.Receive(myKey, &theirKey.PublicKey, installationID2)
s.Require().NoError(err)
s.Require().NotNil(sharedKey4, "it generates a shared key")
s.Require().Equal(sharedKey1, sharedKey4, "It generates the same key")
// We want to send a message to installationID 1 & 2, both have been
sharedKey5, agreed5, err := s.service.Send(myKey, ourInstallationID, &theirKey.PublicKey, []string{installationID1, installationID2})
s.Require().NoError(err)
s.Require().NotNil(sharedKey5, "We can retrieve a shared secret")
s.Require().True(agreed5)
s.Require().Equal(sharedKey1, sharedKey5, "The shared secret is the same as the one stored")
}
func (s *ServiceTestSuite) TestAll() {
installationID1 := "1"
installationID2 := "2"
myKey, err := crypto.GenerateKey()
s.Require().NoError(err)
theirKey1, err := crypto.GenerateKey()
s.Require().NoError(err)
theirKey2, err := crypto.GenerateKey()
s.Require().NoError(err)
// We receive a message from user 1
sharedKey1, err := s.service.Receive(myKey, &theirKey1.PublicKey, installationID1)
s.Require().NoError(err)
s.Require().NotNil(sharedKey1, "it generates a shared key")
// We receive a message from user 2
sharedKey2, err := s.service.Receive(myKey, &theirKey2.PublicKey, installationID2)
s.Require().NoError(err)
s.Require().NotNil(sharedKey2, "it generates a shared key")
// All the secrets are there
secrets, err := s.service.All()
s.Require().NoError(err)
expected := []*Secret{
sharedKey1,
sharedKey2,
}
s.Require().Equal(expected, secrets)
}

View File

@ -9,20 +9,21 @@ import (
"math/big"
"time"
"github.com/status-im/status-go/services/shhext/dedup"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/rlp"
"github.com/status-im/status-go/db"
"github.com/status-im/status-go/mailserver"
"github.com/status-im/status-go/messaging/filter"
"github.com/status-im/status-go/messaging/multidevice"
"github.com/status-im/status-go/services/shhext/dedup"
"github.com/status-im/status-go/services/shhext/mailservers"
whisper "github.com/status-im/whisper/whisperv6"
"github.com/status-im/status-protocol-go/encryption/multidevice"
"github.com/status-im/status-protocol-go/transport/whisper/filter"
)
const (
@ -188,18 +189,6 @@ func NewPublicAPI(s *Service) *PublicAPI {
}
}
// Post shamelessly copied from whisper codebase with slight modifications.
func (api *PublicAPI) Post(ctx context.Context, req whisper.NewMessage) (hexutil.Bytes, error) {
hexID, err := api.publicAPI.Post(ctx, req)
if err == nil {
api.service.envelopesMonitor.Add(common.BytesToHash(hexID), req)
} else {
return nil, err
}
mID := messageID(req)
return mID[:], err
}
func (api *PublicAPI) getPeer(rawurl string) (*enode.Node, error) {
if len(rawurl) == 0 {
return mailservers.GetFirstConnected(api.service.server, api.service.peerStore)
@ -409,14 +398,13 @@ func (api *PublicAPI) SyncMessages(ctx context.Context, r SyncMessagesRequest) (
}
}
// GetNewFilterMessages is a prototype method with deduplication
// GetNewFilterMessages is a prototype method with deduplication.
func (api *PublicAPI) GetNewFilterMessages(filterID string) ([]dedup.DeduplicateMessage, error) {
msgs, err := api.publicAPI.GetFilterMessages(filterID)
messages, err := api.publicAPI.GetFilterMessages(filterID)
if err != nil {
return nil, err
}
return api.service.processReceivedMessages(msgs)
return api.service.deduplicator.Deduplicate(messages), nil
}
// ConfirmMessagesProcessed is a method to confirm that messages was consumed by
@ -443,47 +431,49 @@ func (api *PublicAPI) ConfirmMessagesProcessed(messages []*whisper.Message) (err
// ConfirmMessagesProcessedByID is a method to confirm that messages was consumed by
// the client side.
// TODO: this is broken now as it requires dedup ID while a message hash should be used.
func (api *PublicAPI) ConfirmMessagesProcessedByID(messageIDs [][]byte) error {
if err := api.service.ConfirmMessagesProcessed(messageIDs); err != nil {
/*if err := api.service.ConfirmMessagesProcessed(messageIDs); err != nil {
return err
}
}*/
return api.service.deduplicator.AddMessageByID(messageIDs)
}
// SendPublicMessage sends a public chat message to the underlying transport
func (api *PublicAPI) SendPublicMessage(ctx context.Context, msg SendPublicMessageRPC) (hexutil.Bytes, error) {
privateKey, err := api.service.w.GetPrivateKey(msg.Sig)
if err != nil {
return nil, fmt.Errorf("failed to obtain a private key from Sig: %v", err)
}
message, err := api.service.CreatePublicMessage(privateKey, msg.Chat, msg.Payload, false)
// Post is used to send one-to-one for those who did not enabled device-to-device sync,
// in other words don't use PFS-enabled messages. Otherwise, SendDirectMessage is used.
// It's important to call PublicAPI.afterSend() so that the client receives a signal
// with confirmation that the message left the device.
func (api *PublicAPI) Post(ctx context.Context, newMessage whisper.NewMessage) (hexutil.Bytes, error) {
hash, err := api.publicAPI.Post(ctx, newMessage)
if err != nil {
return nil, err
}
return api.service.afterPost(hash, newMessage), nil
}
return api.Post(ctx, *message)
// SendPublicMessage sends a public chat message to the underlying transport.
// Message's payload is a transit encoded message.
// It's important to call PublicAPI.afterSend() so that the client receives a signal
// with confirmation that the message left the device.
func (api *PublicAPI) SendPublicMessage(ctx context.Context, msg SendPublicMessageRPC) (hexutil.Bytes, error) {
hash, newMessage, err := api.service.messenger.SendRaw(ctx, msg, msg.Payload)
if err != nil {
return nil, err
}
return api.service.afterPost(hash, newMessage), nil
}
// SendDirectMessage sends a 1:1 chat message to the underlying transport
// Message's payload is a transit encoded message.
// It's important to call PublicAPI.afterSend() so that the client receives a signal
// with confirmation that the message left the device.
func (api *PublicAPI) SendDirectMessage(ctx context.Context, msg SendDirectMessageRPC) (hexutil.Bytes, error) {
privateKey, err := api.service.w.GetPrivateKey(msg.Sig)
hash, newMessage, err := api.service.messenger.SendRaw(ctx, msg, msg.Payload)
if err != nil {
return nil, err
}
publicKey, err := crypto.UnmarshalPubkey(msg.PubKey)
if err != nil {
return nil, err
}
message, err := api.service.CreateDirectMessage(privateKey, publicKey, msg.DH, msg.Payload)
if err != nil {
return nil, err
}
return api.Post(ctx, *message)
return api.service.afterPost(hash, newMessage), nil
}
func (api *PublicAPI) requestMessagesUsingPayload(request db.HistoryRequest, peer, symkeyID string, payload []byte, force bool, timeout time.Duration, topics []whisper.TopicType) (hash common.Hash, err error) {
@ -594,39 +584,32 @@ func (api *PublicAPI) CompleteRequest(parent context.Context, hex string) (err e
return err
}
// LoadFilters load all the necessary filters
func (api *PublicAPI) LoadFilters(parent context.Context, chats []*filter.Chat) ([]*filter.Chat, error) {
return api.service.LoadFilters(chats)
return api.service.messenger.LoadFilters(chats)
}
// LoadFilter load a single filter
func (api *PublicAPI) LoadFilter(parent context.Context, chat *filter.Chat) ([]*filter.Chat, error) {
return api.service.LoadFilter(chat)
}
// RemoveFilter remove a single filter
func (api *PublicAPI) RemoveFilters(parent context.Context, chats []*filter.Chat) error {
return api.service.RemoveFilters(chats)
return api.service.messenger.RemoveFilters(chats)
}
// EnableInstallation enables an installation for multi-device sync.
func (api *PublicAPI) EnableInstallation(installationID string) error {
return api.service.EnableInstallation(installationID)
return api.service.messenger.EnableInstallation(installationID)
}
// DisableInstallation disables an installation for multi-device sync.
func (api *PublicAPI) DisableInstallation(installationID string) error {
return api.service.DisableInstallation(installationID)
return api.service.messenger.DisableInstallation(installationID)
}
// GetOurInstallations returns all the installations available given an identity
func (api *PublicAPI) GetOurInstallations() ([]*multidevice.Installation, error) {
return api.service.GetOurInstallations()
return api.service.messenger.Installations()
}
// SetInstallationMetadata sets the metadata for our own installation
func (api *PublicAPI) SetInstallationMetadata(installationID string, data *multidevice.InstallationMetadata) error {
return api.service.SetInstallationMetadata(installationID, data)
return api.service.messenger.SetInstallationMetadata(installationID, data)
}
// -----

View File

@ -4,12 +4,16 @@ import (
"context"
"encoding/hex"
"fmt"
"github.com/ethereum/go-ethereum/crypto"
"github.com/status-im/status-go/params"
"io/ioutil"
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/status-im/status-go/mailserver"
protocol "github.com/status-im/status-protocol-go"
whisper "github.com/status-im/whisper/whisperv6"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -237,3 +241,56 @@ func TestExpiredOrCompleted(t *testing.T) {
require.EqualError(t, err, fmt.Sprintf("request %x expired", hash))
}
}
func TestAfterPostIsCalled(t *testing.T) {
shhConfig := whisper.DefaultConfig
shhConfig.MinimumAcceptedPOW = 0
w := whisper.New(&shhConfig)
symKeyID, err := w.AddSymKeyFromPassword("abc")
require.NoError(t, err)
identity, err := crypto.GenerateKey()
require.NoError(t, err)
tmpDir, err := ioutil.TempDir("", "test-after-post")
require.NoError(t, err)
service := New(w, nil, nil, params.ShhextConfig{})
service.messenger, err = protocol.NewMessenger(
identity,
nil,
w,
tmpDir,
"encKey",
"installation1",
)
require.NoError(t, err)
api := NewPublicAPI(service)
// Using api.Post()
hash1, err := api.Post(context.Background(), whisper.NewMessage{
SymKeyID: symKeyID,
Topic: stringToTopic("topic"),
})
require.NoError(t, err)
require.Equal(t, EnvelopePosted, service.envelopesMonitor.GetMessageState(common.BytesToHash(hash1)))
// Using api.SendPublicMessage()
hash2, err := api.SendPublicMessage(context.Background(), SendPublicMessageRPC{
Chat: "test-channel",
Payload: []byte("abc"),
})
require.NoError(t, err)
require.Equal(t, EnvelopePosted, service.envelopesMonitor.GetMessageState(common.BytesToHash(hash2)))
// Using api.SendDirectMessage()
recipient, err := crypto.GenerateKey()
require.NoError(t, err)
hash3, err := api.SendDirectMessage(context.Background(), SendDirectMessageRPC{
PubKey: crypto.FromECDSAPub(&recipient.PublicKey),
Payload: []byte("abc"),
})
require.NoError(t, err)
require.Equal(t, EnvelopePosted, service.envelopesMonitor.GetMessageState(common.BytesToHash(hash3)))
}

View File

@ -1,15 +1,12 @@
package db
package shhext
import (
"database/sql"
"fmt"
"os"
sqlite "github.com/mutecomm/go-sqlcipher" // We require go sqlcipher that overrides default implementation
"github.com/status-im/migrate/v4"
"github.com/status-im/migrate/v4/database/sqlcipher"
"github.com/status-im/migrate/v4/source/go_bindata"
"github.com/status-im/status-go/messaging/db/migrations"
sqliteDriver "github.com/mutecomm/go-sqlcipher" // We require go sqlcipher that overrides default implementation
"github.com/status-im/status-protocol-go/sqlite"
)
const exportDB = "SELECT sqlcipher_export('newdb')"
@ -25,7 +22,7 @@ const defaultKdfIterationsNumber = 64000
// https://notes.status.im/i8Y_l7ccTiOYq09HVgoFwA
const KdfIterationsNumber = 3200
func MigrateDBFile(oldPath string, newPath string, oldKey string, newKey string) error {
func migrateDBFile(oldPath string, newPath string, oldKey string, newKey string) error {
_, err := os.Stat(oldPath)
// No files, nothing to do
@ -42,7 +39,7 @@ func MigrateDBFile(oldPath string, newPath string, oldKey string, newKey string)
return err
}
db, err := Open(newPath, oldKey, defaultKdfIterationsNumber)
db, err := sqlite.OpenWithIter(newPath, oldKey, defaultKdfIterationsNumber, sqlite.MigrationConfig{})
if err != nil {
return err
}
@ -54,16 +51,15 @@ func MigrateDBFile(oldPath string, newPath string, oldKey string, newKey string)
}
return nil
}
// MigrateDBKeyKdfIterations changes the number of kdf iterations executed
// migrateDBKeyKdfIterations changes the number of kdf iterations executed
// during the database key derivation. This change is necessary because
// of performance reasons.
// https://github.com/status-im/status-go/pull/1343
// `sqlcipher_export` is used for migration, check out this link for details:
// https://www.zetetic.net/sqlcipher/sqlcipher-api/#sqlcipher_export
func MigrateDBKeyKdfIterations(oldPath string, newPath string, key string) error {
func migrateDBKeyKdfIterations(oldPath string, newPath string, key string) error {
_, err := os.Stat(oldPath)
// No files, nothing to do
@ -76,7 +72,7 @@ func MigrateDBKeyKdfIterations(oldPath string, newPath string, key string) error
return err
}
isEncrypted, err := sqlite.IsEncrypted(oldPath)
isEncrypted, err := sqliteDriver.IsEncrypted(oldPath)
if err != nil {
return err
}
@ -86,7 +82,7 @@ func MigrateDBKeyKdfIterations(oldPath string, newPath string, key string) error
return os.Rename(oldPath, newPath)
}
db, err := Open(oldPath, key, defaultKdfIterationsNumber)
db, err := sqlite.OpenWithIter(oldPath, key, defaultKdfIterationsNumber, sqlite.MigrationConfig{})
if err != nil {
return err
}
@ -119,8 +115,8 @@ func MigrateDBKeyKdfIterations(oldPath string, newPath string, key string) error
return os.Remove(oldPath)
}
// EncryptDatabase encrypts an unencrypted database with key
func EncryptDatabase(oldPath string, newPath string, key string) error {
// encryptDatabase encrypts an unencrypted database with key
func encryptDatabase(oldPath string, newPath string, key string) error {
_, err := os.Stat(oldPath)
// No files, nothing to do
@ -133,7 +129,7 @@ func EncryptDatabase(oldPath string, newPath string, key string) error {
return err
}
isEncrypted, err := sqlite.IsEncrypted(oldPath)
isEncrypted, err := sqliteDriver.IsEncrypted(oldPath)
if err != nil {
return err
}
@ -143,7 +139,7 @@ func EncryptDatabase(oldPath string, newPath string, key string) error {
return os.Rename(oldPath, newPath)
}
db, err := Open(oldPath, "", defaultKdfIterationsNumber)
db, err := sqlite.OpenWithIter(oldPath, "", defaultKdfIterationsNumber, sqlite.MigrationConfig{})
if err != nil {
return err
}
@ -175,70 +171,3 @@ func EncryptDatabase(oldPath string, newPath string, key string) error {
return os.Remove(oldPath)
}
func migrateDB(db *sql.DB) error {
resources := bindata.Resource(
migrations.AssetNames(),
func(name string) ([]byte, error) {
return migrations.Asset(name)
},
)
source, err := bindata.WithInstance(resources)
if err != nil {
return err
}
driver, err := sqlcipher.WithInstance(db, &sqlcipher.Config{})
if err != nil {
return err
}
m, err := migrate.NewWithInstance(
"go-bindata",
source,
"sqlcipher",
driver)
if err != nil {
return err
}
if err = m.Up(); err != migrate.ErrNoChange {
return err
}
return nil
}
func Open(path string, key string, kdfIter int) (*sql.DB, error) {
db, err := sql.Open("sqlite3", path)
if err != nil {
return nil, err
}
keyString := fmt.Sprintf("PRAGMA key = '%s'", key)
// Disable concurrent access as not supported by the driver
db.SetMaxOpenConns(1)
if _, err = db.Exec("PRAGMA foreign_keys=ON"); err != nil {
return nil, err
}
if _, err = db.Exec(keyString); err != nil {
return nil, err
}
kdfString := fmt.Sprintf("PRAGMA kdf_iter = '%d'", kdfIter)
if _, err = db.Exec(kdfString); err != nil {
return nil, err
}
// Migrate db
if err = migrateDB(db); err != nil {
return nil, err
}
return db, nil
}

49
services/shhext/file.go Normal file
View File

@ -0,0 +1,49 @@
package shhext
import (
"io"
"io/ioutil"
"os"
"path/filepath"
)
// copyFile implementation is borrowed from https://go-review.googlesource.com/c/go/+/1591
// which didn't make into ioutil package.
// A slight modification is that the file permissions are copied from the source file.
// Another modification is the order of parameters which was reversed.
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
inPerm, err := in.Stat()
if err != nil {
return err
}
tmp, err := ioutil.TempFile(filepath.Dir(dst), "")
if err != nil {
return err
}
_, err = io.Copy(tmp, in)
if err != nil {
tmp.Close()
os.Remove(tmp.Name())
return err
}
if err = tmp.Close(); err != nil {
os.Remove(tmp.Name())
return err
}
if err = os.Chmod(tmp.Name(), inPerm.Mode()); err != nil {
os.Remove(tmp.Name())
return err
}
return os.Rename(tmp.Name(), dst)
}

View File

@ -4,21 +4,63 @@
package shhext
import (
"crypto/ecdsa"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
)
// SendPublicMessageRPC represents the RPC payload for the SendPublicMessage RPC method
type SendPublicMessageRPC struct {
Sig string
Sig string // TODO: remove
Chat string
Payload hexutil.Bytes
}
// TODO: implement with accordance to https://github.com/status-im/status-protocol-go/issues/28.
func (m SendPublicMessageRPC) ID() string { return m.Chat }
func (m SendPublicMessageRPC) PublicName() string { return m.Chat }
func (m SendPublicMessageRPC) PublicKey() *ecdsa.PublicKey { return nil }
// SendDirectMessageRPC represents the RPC payload for the SendDirectMessage RPC method
type SendDirectMessageRPC struct {
Sig string
Sig string // TODO: remove
Chat string
Payload hexutil.Bytes
PubKey hexutil.Bytes
DH bool
DH bool // TODO: make sure to remove safely
}
// TODO: implement with accordance to https://github.com/status-im/status-protocol-go/issues/28.
func (m SendDirectMessageRPC) ID() string { return "" }
func (m SendDirectMessageRPC) PublicName() string { return "" }
func (m SendDirectMessageRPC) PublicKey() *ecdsa.PublicKey {
publicKey, _ := crypto.UnmarshalPubkey(m.PubKey)
return publicKey
}
type JoinRPC struct {
Chat string
PubKey hexutil.Bytes
Payload hexutil.Bytes
}
func (m JoinRPC) ID() string { return m.Chat }
func (m JoinRPC) PublicName() string {
if len(m.PubKey) > 0 {
return ""
}
return m.Chat
}
func (m JoinRPC) PublicKey() *ecdsa.PublicKey {
if len(m.PubKey) > 0 {
return nil
}
publicKey, _ := crypto.UnmarshalPubkey(m.PubKey)
return publicKey
}

38
services/shhext/server.go Normal file
View File

@ -0,0 +1,38 @@
package shhext
import (
"crypto/ecdsa"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/p2p/enode"
)
type server struct {
server *p2p.Server
}
func (s *server) NodeID() *ecdsa.PrivateKey {
return s.server.PrivateKey
}
func (s *server) Online() bool {
return s.server.PeerCount() != 0
}
func (s *server) AddPeer(url string) error {
parsedNode, err := enode.ParseV4(url)
if err != nil {
return err
}
s.server.AddPeer(parsedNode)
return nil
}
func (s *server) Connected(id enode.ID) (bool, error) {
for _, p := range s.server.Peers() {
if p.ID() == id {
return true, nil
}
}
return false, nil
}

View File

@ -4,6 +4,8 @@ import (
"context"
"crypto/ecdsa"
"fmt"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/status-im/status-go/logutils"
"os"
"path/filepath"
"time"
@ -16,17 +18,12 @@ import (
"github.com/ethereum/go-ethereum/rpc"
"github.com/status-im/status-go/db"
"github.com/status-im/status-go/messaging/chat"
msgdb "github.com/status-im/status-go/messaging/db"
"github.com/status-im/status-go/messaging/filter"
"github.com/status-im/status-go/messaging/multidevice"
"github.com/status-im/status-go/messaging/publisher"
"github.com/status-im/status-go/messaging/sharedsecret"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/services/shhext/dedup"
"github.com/status-im/status-go/services/shhext/mailservers"
"github.com/status-im/status-go/signal"
protocol "github.com/status-im/status-protocol-go"
whisper "github.com/status-im/whisper/whisperv6"
"github.com/syndtr/goleveldb/leveldb"
"golang.org/x/crypto/sha3"
@ -37,8 +34,6 @@ const (
defaultConnectionsTarget = 1
// defaultTimeoutWaitAdded is a timeout to use to establish initial connections.
defaultTimeoutWaitAdded = 5 * time.Second
// maxInstallations is a maximum number of supported devices for one account.
maxInstallations = 3
)
// EnvelopeEventsHandler used for two different event types.
@ -51,7 +46,9 @@ type EnvelopeEventsHandler interface {
// Service is a service that provides some additional Whisper API.
type Service struct {
*publisher.Publisher
messenger *protocol.Messenger
cancelMessenger chan struct{}
storage db.TransactionalStorage
w *whisper.Whisper
config params.ShhextConfig
@ -66,7 +63,6 @@ type Service struct {
cache *mailservers.Cache
connManager *mailservers.ConnectionManager
lastUsedMonitor *mailservers.LastUsedConnectionMonitor
filter *filter.Service
}
// Make sure that Service implements node.Service interface.
@ -89,9 +85,7 @@ func New(w *whisper.Whisper, handler EnvelopeEventsHandler, ldb *leveldb.DB, con
requestsRegistry: requestsRegistry,
}
envelopesMonitor := NewEnvelopesMonitor(w, handler, config.MailServerConfirmations, ps, config.MaxMessageDeliveryAttempts)
publisher := publisher.New(w, publisher.Config{PFSEnabled: config.PFSEnabled})
return &Service{
Publisher: publisher,
storage: db.NewLevelDBStorage(ldb),
w: w,
config: config,
@ -133,11 +127,11 @@ func (s *Service) initProtocol(address, encKey, password string) error {
v4Path := filepath.Join(dataDir, fmt.Sprintf("%s.v4.db", s.config.InstallationID))
if password != "" {
if err := msgdb.MigrateDBFile(v0Path, v1Path, "ON", password); err != nil {
if err := migrateDBFile(v0Path, v1Path, "ON", password); err != nil {
return err
}
if err := msgdb.MigrateDBFile(v1Path, v2Path, password, encKey); err != nil {
if err := migrateDBFile(v1Path, v2Path, password, encKey); err != nil {
// Remove db file as created with a blank password and never used,
// and there's no need to rekey in this case
os.Remove(v1Path)
@ -145,13 +139,13 @@ func (s *Service) initProtocol(address, encKey, password string) error {
}
}
if err := msgdb.MigrateDBKeyKdfIterations(v2Path, v3Path, encKey); err != nil {
if err := migrateDBKeyKdfIterations(v2Path, v3Path, encKey); err != nil {
os.Remove(v2Path)
os.Remove(v3Path)
}
// Fix IOS not encrypting database
if err := msgdb.EncryptDatabase(v3Path, v4Path, encKey); err != nil {
if err := encryptDatabase(v3Path, v4Path, encKey); err != nil {
os.Remove(v3Path)
os.Remove(v4Path)
}
@ -168,74 +162,103 @@ func (s *Service) initProtocol(address, encKey, password string) error {
return err
}
persistence, err := chat.NewSQLLitePersistence(v4Path, encKey)
// Because status-protocol-go split a single database file into multiple ones,
// in order to keep the backward compatibility, we just copy existing database
// into multiple locations. The tables and schemas did not change so it will work.
sessionsDatabasePath := filepath.Join(dataDir, fmt.Sprintf("%s.sessions.v4.sql", s.config.InstallationID))
transportDatabasePath := filepath.Join(dataDir, fmt.Sprintf("%s.transport.v4.sql", s.config.InstallationID))
if _, err := os.Stat(v4Path); err == nil {
if err := copyFile(v4Path, sessionsDatabasePath); err != nil {
return fmt.Errorf("failed to copy a file from %s to %s: %v", v4Path, sessionsDatabasePath, err)
}
// TODO: investigate why copying v4Path to transportDatabasePath does not work and WhisperTransportService
// returns an error.
os.Remove(v4Path)
}
selectedKeyID := s.w.SelectedKeyPairID()
identity, err := s.w.GetPrivateKey(selectedKeyID)
if err != nil {
return err
}
multideviceConfig := &multidevice.Config{
InstallationID: s.config.InstallationID,
ProtocolVersion: chat.ProtocolVersion,
MaxInstallations: maxInstallations,
// Create a custom zap.Logger which will forward logs from status-protocol-go to status-go logger.
zapLogger, err := logutils.NewZapLoggerWithAdapter(logutils.Logger())
if err != nil {
return err
}
addedBundlesHandler := func(addedBundles []*multidevice.Installation) {
handler := PublisherSignalHandler{}
for _, bundle := range addedBundles {
handler.BundleAdded(bundle.Identity, bundle.ID)
}
messenger, err := protocol.NewMessenger(
identity,
&server{server: s.server},
s.w,
dataDir,
encKey,
s.config.InstallationID,
protocol.WithDatabaseFilePaths(
sessionsDatabasePath,
transportDatabasePath,
),
protocol.WithGenericDiscoveryTopicSupport(),
protocol.WithCustomLogger(zapLogger),
)
if err != nil {
return err
}
s.messenger = messenger
// Start a loop that retrieves all messages and propagates them to status-react.
s.cancelMessenger = make(chan struct{})
go s.retrieveMessagesLoop(time.Second, s.cancelMessenger)
protocolService := chat.NewProtocolService(
chat.NewEncryptionService(
persistence,
chat.DefaultEncryptionServiceConfig(s.config.InstallationID)),
sharedsecret.NewService(persistence.GetSharedSecretStorage()),
multidevice.New(multideviceConfig, persistence.GetMultideviceStorage()),
addedBundlesHandler,
s.ProcessNegotiatedSecret)
return nil
}
onNewMessagesHandler := func(messages []*filter.Messages) {
var signalMessages []*signal.Messages
for _, chatMessages := range messages {
signalMessage := &signal.Messages{
Error: chatMessages.Error,
Chat: chatMessages.Chat,
}
signalMessages = append(signalMessages, signalMessage)
dedupMessages, err := s.processReceivedMessages(chatMessages.Messages)
func (s *Service) retrieveMessagesLoop(tick time.Duration, cancel <-chan struct{}) {
ticker := time.NewTicker(tick)
defer ticker.Stop()
for {
select {
case <-ticker.C:
chatWithMessages, err := s.messenger.RetrieveRawAll()
if err != nil {
log.Error("could not process messages", "err", err)
log.Error("failed to retrieve raw messages", "err", err)
continue
}
signalMessage.Messages = dedupMessages
var signalMessages []*signal.Messages
for chat, messages := range chatWithMessages {
signalMessage := &signal.Messages{
Chat: chat,
Error: nil, // TODO: what is it needed for?
Messages: s.deduplicator.Deduplicate(messages),
}
signalMessages = append(signalMessages, signalMessage)
}
log.Debug("retrieve messages loop", "messages", len(signalMessages))
if len(signalMessages) == 0 {
continue
}
PublisherSignalHandler{}.NewMessages(signalMessages)
case <-cancel:
return
}
PublisherSignalHandler{}.NewMessages(signalMessages)
}
s.Publisher.Init(persistence.DB, protocolService, onNewMessagesHandler)
return s.Publisher.Start(s.online, true)
}
func (s *Service) processReceivedMessages(messages []*whisper.Message) ([]dedup.DeduplicateMessage, error) {
dedupMessages := s.deduplicator.Deduplicate(messages)
func (s *Service) ConfirmMessagesProcessed(messageIDs [][]byte) error {
return s.messenger.ConfirmMessagesProcessed(messageIDs)
}
// Attempt to decrypt message, otherwise leave unchanged
for _, dedupMessage := range dedupMessages {
err := s.ProcessMessage(dedupMessage.Message, dedupMessage.DedupID)
switch err {
case chat.ErrNotPairedDevice:
log.Info("Received a message from non-paired device", "err", err)
case chat.ErrDeviceNotFound:
log.Warn("Received a message not targeted to us", "err", err)
default:
if err != nil {
log.Error("Failed handling message with error", "err", err)
}
}
}
func (s *Service) EnableInstallation(installationID string) error {
return s.messenger.EnableInstallation(installationID)
}
return dedupMessages, nil
// DisableInstallation disables an installation for multi-device sync.
func (s *Service) DisableInstallation(installationID string) error {
return s.messenger.DisableInstallation(installationID)
}
// UpdateMailservers updates information about selected mail servers.
@ -297,10 +320,6 @@ func (s *Service) Start(server *p2p.Server) error {
return nil
}
func (s *Service) online() bool {
return s.server.PeerCount() != 0
}
// Stop is run when a service is stopped.
func (s *Service) Stop() error {
log.Info("Stopping shhext service")
@ -313,13 +332,24 @@ func (s *Service) Stop() error {
s.requestsRegistry.Clear()
s.envelopesMonitor.Stop()
s.mailMonitor.Stop()
if s.filter != nil {
if err := s.filter.Stop(); err != nil {
log.Error("Failed to stop filter service with error", "err", err)
if s.cancelMessenger != nil {
select {
case <-s.cancelMessenger:
// channel already closed
default:
close(s.cancelMessenger)
s.cancelMessenger = nil
}
}
return s.Publisher.Stop()
if s.messenger != nil {
if err := s.messenger.Shutdown(); err != nil {
return err
}
}
return nil
}
func (s *Service) syncMessages(ctx context.Context, mailServerID []byte, r whisper.SyncMailRequest) (resp whisper.SyncEventResponse, err error) {
@ -364,3 +394,9 @@ func (s *Service) syncMessages(ctx context.Context, mailServerID []byte, r whisp
}
}
}
func (s *Service) afterPost(hash []byte, newMessage whisper.NewMessage) hexutil.Bytes {
s.envelopesMonitor.Add(common.BytesToHash(hash), newMessage)
mID := messageID(newMessage)
return mID[:]
}

View File

@ -3,8 +3,10 @@ package shhext
import (
"context"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"github.com/status-im/status-go/signal"
"io/ioutil"
"math"
"net"
@ -115,6 +117,12 @@ func (s *ShhExtSuite) SetupTest() {
stack, err := node.New(cfg)
s.NoError(err)
s.whisper[i] = whisper.New(nil)
privateKey, err := crypto.GenerateKey()
s.NoError(err)
err = s.whisper[i].SelectKeyPair(privateKey)
s.NoError(err)
s.NoError(stack.Register(func(n *node.ServiceContext) (node.Service, error) {
return s.whisper[i], nil
}))
@ -152,7 +160,14 @@ func (s *ShhExtSuite) TestInitProtocol() {
}
db, err := leveldb.Open(storage.NewMemStorage(), nil)
s.Require().NoError(err)
service := New(whisper.New(nil), nil, db, config)
shh := whisper.New(nil)
privateKey, err := crypto.GenerateKey()
s.Require().NoError(err)
err = shh.SelectKeyPair(privateKey)
s.Require().NoError(err)
service := New(shh, nil, db, config)
err = service.InitProtocolWithPassword("example-address", "`090///\nhtaa\rhta9x8923)$$'23")
s.NoError(err)
@ -344,6 +359,10 @@ func (s *ShhExtSuite) TestRequestMessagesSuccess() {
var err error
shh := whisper.New(nil)
privateKey, err := crypto.GenerateKey()
s.Require().NoError(err)
err = shh.SelectKeyPair(privateKey)
s.Require().NoError(err)
aNode, err := node.New(&node.Config{
P2P: p2p.Config{
MaxPeers: math.MaxInt32,
@ -418,6 +437,94 @@ func (s *ShhExtSuite) TestRequestMessagesSuccess() {
s.Require().NoError(waitForHashInMonitor(api.service.mailMonitor, common.BytesToHash(hash), MailServerRequestSent, time.Second))
}
// TestRetrieveMessageLoopNoMessages verifies that there are no signals sent
// if there are no messages.
func (s *ShhExtSuite) TestRetrieveMessageLoopNoMessages() {
shhConfig := whisper.DefaultConfig
shhConfig.MinimumAcceptedPOW = 0 // accept all messages
shh := whisper.New(&shhConfig)
privateKey, err := crypto.GenerateKey()
s.Require().NoError(err)
err = shh.SelectKeyPair(privateKey)
s.Require().NoError(err)
aNode, err := node.New(&node.Config{
P2P: p2p.Config{
MaxPeers: math.MaxInt32,
NoDiscovery: true,
},
NoUSB: true,
}) // in-memory node as no data dir
s.Require().NoError(err)
err = aNode.Register(func(*node.ServiceContext) (node.Service, error) { return shh, nil })
s.Require().NoError(err)
err = aNode.Start()
s.Require().NoError(err)
defer func() { err := aNode.Stop(); s.NoError(err) }()
mock := newHandlerMock(1)
config := params.ShhextConfig{
InstallationID: "1",
BackupDisabledDataDir: os.TempDir(),
PFSEnabled: true,
}
db, err := leveldb.Open(storage.NewMemStorage(), nil)
s.Require().NoError(err)
service := New(shh, mock, db, config)
s.Require().NoError(service.InitProtocolWithPassword("abc", "password"))
testCases := []struct {
name string
signalName string
action func()
expectedValue int
}{
{
name: "send one public message",
signalName: signal.EventNewMessages,
action: func() {
api := NewPublicAPI(service)
_, err = api.SendPublicMessage(context.Background(), SendPublicMessageRPC{
Chat: "test",
Payload: []byte("abc"),
})
s.Require().NoError(err)
},
expectedValue: 1,
},
{
name: "no messages",
action: func() {},
expectedValue: 0,
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
// Verify a proper signal is sent when a message is received.
var counter int64
signal.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
var envelope signal.Envelope
err := json.Unmarshal([]byte(jsonEvent), &envelope)
s.NoError(err)
switch envelope.Type {
case signal.EventNewMessages:
atomic.AddInt64(&counter, 1)
}
})
tc.action()
cancel := make(chan struct{})
go service.retrieveMessagesLoop(time.Millisecond*10, cancel)
time.Sleep(time.Millisecond * 100)
close(cancel)
s.Require().EqualValues(tc.expectedValue, counter)
})
}
}
func (s *ShhExtSuite) TearDown() {
for _, n := range s.nodes {
s.NoError(n.Stop())

View File

@ -4,9 +4,10 @@ import (
"encoding/hex"
"github.com/ethereum/go-ethereum/common"
"github.com/status-im/status-go/messaging/filter"
"github.com/status-im/status-go/services/shhext/dedup"
whisper "github.com/status-im/whisper/whisperv6"
"github.com/status-im/status-protocol-go/transport/whisper/filter"
)
const (
@ -134,7 +135,7 @@ type EnodeDiscoveredSignal struct {
type Messages struct {
Error error `json:"error"`
Messages []dedup.DeduplicateMessage `json:"messages"`
Chat *filter.Chat `json:"chat"`
Chat filter.Chat `json:"chat"`
}
// SendEnodeDiscovered tiggered when an enode is discovered.

View File

@ -1,4 +0,0 @@
DROP TABLE sessions;
DROP TABLE bundles;
DROP TABLE keys;
DROP TABLE ratchet_info;

View File

@ -1,40 +0,0 @@
CREATE TABLE sessions (
dhr BLOB,
dhs_public BLOB,
dhs_private BLOB,
root_chain_key BLOB,
send_chain_key BLOB,
send_chain_n INTEGER,
recv_chain_key BLOB,
recv_chain_n INTEGER,
step INTEGER,
pn INTEGER,
id BLOB NOT NULL PRIMARY KEY,
UNIQUE(id) ON CONFLICT REPLACE
);
CREATE TABLE keys (
public_key BLOB NOT NULL,
msg_num INTEGER,
message_key BLOB NOT NULL,
UNIQUE (msg_num, message_key) ON CONFLICT REPLACE
);
CREATE TABLE bundles (
identity BLOB NOT NULL,
installation_id TEXT NOT NULL,
private_key BLOB,
signed_pre_key BLOB NOT NULL PRIMARY KEY ON CONFLICT IGNORE,
timestamp UNSIGNED BIG INT NOT NULL,
expired BOOLEAN DEFAULT 0
);
CREATE TABLE ratchet_info (
bundle_id BLOB NOT NULL,
ephemeral_key BLOB,
identity BLOB NOT NULL,
symmetric_key BLOB NOT NULL,
installation_id TEXT NOT NULL,
UNIQUE(bundle_id, identity) ON CONFLICT REPLACE,
FOREIGN KEY (bundle_id) REFERENCES bundles(signed_pre_key)
);

View File

@ -1,11 +0,0 @@
DROP TABLE ratchet_info_v2;
CREATE TABLE ratchet_info (
bundle_id BLOB NOT NULL,
ephemeral_key BLOB,
identity BLOB NOT NULL,
symmetric_key BLOB NOT NULL,
installation_id TEXT NOT NULL,
UNIQUE(bundle_id, identity) ON CONFLICT REPLACE,
FOREIGN KEY (bundle_id) REFERENCES bundles(signed_pre_key)
);

View File

@ -1,13 +0,0 @@
DELETE FROM sessions;
DELETE FROM keys;
DROP TABLE ratchet_info;
CREATE TABLE ratchet_info_v2 (
bundle_id BLOB NOT NULL,
ephemeral_key BLOB,
identity BLOB NOT NULL,
symmetric_key BLOB NOT NULL,
installation_id TEXT NOT NULL,
UNIQUE(bundle_id, identity, installation_id) ON CONFLICT REPLACE,
FOREIGN KEY (bundle_id) REFERENCES bundles(signed_pre_key)
);

View File

@ -1,3 +0,0 @@
ALTER TABLE keys DROP COLUMN session_id;
ALTER TABLE sessions DROP COLUMN keys_count;
ALTER TABLE bundles DROP COLUMN version;

View File

@ -1,5 +0,0 @@
DELETE FROM keys;
ALTER TABLE keys ADD COLUMN seq_num INTEGER NOT NULL DEFAULT 0;
ALTER TABLE keys ADD COLUMN session_id BLOB;
ALTER TABLE sessions ADD COLUMN keys_count INTEGER NOT NULL DEFAULT 0;
ALTER TABLE bundles ADD COLUMN version INTEGER NOT NULL DEFAULT 0;

View File

@ -1 +0,0 @@
DROP TABLE installations;

View File

@ -1,7 +0,0 @@
CREATE TABLE installations (
identity BLOB NOT NULL,
installation_id TEXT NOT NULL,
timestamp UNSIGNED BIG INT NOT NULL,
enabled BOOLEAN DEFAULT 1,
UNIQUE(identity, installation_id) ON CONFLICT REPLACE
);

View File

@ -1,2 +0,0 @@
DROP TABLE secret_installation_ids;
DROP TABLE secrets;

View File

@ -1,11 +0,0 @@
CREATE TABLE secrets (
identity BLOB NOT NULL PRIMARY KEY ON CONFLICT IGNORE,
secret BLOB NOT NULL
);
CREATE TABLE secret_installation_ids (
id TEXT NOT NULL,
identity_id BLOB NOT NULL,
UNIQUE(id, identity_id) ON CONFLICT IGNORE,
FOREIGN KEY (identity_id) REFERENCES secrets(identity)
);

View File

@ -1 +0,0 @@
ALTER TABLE installations ADD version INTEGER DEFAULT 0;

View File

@ -1 +0,0 @@
DROP TABLE contact_code_config;

View File

@ -1,6 +0,0 @@
CREATE TABLE contact_code_config (
unique_constraint varchar(1) NOT NULL PRIMARY KEY DEFAULT 'X',
last_published INTEGER NOT NULL DEFAULT 0
);
INSERT INTO contact_code_config VALUES ('X', 0);

View File

@ -1 +0,0 @@
DROP TABLE whisper_keys;

View File

@ -1,4 +0,0 @@
CREATE TABLE whisper_keys (
chat_id TEXT PRIMARY KEY ON CONFLICT IGNORE,
key BLOB NOT NULL
) WITHOUT ROWID;

View File

@ -1 +0,0 @@
DROP TABLE installations_metadata;

View File

@ -1,8 +0,0 @@
CREATE TABLE installation_metadata (
identity BLOB NOT NULL,
installation_id TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '',
device_type TEXT NOT NULL DEFAULT '',
fcm_token TEXT NOT NULL DEFAULT '',
UNIQUE(identity, installation_id) ON CONFLICT REPLACE
);

View File

@ -1,4 +0,0 @@
// Package static embeds static (JS, HTML) resources right into the binaries
package static
//go:generate go-bindata -pkg migrations -o ../../messaging/db/migrations/bindata.go .

View File

@ -226,8 +226,14 @@ func (set *threadSafeSet) String() string {
func (set *threadSafeSet) PowerSet() Set {
set.RLock()
ret := set.s.PowerSet()
unsafePowerSet := set.s.PowerSet().(*threadUnsafeSet)
set.RUnlock()
ret := &threadSafeSet{s: newThreadUnsafeSet()}
for subset := range unsafePowerSet.Iter() {
unsafeSubset := subset.(*threadUnsafeSet)
ret.Add(&threadSafeSet{s: *unsafeSubset})
}
return ret
}

View File

@ -9,7 +9,7 @@ COPY . ./
ENV GO111MODULE=on
ENV DATABASES="postgres mysql redshift cassandra spanner cockroachdb clickhouse mongodb sqlserver"
ENV SOURCES="file go_bindata github aws_s3 google_cloud_storage godoc_vfs gitlab"
ENV SOURCES="file go_bindata github github_ee aws_s3 google_cloud_storage godoc_vfs gitlab"
RUN go build -a -o build/migrate.linux-386 -ldflags="-X main.Version=${VERSION}" -tags "$DATABASES $SOURCES" ./cmd/migrate

View File

@ -44,9 +44,14 @@ It is suggested that the version number of corresponding `up` and `down` migrati
files be equivalent for clarity, but they are allowed to differ so long as the
relative ordering of the migrations is preserved.
The migration files are permitted to be empty, so in the event that a migration
is a no-op or is irreversible, it is recommended to still include both migration
files, and either leaving them empty or adding a comment as appropriate.
The migration files are permitted to be "empty", in the event that a migration
is a no-op or is irreversible. It is recommended to still include both migration
files by making the whole migration file consist of a comment.
If your database does not support comments, then deleting the migration file will also work.
Note, an actual empty file (e.g. a 0 byte file) may cause issues with your database since migrate
will attempt to run an empty query. In this case, deleting the migration file will also work.
For the rational of this behavior see:
[#244 (comment)](https://github.com/golang-migrate/migrate/issues/244#issuecomment-510758270)
## Migration Content Format

View File

@ -1,4 +1,4 @@
SOURCE ?= file go_bindata github aws_s3 google_cloud_storage godoc_vfs gitlab
SOURCE ?= file go_bindata github github_ee aws_s3 google_cloud_storage godoc_vfs gitlab
DATABASE ?= postgres mysql redshift cassandra spanner cockroachdb clickhouse mongodb sqlserver
VERSION ?= $(shell git describe --tags 2>/dev/null | cut -c 2-)
TEST_FLAGS ?=
@ -27,27 +27,14 @@ test-short:
test:
@-rm -r $(COVERAGE_DIR)
@mkdir $(COVERAGE_DIR)
make test-with-flags TEST_FLAGS='-v -race -covermode atomic -coverprofile $$(COVERAGE_DIR)/_$$(RAND).txt -bench=. -benchmem -timeout 20m'
@echo 'mode: atomic' > $(COVERAGE_DIR)/combined.txt
@cat $(COVERAGE_DIR)/_*.txt | grep -v 'mode: atomic' >> $(COVERAGE_DIR)/combined.txt
make test-with-flags TEST_FLAGS='-v -race -covermode atomic -coverprofile $$(COVERAGE_DIR)/combined.txt -bench=. -benchmem -timeout 20m'
test-with-flags:
@echo SOURCE: $(SOURCE)
@echo SOURCE: $(SOURCE)
@echo DATABASE: $(DATABASE)
@go test $(TEST_FLAGS) .
@go test $(TEST_FLAGS) ./cli/...
@go test $(TEST_FLAGS) ./database
@go test $(TEST_FLAGS) ./testing/...
@echo -n '$(SOURCE)' | tr -s ' ' '\n' | xargs -I{} go test $(TEST_FLAGS) ./source/{}
@go test $(TEST_FLAGS) ./source/testing/...
@go test $(TEST_FLAGS) ./source/stub/...
@echo -n '$(DATABASE)' | tr -s ' ' '\n' | xargs -I{} go test $(TEST_FLAGS) ./database/{}
@go test $(TEST_FLAGS) ./database/testing/...
@go test $(TEST_FLAGS) ./database/stub/...
@go test $(TEST_FLAGS) ./...
kill-orphaned-docker-containers:
@ -84,7 +71,7 @@ rewrite-import-paths:
docs:
-make kill-docs
nohup godoc -play -http=127.0.0.1:6064 </dev/null >/dev/null 2>&1 & echo $$! > .godoc.pid
cat .godoc.pid
cat .godoc.pid
kill-docs:

View File

@ -6,16 +6,15 @@
![Supported Go Versions](https://img.shields.io/badge/Go-1.11%2C%201.12-lightgrey.svg)
[![GitHub Release](https://img.shields.io/github/release/golang-migrate/migrate.svg)](https://github.com/golang-migrate/migrate/releases)
# migrate
__Database migrations written in Go. Use as [CLI](#cli-usage) or import as [library](#use-in-your-go-project).__
* Migrate reads migrations from [sources](#migration-sources)
* Migrate reads migrations from [sources](#migration-sources)
and applies them in correct order to a [database](#databases).
* Drivers are "dumb", migrate glues everything together and makes sure the logic is bulletproof.
* Drivers are "dumb", migrate glues everything together and makes sure the logic is bulletproof.
(Keeps the drivers lightweight, too.)
* Database drivers don't assume things or try to correct user input. When in doubt, fail.
* Database drivers don't assume things or try to correct user input. When in doubt, fail.
Forked from [mattes/migrate](https://github.com/mattes/migrate)
@ -23,21 +22,21 @@ Forked from [mattes/migrate](https://github.com/mattes/migrate)
Database drivers run migrations. [Add a new database?](database/driver.go)
* [PostgreSQL](database/postgres)
* [Redshift](database/redshift)
* [Ql](database/ql)
* [Cassandra](database/cassandra)
* [SQLite](database/sqlite3) ([todo #165](https://github.com/mattes/migrate/issues/165))
* [MySQL/ MariaDB](database/mysql)
* [Neo4j](database/neo4j) ([todo #167](https://github.com/mattes/migrate/issues/167))
* [MongoDB](database/mongodb)
* [CrateDB](database/crate) ([todo #170](https://github.com/mattes/migrate/issues/170))
* [Shell](database/shell) ([todo #171](https://github.com/mattes/migrate/issues/171))
* [Google Cloud Spanner](database/spanner)
* [CockroachDB](database/cockroachdb)
* [ClickHouse](database/clickhouse)
* [Firebird](database/firebird) ([todo #49](https://github.com/golang-migrate/migrate/issues/49))
* [MS SQL Server](database/sqlserver)
* [PostgreSQL](database/postgres)
* [Redshift](database/redshift)
* [Ql](database/ql)
* [Cassandra](database/cassandra)
* [SQLite](database/sqlite3) ([todo #165](https://github.com/mattes/migrate/issues/165))
* [MySQL/ MariaDB](database/mysql)
* [Neo4j](database/neo4j) ([todo #167](https://github.com/mattes/migrate/issues/167))
* [MongoDB](database/mongodb)
* [CrateDB](database/crate) ([todo #170](https://github.com/mattes/migrate/issues/170))
* [Shell](database/shell) ([todo #171](https://github.com/mattes/migrate/issues/171))
* [Google Cloud Spanner](database/spanner)
* [CockroachDB](database/cockroachdb)
* [ClickHouse](database/clickhouse)
* [Firebird](database/firebird) ([todo #49](https://github.com/golang-migrate/migrate/issues/49))
* [MS SQL Server](database/sqlserver)
### Database URLs
@ -49,6 +48,7 @@ Explicitly, the following characters need to be escaped:
`!`, `#`, `$`, `%`, `&`, `'`, `(`, `)`, `*`, `+`, `,`, `/`, `:`, `;`, `=`, `?`, `@`, `[`, `]`
It's easiest to always run the URL parts of your DB connection URL (e.g. username, password, etc) through an URL encoder. See the example Python snippets below:
```bash
$ python3 -c 'import urllib.parse; print(urllib.parse.quote(input("String to encode: "), ""))'
String to encode: FAKEpassword!#$%&'()*+,/:;=?@[]
@ -63,44 +63,43 @@ $
Source drivers read migrations from local or remote sources. [Add a new source?](source/driver.go)
* [Filesystem](source/file) - read from fileystem
* [Go-Bindata](source/go_bindata) - read from embedded binary data ([jteeuwen/go-bindata](https://github.com/jteeuwen/go-bindata))
* [Github](source/github) - read from remote Github repositories
* [Gitlab](source/gitlab) - read from remote Gitlab repositories
* [AWS S3](source/aws_s3) - read from Amazon Web Services S3
* [Google Cloud Storage](source/google_cloud_storage) - read from Google Cloud Platform Storage
* [Filesystem](source/file) - read from filesystem
* [Go-Bindata](source/go_bindata) - read from embedded binary data ([jteeuwen/go-bindata](https://github.com/jteeuwen/go-bindata))
* [Github](source/github) - read from remote Github repositories
* [Github Enterprise](source/github_ee) - read from remote Github Enterprise repositories
* [Gitlab](source/gitlab) - read from remote Gitlab repositories
* [AWS S3](source/aws_s3) - read from Amazon Web Services S3
* [Google Cloud Storage](source/google_cloud_storage) - read from Google Cloud Platform Storage
## CLI usage
* Simple wrapper around this library.
* Handles ctrl+c (SIGINT) gracefully.
* No config search paths, no config files, no magic ENV var injections.
* Simple wrapper around this library.
* Handles ctrl+c (SIGINT) gracefully.
* No config search paths, no config files, no magic ENV var injections.
__[CLI Documentation](cli)__
__[CLI Documentation](cmd/migrate)__
### Basic usage:
### Basic usage
```
```bash
$ migrate -source file://path/to/migrations -database postgres://localhost:5432/database up 2
```
### Docker usage
```
```bash
$ docker run -v {{ migration dir }}:/migrations --network host migrate/migrate
-path=/migrations/ -database postgres://localhost:5432/database up 2
```
## Use in your Go project
* API is stable and frozen for this release (v3 & v4).
* Uses [Go modules](https://golang.org/cmd/go/#hdr-Modules__module_versions__and_more) to manage dependencies.
* To help prevent database corruptions, it supports graceful stops via `GracefulStop chan bool`.
* Bring your own logger.
* Uses `io.Reader` streams internally for low memory overhead.
* Thread-safe and no goroutine leaks.
* API is stable and frozen for this release (v3 & v4).
* Uses [Go modules](https://golang.org/cmd/go/#hdr-Modules__module_versions__and_more) to manage dependencies.
* To help prevent database corruptions, it supports graceful stops via `GracefulStop chan bool`.
* Bring your own logger.
* Uses `io.Reader` streams internally for low memory overhead.
* Thread-safe and no goroutine leaks.
__[Go Documentation](https://godoc.org/github.com/golang-migrate/migrate)__
@ -144,7 +143,7 @@ func main() {
Each migration has an up and down migration. [Why?](FAQ.md#why-two-separate-files-up-and-down-for-a-migration)
```
```bash
1481574547_create_users_table.up.sql
1481574547_create_users_table.down.sql
```
@ -166,8 +165,6 @@ read the [development guide](CONTRIBUTING.md).
Also have a look at the [FAQ](FAQ.md).
---
Looking for alternatives? [https://awesome-go.com/#database](https://awesome-go.com/#database).

View File

@ -950,7 +950,7 @@ func (m *Migrate) unlock() error {
// if a prevErr is not nil.
func (m *Migrate) unlockErr(prevErr error) error {
if err := m.unlock(); err != nil {
return NewMultiError(prevErr, err)
return multierror.Append(prevErr, err)
}
return prevErr
}

View File

@ -8,11 +8,16 @@ import (
)
// MultiError holds multiple errors.
//
// Deprecated: Use github.com/hashicorp/go-multierror instead
type MultiError struct {
Errs []error
}
// NewMultiError returns an error type holding multiple errors.
//
// Deprecated: Use github.com/hashicorp/go-multierror instead
//
func NewMultiError(errs ...error) MultiError {
compactErrs := make([]error, 0)
for _, e := range errs {

9
vendor/github.com/leodido/go-urn/.gitignore generated vendored Normal file
View File

@ -0,0 +1,9 @@
*.exe
*.dll
*.so
*.dylib
*.test
*.out
*.txt

15
vendor/github.com/leodido/go-urn/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,15 @@
language: go
go:
- 1.9.x
- 1.10.x
- tip
before_install:
- go get -t -v ./...
script:
- go test -race -coverprofile=coverage.txt -covermode=atomic
after_success:
- bash <(curl -s https://codecov.io/bash)

55
vendor/github.com/leodido/go-urn/README.md generated vendored Normal file
View File

@ -0,0 +1,55 @@
[![Build](https://img.shields.io/travis/leodido/go-urn/master.svg?style=for-the-badge)](https://travis-ci.org/leodido/go-urn) [![Coverage](https://img.shields.io/codecov/c/github/leodido/go-urn.svg?style=for-the-badge)](https://codecov.io/gh/leodido/go-urn) [![Documentation](https://img.shields.io/badge/godoc-reference-blue.svg?style=for-the-badge)](https://godoc.org/github.com/leodido/go-urn)
**A parser for URNs**.
> As seen on [RFC 2141](https://tools.ietf.org/html/rfc2141#ref-1).
[API documentation](https://godoc.org/github.com/leodido/go-urn).
## Installation
```
go get github.com/leodido/go-urn
```
## Performances
This implementation results to be really fast.
Usually below ½ microsecond on my machine<sup>[1](#mymachine)</sup>.
Notice it also performs, while parsing:
1. fine-grained and informative erroring
2. specific-string normalization
```
ok/00/urn:a:b______________________________________/-4 20000000 265 ns/op 182 B/op 6 allocs/op
ok/01/URN:foo:a123,456_____________________________/-4 30000000 296 ns/op 200 B/op 6 allocs/op
ok/02/urn:foo:a123%2c456___________________________/-4 20000000 331 ns/op 208 B/op 6 allocs/op
ok/03/urn:ietf:params:scim:schemas:core:2.0:User___/-4 20000000 430 ns/op 280 B/op 6 allocs/op
ok/04/urn:ietf:params:scim:schemas:extension:enterp/-4 20000000 411 ns/op 312 B/op 6 allocs/op
ok/05/urn:ietf:params:scim:schemas:extension:enterp/-4 20000000 472 ns/op 344 B/op 6 allocs/op
ok/06/urn:burnout:nss______________________________/-4 30000000 257 ns/op 192 B/op 6 allocs/op
ok/07/urn:abcdefghilmnopqrstuvzabcdefghilm:x_______/-4 20000000 375 ns/op 213 B/op 6 allocs/op
ok/08/urn:urnurnurn:urn____________________________/-4 30000000 265 ns/op 197 B/op 6 allocs/op
ok/09/urn:ciao:@!=%2c(xyz)+a,b.*@g=$_'_____________/-4 20000000 307 ns/op 248 B/op 6 allocs/op
ok/10/URN:x:abc%1dz%2f%3az_________________________/-4 30000000 259 ns/op 212 B/op 6 allocs/op
no/11/URN:-xxx:x___________________________________/-4 20000000 445 ns/op 320 B/op 6 allocs/op
no/12/urn::colon:nss_______________________________/-4 20000000 461 ns/op 320 B/op 6 allocs/op
no/13/urn:abcdefghilmnopqrstuvzabcdefghilmn:specifi/-4 10000000 660 ns/op 320 B/op 6 allocs/op
no/14/URN:a!?:x____________________________________/-4 20000000 507 ns/op 320 B/op 6 allocs/op
no/15/urn:urn:NSS__________________________________/-4 20000000 429 ns/op 288 B/op 6 allocs/op
no/16/urn:white_space:NSS__________________________/-4 20000000 482 ns/op 320 B/op 6 allocs/op
no/17/urn:concat:no_spaces_________________________/-4 20000000 539 ns/op 328 B/op 7 allocs/op
no/18/urn:a:/______________________________________/-4 20000000 470 ns/op 320 B/op 7 allocs/op
no/19/urn:UrN:NSS__________________________________/-4 20000000 399 ns/op 288 B/op 6 allocs/op
```
---
* <a name="mymachine">[1]</a>: Intel Core i7-7600U CPU @ 2.80GHz
---
[![Analytics](https://ga-beacon.appspot.com/UA-49657176-1/go-urn?flat)](https://github.com/igrigorik/ga-beacon)

1670
vendor/github.com/leodido/go-urn/machine.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

159
vendor/github.com/leodido/go-urn/machine.go.rl generated vendored Normal file
View File

@ -0,0 +1,159 @@
package urn
import (
"fmt"
)
var (
errPrefix = "expecting the prefix to be the \"urn\" string (whatever case) [col %d]"
errIdentifier = "expecting the identifier to be string (1..31 alnum chars, also containing dashes but not at its start) [col %d]"
errSpecificString = "expecting the specific string to be a string containing alnum, hex, or others ([()+,-.:=@;$_!*']) chars [col %d]"
errNoUrnWithinID = "expecting the identifier to not contain the \"urn\" reserved string [col %d]"
errHex = "expecting the specific string hex chars to be well-formed (%%alnum{2}) [col %d]"
errParse = "parsing error [col %d]"
)
%%{
machine urn;
# unsigned alphabet
alphtype uint8;
action mark {
m.pb = m.p
}
action tolower {
m.tolower = append(m.tolower, m.p - m.pb)
}
action set_pre {
output.prefix = string(m.text())
}
action set_nid {
output.ID = string(m.text())
}
action set_nss {
raw := m.text()
output.SS = string(raw)
// Iterate upper letters lowering them
for _, i := range m.tolower {
raw[i] = raw[i] + 32
}
output.norm = string(raw)
}
action err_pre {
m.err = fmt.Errorf(errPrefix, m.p)
fhold;
fgoto fail;
}
action err_nid {
m.err = fmt.Errorf(errIdentifier, m.p)
fhold;
fgoto fail;
}
action err_nss {
m.err = fmt.Errorf(errSpecificString, m.p)
fhold;
fgoto fail;
}
action err_urn {
m.err = fmt.Errorf(errNoUrnWithinID, m.p)
fhold;
fgoto fail;
}
action err_hex {
m.err = fmt.Errorf(errHex, m.p)
fhold;
fgoto fail;
}
action err_parse {
m.err = fmt.Errorf(errParse, m.p)
fhold;
fgoto fail;
}
pre = ([uU][rR][nN] @err(err_pre)) >mark %set_pre;
nid = (alnum >mark (alnum | '-'){0,31}) %set_nid;
hex = '%' (digit | lower | upper >tolower){2} $err(err_hex);
sss = (alnum | [()+,\-.:=@;$_!*']);
nss = (sss | hex)+ $err(err_nss);
fail := (any - [\n\r])* @err{ fgoto main; };
main := (pre ':' (nid - pre %err(err_urn)) $err(err_nid) ':' nss >mark %set_nss) $err(err_parse);
}%%
%% write data noerror noprefix;
// Machine is the interface representing the FSM
type Machine interface {
Error() error
Parse(input []byte) (*URN, error)
}
type machine struct {
data []byte
cs int
p, pe, eof, pb int
err error
tolower []int
}
// NewMachine creates a new FSM able to parse RFC 2141 strings.
func NewMachine() Machine {
m := &machine{}
%% access m.;
%% variable p m.p;
%% variable pe m.pe;
%% variable eof m.eof;
%% variable data m.data;
return m
}
// Err returns the error that occurred on the last call to Parse.
//
// If the result is nil, then the line was parsed successfully.
func (m *machine) Error() error {
return m.err
}
func (m *machine) text() []byte {
return m.data[m.pb:m.p]
}
// Parse parses the input byte array as a RFC 2141 string.
func (m *machine) Parse(input []byte) (*URN, error) {
m.data = input
m.p = 0
m.pb = 0
m.pe = len(input)
m.eof = len(input)
m.err = nil
m.tolower = []int{}
output := &URN{}
%% write init;
%% write exec;
if m.cs < first_final || m.cs == en_fail {
return nil, m.err
}
return output, nil
}

17
vendor/github.com/leodido/go-urn/makefile generated vendored Normal file
View File

@ -0,0 +1,17 @@
SHELL := /bin/bash
machine.go: machine.go.rl
ragel -Z -G2 -e -o $@ $<
@gofmt -w -s $@
@sed -i '/^\/\/line/d' $@
.PHONY: build
build: machine.go
.PHONY: bench
bench: *_test.go machine.go
go test -bench=. -benchmem -benchtime=5s ./...
.PHONY: tests
tests: *_test.go machine.go
go test -race -timeout 10s -coverprofile=coverage.out -covermode=atomic -v ./...

63
vendor/github.com/leodido/go-urn/urn.go generated vendored Normal file
View File

@ -0,0 +1,63 @@
package urn
import (
"strings"
)
// URN represents an Uniform Resource Name.
//
// The general form represented is:
//
// urn:<id>:<ss>
//
// Details at https://tools.ietf.org/html/rfc2141.
type URN struct {
prefix string // Static prefix. Equal to "urn" when empty.
ID string // Namespace identifier
SS string // Namespace specific string
norm string // Normalized namespace specific string
}
// Normalize turns the receiving URN into its norm version.
//
// Which means: lowercase prefix, lowercase namespace identifier, and immutate namespace specific string chars (except <hex> tokens which are lowercased).
func (u *URN) Normalize() *URN {
return &URN{
prefix: "urn",
ID: strings.ToLower(u.ID),
SS: u.norm,
}
}
// Equal checks the lexical equivalence of the current URN with another one.
func (u *URN) Equal(x *URN) bool {
return *u.Normalize() == *x.Normalize()
}
// String reassembles the URN into a valid URN string.
//
// This requires both ID and SS fields to be non-empty.
// Otherwise it returns an empty string.
//
// Default URN prefix is "urn".
func (u *URN) String() string {
var res string
if u.ID != "" && u.SS != "" {
if u.prefix == "" {
res += "urn"
}
res += u.prefix + ":" + u.ID + ":" + u.SS
}
return res
}
// Parse is responsible to create an URN instance from a byte array matching the correct URN syntax.
func Parse(u []byte) (*URN, bool) {
urn, err := NewMachine().Parse(u)
if err != nil {
return nil, false
}
return urn, true
}

View File

@ -1,4 +1,8 @@
language: go
go:
- 1.3
- 1.4
- 1.9
- "1.10"
- tip
matrix:
allow_failures:
- go: tip

20
vendor/github.com/rs/cors/README.md generated vendored
View File

@ -49,6 +49,14 @@ The server now runs on `localhost:8080`:
{"hello": "world"}
### Allow * With Credentials Security Protection
This library has been modified to avoid a well known security issue when configured with `AllowedOrigins` to `*` and `AllowCredentials` to `true`. Such setup used to make the library reflects the request `Origin` header value, working around a security protection embedded into the standard that makes clients to refuse such configuration. This behavior has been removed with [#55](https://github.com/rs/cors/issues/55) and [#57](https://github.com/rs/cors/issues/57).
If you depend on this behavior and understand the implications, you can restore it using the `AllowOriginFunc` with `func(origin string) {return true}`.
Please refer to [#55](https://github.com/rs/cors/issues/55) for more information about the security implications.
### More Examples
* `net/http`: [examples/nethttp/server.go](https://github.com/rs/cors/blob/master/examples/nethttp/server.go)
@ -56,6 +64,11 @@ The server now runs on `localhost:8080`:
* [Martini](http://martini.codegangsta.io): [examples/martini/server.go](https://github.com/rs/cors/blob/master/examples/martini/server.go)
* [Negroni](https://github.com/codegangsta/negroni): [examples/negroni/server.go](https://github.com/rs/cors/blob/master/examples/negroni/server.go)
* [Alice](https://github.com/justinas/alice): [examples/alice/server.go](https://github.com/rs/cors/blob/master/examples/alice/server.go)
* [HttpRouter](https://github.com/julienschmidt/httprouter): [examples/httprouter/server.go](https://github.com/rs/cors/blob/master/examples/httprouter/server.go)
* [Gorilla](http://www.gorillatoolkit.org/pkg/mux): [examples/gorilla/server.go](https://github.com/rs/cors/blob/master/examples/gorilla/server.go)
* [Buffalo](https://gobuffalo.io): [examples/buffalo/server.go](https://github.com/rs/cors/blob/master/examples/buffalo/server.go)
* [Gin](https://gin-gonic.github.io/gin): [examples/gin/server.go](https://github.com/rs/cors/blob/master/examples/gin/server.go)
* [Chi](https://github.com/go-chi/chi): [examples/chi/server.go](https://github.com/rs/cors/blob/master/examples/chi/server.go)
## Parameters
@ -63,8 +76,10 @@ Parameters are passed to the middleware thru the `cors.New` method as follow:
```go
c := cors.New(cors.Options{
AllowedOrigins: []string{"http://foo.com"},
AllowedOrigins: []string{"http://foo.com", "http://foo.com:8080"},
AllowCredentials: true,
// Enable Debugging for testing, consider disabling in production
Debug: true,
})
// Insert the middleware
@ -72,7 +87,8 @@ handler = c.Handler(handler)
```
* **AllowedOrigins** `[]string`: A list of origins a cross-domain request can be executed from. If the special `*` value is present in the list, all origins will be allowed. An origin may contain a wildcard (`*`) to replace 0 or more characters (i.e.: `http://*.domain.com`). Usage of wildcards implies a small performance penality. Only one wildcard can be used per origin. The default value is `*`.
* **AllowOriginFunc** `func (origin string) bool`: A custom function to validate the origin. It take the origin as argument and returns true if allowed or false otherwise. If this option is set, the content of `AllowedOrigins` is ignored
* **AllowOriginFunc** `func (origin string) bool`: A custom function to validate the origin. It takes the origin as an argument and returns true if allowed, or false otherwise. If this option is set, the content of `AllowedOrigins` is ignored.
* **AllowOriginRequestFunc** `func (r *http.Request origin string) bool`: A custom function to validate the origin. It takes the HTTP Request object and the origin as argument and returns true if allowed or false otherwise. If this option is set, the content of `AllowedOrigins` and `AllowOriginFunc` is ignored
* **AllowedMethods** `[]string`: A list of methods the client is allowed to use with cross-domain requests. Default value is simple methods (`GET` and `POST`).
* **AllowedHeaders** `[]string`: A list of non simple headers the client is allowed to use with cross-domain requests.
* **ExposedHeaders** `[]string`: Indicates which headers are safe to expose to the API of a CORS API specification

123
vendor/github.com/rs/cors/cors.go generated vendored
View File

@ -26,9 +26,6 @@ import (
"os"
"strconv"
"strings"
"github.com/rs/xhandler"
"golang.org/x/net/context"
)
// Options is a configuration container to setup the CORS middleware.
@ -36,7 +33,7 @@ type Options struct {
// AllowedOrigins is a list of origins a cross-domain request can be executed from.
// If the special "*" value is present in the list, all origins will be allowed.
// An origin may contain a wildcard (*) to replace 0 or more characters
// (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penality.
// (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penalty.
// Only one wildcard can be used per origin.
// Default value is ["*"]
AllowedOrigins []string
@ -44,8 +41,12 @@ type Options struct {
// as argument and returns true if allowed or false otherwise. If this option is
// set, the content of AllowedOrigins is ignored.
AllowOriginFunc func(origin string) bool
// AllowOriginFunc is a custom function to validate the origin. It takes the HTTP Request object and the origin as
// argument and returns true if allowed or false otherwise. If this option is set, the content of `AllowedOrigins`
// and `AllowOriginFunc` is ignored.
AllowOriginRequestFunc func(r *http.Request, origin string) bool
// AllowedMethods is a list of methods the client is allowed to use with
// cross-domain requests. Default value is simple methods (GET and POST)
// cross-domain requests. Default value is simple methods (HEAD, GET and POST).
AllowedMethods []string
// AllowedHeaders is list of non simple headers the client is allowed to use with
// cross-domain requests.
@ -55,12 +56,12 @@ type Options struct {
// ExposedHeaders indicates which headers are safe to expose to the API of a CORS
// API specification
ExposedHeaders []string
// AllowCredentials indicates whether the request can include user credentials like
// cookies, HTTP authentication or client side SSL certificates.
AllowCredentials bool
// MaxAge indicates how long (in seconds) the results of a preflight request
// can be cached
MaxAge int
// AllowCredentials indicates whether the request can include user credentials like
// cookies, HTTP authentication or client side SSL certificates.
AllowCredentials bool
// OptionsPassthrough instructs preflight to let other potential next handlers to
// process the OPTIONS method. Turn this on if your application handles OPTIONS.
OptionsPassthrough bool
@ -72,35 +73,38 @@ type Options struct {
type Cors struct {
// Debug logger
Log *log.Logger
// Set to true when allowed origins contains a "*"
allowedOriginsAll bool
// Normalized list of plain allowed origins
allowedOrigins []string
// List of allowed origins containing wildcards
allowedWOrigins []wildcard
// Optional origin validator function
allowOriginFunc func(origin string) bool
// Set to true when allowed headers contains a "*"
allowedHeadersAll bool
// Optional origin validator (with request) function
allowOriginRequestFunc func(r *http.Request, origin string) bool
// Normalized list of allowed headers
allowedHeaders []string
// Normalized list of allowed methods
allowedMethods []string
// Normalized list of exposed headers
exposedHeaders []string
exposedHeaders []string
maxAge int
// Set to true when allowed origins contains a "*"
allowedOriginsAll bool
// Set to true when allowed headers contains a "*"
allowedHeadersAll bool
allowCredentials bool
maxAge int
optionPassthrough bool
}
// New creates a new Cors handler with the provided options.
func New(options Options) *Cors {
c := &Cors{
exposedHeaders: convert(options.ExposedHeaders, http.CanonicalHeaderKey),
allowOriginFunc: options.AllowOriginFunc,
allowCredentials: options.AllowCredentials,
maxAge: options.MaxAge,
optionPassthrough: options.OptionsPassthrough,
exposedHeaders: convert(options.ExposedHeaders, http.CanonicalHeaderKey),
allowOriginFunc: options.AllowOriginFunc,
allowOriginRequestFunc: options.AllowOriginRequestFunc,
allowCredentials: options.AllowCredentials,
maxAge: options.MaxAge,
optionPassthrough: options.OptionsPassthrough,
}
if options.Debug {
c.Log = log.New(os.Stdout, "[cors] ", log.LstdFlags)
@ -112,8 +116,10 @@ func New(options Options) *Cors {
// Allowed Origins
if len(options.AllowedOrigins) == 0 {
// Default is all origins
c.allowedOriginsAll = true
if options.AllowOriginFunc == nil && options.AllowOriginRequestFunc == nil {
// Default is all origins
c.allowedOriginsAll = true
}
} else {
c.allowedOrigins = []string{}
c.allowedWOrigins = []wildcard{}
@ -128,7 +134,7 @@ func New(options Options) *Cors {
break
} else if i := strings.IndexByte(origin, '*'); i >= 0 {
// Split the origin in two: start and end string without the *
w := wildcard{origin[0:i], origin[i+1 : len(origin)]}
w := wildcard{origin[0:i], origin[i+1:]}
c.allowedWOrigins = append(c.allowedWOrigins, w)
} else {
c.allowedOrigins = append(c.allowedOrigins, origin)
@ -139,7 +145,7 @@ func New(options Options) *Cors {
// Allowed Headers
if len(options.AllowedHeaders) == 0 {
// Use sensible defaults
c.allowedHeaders = []string{"Origin", "Accept", "Content-Type"}
c.allowedHeaders = []string{"Origin", "Accept", "Content-Type", "X-Requested-With"}
} else {
// Origin is always appended as some browsers will always request for this header at preflight
c.allowedHeaders = convert(append(options.AllowedHeaders, "Origin"), http.CanonicalHeaderKey)
@ -155,7 +161,7 @@ func New(options Options) *Cors {
// Allowed Methods
if len(options.AllowedMethods) == 0 {
// Default is spec's "simple" methods
c.allowedMethods = []string{"GET", "POST"}
c.allowedMethods = []string{"GET", "POST", "HEAD"}
} else {
c.allowedMethods = convert(options.AllowedMethods, strings.ToUpper)
}
@ -163,16 +169,27 @@ func New(options Options) *Cors {
return c
}
// Default creates a new Cors handler with default options
// Default creates a new Cors handler with default options.
func Default() *Cors {
return New(Options{})
}
// AllowAll create a new Cors handler with permissive configuration allowing all
// origins with all standard methods with any header and credentials.
func AllowAll() *Cors {
return New(Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"HEAD", "GET", "POST", "PUT", "PATCH", "DELETE"},
AllowedHeaders: []string{"*"},
AllowCredentials: false,
})
}
// Handler apply the CORS specification on the request, and add relevant CORS headers
// as necessary.
func (c *Cors) Handler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "OPTIONS" {
if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" {
c.logf("Handler: Preflight request")
c.handlePreflight(w, r)
// Preflight requests are standalone and should stop the chain as some other
@ -192,32 +209,9 @@ func (c *Cors) Handler(h http.Handler) http.Handler {
})
}
// HandlerC is net/context aware handler
func (c *Cors) HandlerC(h xhandler.HandlerC) xhandler.HandlerC {
return xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
if r.Method == "OPTIONS" {
c.logf("Handler: Preflight request")
c.handlePreflight(w, r)
// Preflight requests are standalone and should stop the chain as some other
// middleware may not handle OPTIONS requests correctly. One typical example
// is authentication middleware ; OPTIONS requests won't carry authentication
// headers (see #1)
if c.optionPassthrough {
h.ServeHTTPC(ctx, w, r)
} else {
w.WriteHeader(http.StatusOK)
}
} else {
c.logf("Handler: Actual request")
c.handleActualRequest(w, r)
h.ServeHTTPC(ctx, w, r)
}
})
}
// HandlerFunc provides Martini compatible handler
func (c *Cors) HandlerFunc(w http.ResponseWriter, r *http.Request) {
if r.Method == "OPTIONS" {
if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" {
c.logf("HandlerFunc: Preflight request")
c.handlePreflight(w, r)
} else {
@ -228,7 +222,7 @@ func (c *Cors) HandlerFunc(w http.ResponseWriter, r *http.Request) {
// Negroni compatible interface
func (c *Cors) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
if r.Method == "OPTIONS" {
if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" {
c.logf("ServeHTTP: Preflight request")
c.handlePreflight(w, r)
// Preflight requests are standalone and should stop the chain as some other
@ -252,7 +246,7 @@ func (c *Cors) handlePreflight(w http.ResponseWriter, r *http.Request) {
headers := w.Header()
origin := r.Header.Get("Origin")
if r.Method != "OPTIONS" {
if r.Method != http.MethodOptions {
c.logf(" Preflight aborted: %s!=OPTIONS", r.Method)
return
}
@ -267,7 +261,7 @@ func (c *Cors) handlePreflight(w http.ResponseWriter, r *http.Request) {
c.logf(" Preflight aborted: empty origin")
return
}
if !c.isOriginAllowed(origin) {
if !c.isOriginAllowed(r, origin) {
c.logf(" Preflight aborted: origin '%s' not allowed", origin)
return
}
@ -282,7 +276,11 @@ func (c *Cors) handlePreflight(w http.ResponseWriter, r *http.Request) {
c.logf(" Preflight aborted: headers '%v' not allowed", reqHeaders)
return
}
headers.Set("Access-Control-Allow-Origin", origin)
if c.allowedOriginsAll {
headers.Set("Access-Control-Allow-Origin", "*")
} else {
headers.Set("Access-Control-Allow-Origin", origin)
}
// Spec says: Since the list of methods can be unbounded, simply returning the method indicated
// by Access-Control-Request-Method (if supported) can be enough
headers.Set("Access-Control-Allow-Methods", strings.ToUpper(reqMethod))
@ -306,7 +304,7 @@ func (c *Cors) handleActualRequest(w http.ResponseWriter, r *http.Request) {
headers := w.Header()
origin := r.Header.Get("Origin")
if r.Method == "OPTIONS" {
if r.Method == http.MethodOptions {
c.logf(" Actual request no headers added: method == %s", r.Method)
return
}
@ -316,7 +314,7 @@ func (c *Cors) handleActualRequest(w http.ResponseWriter, r *http.Request) {
c.logf(" Actual request no headers added: missing origin")
return
}
if !c.isOriginAllowed(origin) {
if !c.isOriginAllowed(r, origin) {
c.logf(" Actual request no headers added: origin '%s' not allowed", origin)
return
}
@ -330,7 +328,11 @@ func (c *Cors) handleActualRequest(w http.ResponseWriter, r *http.Request) {
return
}
headers.Set("Access-Control-Allow-Origin", origin)
if c.allowedOriginsAll {
headers.Set("Access-Control-Allow-Origin", "*")
} else {
headers.Set("Access-Control-Allow-Origin", origin)
}
if len(c.exposedHeaders) > 0 {
headers.Set("Access-Control-Expose-Headers", strings.Join(c.exposedHeaders, ", "))
}
@ -349,7 +351,10 @@ func (c *Cors) logf(format string, a ...interface{}) {
// isOriginAllowed checks if a given origin is allowed to perform cross-domain requests
// on the endpoint
func (c *Cors) isOriginAllowed(origin string) bool {
func (c *Cors) isOriginAllowed(r *http.Request, origin string) bool {
if c.allowOriginRequestFunc != nil {
return c.allowOriginRequestFunc(r, origin)
}
if c.allowOriginFunc != nil {
return c.allowOriginFunc(origin)
}
@ -378,7 +383,7 @@ func (c *Cors) isMethodAllowed(method string) bool {
return false
}
method = strings.ToUpper(method)
if method == "OPTIONS" {
if method == http.MethodOptions {
// Always allow preflight requests
return true
}

1
vendor/github.com/rs/cors/go.mod generated vendored Normal file
View File

@ -0,0 +1 @@
module github.com/rs/cors

7
vendor/github.com/rs/cors/utils.go generated vendored
View File

@ -39,19 +39,20 @@ func parseHeaderList(headerList string) []string {
headers := make([]string, 0, t)
for i := 0; i < l; i++ {
b := headerList[i]
if b >= 'a' && b <= 'z' {
switch {
case b >= 'a' && b <= 'z':
if upper {
h = append(h, b-toLower)
} else {
h = append(h, b)
}
} else if b >= 'A' && b <= 'Z' {
case b >= 'A' && b <= 'Z':
if !upper {
h = append(h, b+toLower)
} else {
h = append(h, b)
}
} else if b == '-' || b == '_' || (b >= '0' && b <= '9') {
case b == '-' || b == '_' || (b >= '0' && b <= '9'):
h = append(h, b)
}

View File

@ -1,7 +0,0 @@
language: go
go:
- 1.5
- tip
matrix:
allow_failures:
- go: tip

View File

@ -1,19 +0,0 @@
Copyright (c) 2015 Olivier Poitrey <rs@dailymotion.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,134 +0,0 @@
# XHandler
[![godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/rs/xhandler) [![license](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://raw.githubusercontent.com/rs/xhandler/master/LICENSE) [![Build Status](https://travis-ci.org/rs/xhandler.svg?branch=master)](https://travis-ci.org/rs/xhandler) [![Coverage](http://gocover.io/_badge/github.com/rs/xhandler)](http://gocover.io/github.com/rs/xhandler)
XHandler is a bridge between [net/context](https://godoc.org/golang.org/x/net/context) and `http.Handler`.
It lets you enforce `net/context` in your handlers without sacrificing compatibility with existing `http.Handlers` nor imposing a specific router.
Thanks to `net/context` deadline management, `xhandler` is able to enforce a per request deadline and will cancel the context when the client closes the connection unexpectedly.
You may create your own `net/context` aware handler pretty much the same way as you would do with http.Handler.
Read more about xhandler on [Dailymotion engineering blog](http://engineering.dailymotion.com/our-way-to-go/).
## Installing
go get -u github.com/rs/xhandler
## Usage
```go
package main
import (
"log"
"net/http"
"time"
"github.com/rs/cors"
"github.com/rs/xhandler"
"golang.org/x/net/context"
)
type myMiddleware struct {
next xhandler.HandlerC
}
func (h myMiddleware) ServeHTTPC(ctx context.Context, w http.ResponseWriter, r *http.Request) {
ctx = context.WithValue(ctx, "test", "World")
h.next.ServeHTTPC(ctx, w, r)
}
func main() {
c := xhandler.Chain{}
// Add close notifier handler so context is cancelled when the client closes
// the connection
c.UseC(xhandler.CloseHandler)
// Add timeout handler
c.UseC(xhandler.TimeoutHandler(2 * time.Second))
// Middleware putting something in the context
c.UseC(func(next xhandler.HandlerC) xhandler.HandlerC {
return myMiddleware{next: next}
})
// Mix it with a non-context-aware middleware handler
c.Use(cors.Default().Handler)
// Final handler (using handlerFuncC), reading from the context
xh := xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
value := ctx.Value("test").(string)
w.Write([]byte("Hello " + value))
})
// Bridge context aware handlers with http.Handler using xhandler.Handle()
http.Handle("/test", c.Handler(xh))
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
```
### Using xmux
Xhandler comes with an optional context aware [muxer](https://github.com/rs/xmux) forked from [httprouter](https://github.com/julienschmidt/httprouter):
```go
package main
import (
"fmt"
"log"
"net/http"
"time"
"github.com/rs/xhandler"
"github.com/rs/xmux"
"golang.org/x/net/context"
)
func main() {
c := xhandler.Chain{}
// Append a context-aware middleware handler
c.UseC(xhandler.CloseHandler)
// Another context-aware middleware handler
c.UseC(xhandler.TimeoutHandler(2 * time.Second))
mux := xmux.New()
// Use c.Handler to terminate the chain with your final handler
mux.GET("/welcome/:name", xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "Welcome %s!", xmux.Params(ctx).Get("name"))
}))
if err := http.ListenAndServe(":8080", c.Handler(mux)); err != nil {
log.Fatal(err)
}
}
```
See [xmux](https://github.com/rs/xmux) for more examples.
## Context Aware Middleware
Here is a list of `net/context` aware middleware handlers implementing `xhandler.HandlerC` interface.
Feel free to put up a PR linking your middleware if you have built one:
| Middleware | Author | Description |
| ---------- | ------ | ----------- |
| [xmux](https://github.com/rs/xmux) | [Olivier Poitrey](https://github.com/rs) | HTTP request muxer |
| [xlog](https://github.com/rs/xlog) | [Olivier Poitrey](https://github.com/rs) | HTTP handler logger |
| [xstats](https://github.com/rs/xstats) | [Olivier Poitrey](https://github.com/rs) | A generic client for service instrumentation |
| [xaccess](https://github.com/rs/xaccess) | [Olivier Poitrey](https://github.com/rs) | HTTP handler access logger with [xlog](https://github.com/rs/xlog) and [xstats](https://github.com/rs/xstats) |
| [cors](https://github.com/rs/cors) | [Olivier Poitrey](https://github.com/rs) | [Cross Origin Resource Sharing](http://www.w3.org/TR/cors/) (CORS) support |
## Licenses
All source code is licensed under the [MIT License](https://raw.github.com/rs/xhandler/master/LICENSE).

View File

@ -1,121 +0,0 @@
package xhandler
import (
"net/http"
"golang.org/x/net/context"
)
// Chain is a helper for chaining middleware handlers together for easier
// management.
type Chain []func(next HandlerC) HandlerC
// Add appends a variable number of additional middleware handlers
// to the middleware chain. Middleware handlers can either be
// context-aware or non-context aware handlers with the appropriate
// function signatures.
func (c *Chain) Add(f ...interface{}) {
for _, h := range f {
switch v := h.(type) {
case func(http.Handler) http.Handler:
c.Use(v)
case func(HandlerC) HandlerC:
c.UseC(v)
default:
panic("Adding invalid handler to the middleware chain")
}
}
}
// With creates a new middleware chain from an existing chain,
// extending it with additional middleware. Middleware handlers
// can either be context-aware or non-context aware handlers
// with the appropriate function signatures.
func (c *Chain) With(f ...interface{}) *Chain {
n := make(Chain, len(*c))
copy(n, *c)
n.Add(f...)
return &n
}
// UseC appends a context-aware handler to the middleware chain.
func (c *Chain) UseC(f func(next HandlerC) HandlerC) {
*c = append(*c, f)
}
// Use appends a standard http.Handler to the middleware chain without
// losing track of the context when inserted between two context aware handlers.
//
// Caveat: the f function will be called on each request so you are better off putting
// any initialization sequence outside of this function.
func (c *Chain) Use(f func(next http.Handler) http.Handler) {
xf := func(next HandlerC) HandlerC {
return HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
n := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTPC(ctx, w, r)
})
f(n).ServeHTTP(w, r)
})
}
*c = append(*c, xf)
}
// Handler wraps the provided final handler with all the middleware appended to
// the chain and returns a new standard http.Handler instance.
// The context.Background() context is injected automatically.
func (c Chain) Handler(xh HandlerC) http.Handler {
ctx := context.Background()
return c.HandlerCtx(ctx, xh)
}
// HandlerFC is a helper to provide a function (HandlerFuncC) to Handler().
//
// HandlerFC is equivalent to:
// c.Handler(xhandler.HandlerFuncC(xhc))
func (c Chain) HandlerFC(xhf HandlerFuncC) http.Handler {
ctx := context.Background()
return c.HandlerCtx(ctx, HandlerFuncC(xhf))
}
// HandlerH is a helper to provide a standard http handler (http.HandlerFunc)
// to Handler(). Your final handler won't have access to the context though.
func (c Chain) HandlerH(h http.Handler) http.Handler {
ctx := context.Background()
return c.HandlerCtx(ctx, HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
h.ServeHTTP(w, r)
}))
}
// HandlerF is a helper to provide a standard http handler function
// (http.HandlerFunc) to Handler(). Your final handler won't have access
// to the context though.
func (c Chain) HandlerF(hf http.HandlerFunc) http.Handler {
ctx := context.Background()
return c.HandlerCtx(ctx, HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
hf(w, r)
}))
}
// HandlerCtx wraps the provided final handler with all the middleware appended to
// the chain and returns a new standard http.Handler instance.
func (c Chain) HandlerCtx(ctx context.Context, xh HandlerC) http.Handler {
return New(ctx, c.HandlerC(xh))
}
// HandlerC wraps the provided final handler with all the middleware appended to
// the chain and returns a HandlerC instance.
func (c Chain) HandlerC(xh HandlerC) HandlerC {
for i := len(c) - 1; i >= 0; i-- {
xh = c[i](xh)
}
return xh
}
// HandlerCF wraps the provided final handler func with all the middleware appended to
// the chain and returns a HandlerC instance.
//
// HandlerCF is equivalent to:
// c.HandlerC(xhandler.HandlerFuncC(xhc))
func (c Chain) HandlerCF(xhc HandlerFuncC) HandlerC {
return c.HandlerC(HandlerFuncC(xhc))
}

View File

@ -1,59 +0,0 @@
package xhandler
import (
"net/http"
"time"
"golang.org/x/net/context"
)
// CloseHandler returns a Handler, cancelling the context when the client
// connection closes unexpectedly.
func CloseHandler(next HandlerC) HandlerC {
return HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
// Cancel the context if the client closes the connection
if wcn, ok := w.(http.CloseNotifier); ok {
var cancel context.CancelFunc
ctx, cancel = context.WithCancel(ctx)
defer cancel()
notify := wcn.CloseNotify()
go func() {
select {
case <-notify:
cancel()
case <-ctx.Done():
}
}()
}
next.ServeHTTPC(ctx, w, r)
})
}
// TimeoutHandler returns a Handler which adds a timeout to the context.
//
// Child handlers have the responsability of obeying the context deadline and to return
// an appropriate error (or not) response in case of timeout.
func TimeoutHandler(timeout time.Duration) func(next HandlerC) HandlerC {
return func(next HandlerC) HandlerC {
return HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
ctx, _ = context.WithTimeout(ctx, timeout)
next.ServeHTTPC(ctx, w, r)
})
}
}
// If is a special handler that will skip insert the condNext handler only if a condition
// applies at runtime.
func If(cond func(ctx context.Context, w http.ResponseWriter, r *http.Request) bool, condNext func(next HandlerC) HandlerC) func(next HandlerC) HandlerC {
return func(next HandlerC) HandlerC {
return HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
if cond(ctx, w, r) {
condNext(next).ServeHTTPC(ctx, w, r)
} else {
next.ServeHTTPC(ctx, w, r)
}
})
}
}

View File

@ -1,42 +0,0 @@
// Package xhandler provides a bridge between http.Handler and net/context.
//
// xhandler enforces net/context in your handlers without sacrificing
// compatibility with existing http.Handlers nor imposing a specific router.
//
// Thanks to net/context deadline management, xhandler is able to enforce
// a per request deadline and will cancel the context in when the client close
// the connection unexpectedly.
//
// You may create net/context aware middlewares pretty much the same way as
// you would with http.Handler.
package xhandler // import "github.com/rs/xhandler"
import (
"net/http"
"golang.org/x/net/context"
)
// HandlerC is a net/context aware http.Handler
type HandlerC interface {
ServeHTTPC(context.Context, http.ResponseWriter, *http.Request)
}
// HandlerFuncC type is an adapter to allow the use of ordinary functions
// as an xhandler.Handler. If f is a function with the appropriate signature,
// xhandler.HandlerFuncC(f) is a xhandler.Handler object that calls f.
type HandlerFuncC func(context.Context, http.ResponseWriter, *http.Request)
// ServeHTTPC calls f(ctx, w, r).
func (f HandlerFuncC) ServeHTTPC(ctx context.Context, w http.ResponseWriter, r *http.Request) {
f(ctx, w, r)
}
// New creates a conventional http.Handler injecting the provided root
// context to sub handlers. This handler is used as a bridge between conventional
// http.Handler and context aware handlers.
func New(ctx context.Context, h HandlerC) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.ServeHTTPC(ctx, w, r)
})
}

View File

@ -0,0 +1,15 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Editors
.idea

View File

@ -0,0 +1,3 @@
run:
modules-download-mode: vendor
deadline: 5m

View File

@ -0,0 +1,27 @@
notifications:
email: false
language: go
install: true
env:
- GO111MODULE=on
before_script:
- make install-linter
matrix:
include:
- go: "1.11.x"
env: GOFLAGS=-mod=vendor
script:
- make lint
# fails without -a
- go test -a ./... # make test
- go: "1.12.x"
env: GOFLAGS=-mod=vendor
script:
- make lint
# fails without -a
- go test -a ./... # make test

373
vendor/github.com/status-im/status-protocol-go/LICENSE generated vendored Normal file
View File

@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

View File

@ -0,0 +1,43 @@
GO111MODULE = on
ENABLE_METRICS ?= true
BUILD_FLAGS ?= $(shell echo "-ldflags '\
-X github.com/status-im/status-protocol-go/vendor/github.com/ethereum/go-ethereum/metrics.EnabledStr=$(ENABLE_METRICS)'")
test:
go test ./...
.PHONY: test
test-race:
go test -race ./...
.PHONY: test-race
lint:
golangci-lint run -v
.PHONY: lint
vendor:
go mod tidy
go mod vendor
modvendor -copy="**/*.c **/*.h" -v
.PHONY: vendor
install-linter:
# install linter
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.17.1
.PHONY: install-linter
install-dev:
# a tool to vendor non-go files
go get github.com/goware/modvendor@latest
go get github.com/golang/mock/gomock@latest
go install github.com/golang/mock/mockgen
go get github.com/kevinburke/go-bindata/go-bindata@v3.13.0
go get github.com/golang/protobuf/protoc-gen-go@v1.3.1
.PHONY: install-dev
generate:
go generate ./...
.PHONY: generate

View File

@ -0,0 +1,5 @@
# status-protocol-go
This is the Status Protocol implementation in Go.
TBD

View File

@ -0,0 +1,492 @@
package statusproto
import (
"context"
"crypto/ecdsa"
"time"
"go.uber.org/zap"
"github.com/status-im/status-protocol-go/encryption/sharedsecret"
"github.com/status-im/status-protocol-go/transport/whisper/filter"
"github.com/ethereum/go-ethereum/crypto"
"github.com/golang/protobuf/proto"
"github.com/pkg/errors"
whisper "github.com/status-im/whisper/whisperv6"
"github.com/status-im/status-protocol-go/encryption"
"github.com/status-im/status-protocol-go/encryption/multidevice"
transport "github.com/status-im/status-protocol-go/transport/whisper"
protocol "github.com/status-im/status-protocol-go/v1"
)
// Whisper message properties.
const (
whisperTTL = 15
whisperPoW = 0.002
whisperPoWTime = 5
)
// whisperAdapter is a bridge between encryption and transport
// layers.
type whisperAdapter struct {
privateKey *ecdsa.PrivateKey
transport *transport.WhisperServiceTransport
protocol *encryption.Protocol
logger *zap.Logger
featureFlags featureFlags
}
func newWhisperAdapter(
pk *ecdsa.PrivateKey,
t *transport.WhisperServiceTransport,
p *encryption.Protocol,
featureFlags featureFlags,
logger *zap.Logger,
) *whisperAdapter {
if logger == nil {
logger = zap.NewNop()
}
return &whisperAdapter{
privateKey: pk,
transport: t,
protocol: p,
featureFlags: featureFlags,
logger: logger.With(zap.Namespace("whisperAdapter")),
}
}
func (a *whisperAdapter) JoinPublic(chatID string) error {
return a.transport.JoinPublic(chatID)
}
func (a *whisperAdapter) LeavePublic(chatID string) error {
return a.transport.LeavePublic(chatID)
}
func (a *whisperAdapter) JoinPrivate(publicKey *ecdsa.PublicKey) error {
return a.transport.JoinPrivate(publicKey)
}
func (a *whisperAdapter) LeavePrivate(publicKey *ecdsa.PublicKey) error {
return a.transport.LeavePrivate(publicKey)
}
// RetrievePublicMessages retrieves the collected public messages.
// It implies joining a chat if it has not been joined yet.
func (a *whisperAdapter) RetrievePublicMessages(chatID string) ([]*protocol.Message, error) {
messages, err := a.transport.RetrievePublicMessages(chatID)
if err != nil {
return nil, err
}
logger := a.logger.With(zap.String("site", "RetrievePublicMessages"))
decodedMessages := make([]*protocol.Message, 0, len(messages))
for _, item := range messages {
shhMessage := whisper.ToWhisperMessage(item)
hlogger := logger.With(zap.Binary("hash", shhMessage.Hash))
hlogger.Debug("received a public message")
statusMessage, err := a.decodeMessage(shhMessage)
if err != nil {
hlogger.Error("failed to decode message", zap.Error(err))
continue
}
switch m := statusMessage.Message.(type) {
case protocol.Message:
m.ID = statusMessage.ID
m.SigPubKey = statusMessage.SigPubKey
decodedMessages = append(decodedMessages, &m)
default:
hlogger.Error("skipped a public message of unsupported type")
}
}
return decodedMessages, nil
}
// RetrievePrivateMessages retrieves the collected private messages.
// It implies joining a chat if it has not been joined yet.
func (a *whisperAdapter) RetrievePrivateMessages(publicKey *ecdsa.PublicKey) ([]*protocol.Message, error) {
messages, err := a.transport.RetrievePrivateMessages(publicKey)
if err != nil {
return nil, err
}
logger := a.logger.With(zap.String("site", "RetrievePrivateMessages"))
decodedMessages := make([]*protocol.Message, 0, len(messages))
for _, item := range messages {
shhMessage := whisper.ToWhisperMessage(item)
hlogger := logger.With(zap.Binary("hash", shhMessage.Hash))
hlogger.Debug("received a private message")
err := a.decryptMessage(context.Background(), shhMessage)
if err != nil {
hlogger.Error("failed to decrypt a message", zap.Error(err))
}
statusMessage, err := a.decodeMessage(shhMessage)
if err != nil {
hlogger.Error("failed to decode a message", zap.Error(err))
continue
}
switch m := statusMessage.Message.(type) {
case protocol.Message:
m.ID = statusMessage.ID
m.SigPubKey = statusMessage.SigPubKey
decodedMessages = append(decodedMessages, &m)
case protocol.PairMessage:
fromOurDevice := isPubKeyEqual(statusMessage.SigPubKey, &a.privateKey.PublicKey)
if !fromOurDevice {
hlogger.Debug("received PairMessage from not our device, skipping")
break
}
metadata := &multidevice.InstallationMetadata{
Name: m.Name,
FCMToken: m.FCMToken,
DeviceType: m.DeviceType,
}
err := a.protocol.SetInstallationMetadata(&a.privateKey.PublicKey, m.InstallationID, metadata)
if err != nil {
return nil, err
}
}
}
return decodedMessages, nil
}
// DEPRECATED
func (a *whisperAdapter) RetrieveRawAll() (map[filter.Chat][]*whisper.Message, error) {
chatWithMessages, err := a.transport.RetrieveRawAll()
if err != nil {
return nil, err
}
logger := a.logger.With(zap.String("site", "RetrieveRawAll"))
result := make(map[filter.Chat][]*whisper.Message)
for chat, messages := range chatWithMessages {
for _, message := range messages {
shhMessage := whisper.ToWhisperMessage(message)
err := a.decryptMessage(context.Background(), shhMessage)
if err != nil {
logger.Warn("failed to decrypt a message", zap.Error(err), zap.Binary("messageID", shhMessage.Hash))
}
result[chat] = append(result[chat], shhMessage)
}
}
return result, nil
}
// DEPRECATED
func (a *whisperAdapter) RetrieveRaw(filterID string) ([]*whisper.Message, error) {
messages, err := a.transport.RetrieveRaw(filterID)
if err != nil {
return nil, err
}
logger := a.logger.With(zap.String("site", "RetrieveRaw"))
var result []*whisper.Message
for _, message := range messages {
shhMessage := whisper.ToWhisperMessage(message)
err := a.decryptMessage(context.Background(), shhMessage)
if err != nil {
logger.Warn("failed to decrypt a message", zap.Error(err), zap.Binary("messageID", shhMessage.Hash))
}
result = append(result, shhMessage)
}
return result, nil
}
func (a *whisperAdapter) decodeMessage(message *whisper.Message) (*protocol.StatusMessage, error) {
publicKey, err := crypto.UnmarshalPubkey(message.Sig)
if err != nil {
return nil, err
}
decoded, err := protocol.DecodeMessage(publicKey, message.Payload)
if err != nil {
return nil, err
}
return &decoded, nil
}
func (a *whisperAdapter) decryptMessage(ctx context.Context, message *whisper.Message) error {
publicKey, err := crypto.UnmarshalPubkey(message.Sig)
if err != nil {
return errors.Wrap(err, "failed to get signature")
}
var protocolMessage encryption.ProtocolMessage
err = proto.Unmarshal(message.Payload, &protocolMessage)
if err != nil {
return errors.Wrap(err, "failed to unmarshal ProtocolMessage")
}
logger := a.logger.With(zap.String("site", "decryptMessage"))
payload, err := a.protocol.HandleMessage(
a.privateKey,
publicKey,
&protocolMessage,
message.Hash,
)
if err == encryption.ErrDeviceNotFound {
handleErr := a.handleErrDeviceNotFound(ctx, publicKey)
if handleErr != nil {
logger.Error("failed to handle error", zap.Error(err), zap.NamedError("handleErr", handleErr))
}
}
if err != nil {
return errors.Wrap(err, "failed to process an encrypted message")
}
message.Payload = payload
return nil
}
func (a *whisperAdapter) handleErrDeviceNotFound(ctx context.Context, publicKey *ecdsa.PublicKey) error {
now := time.Now().Unix()
advertise, err := a.protocol.ShouldAdvertiseBundle(publicKey, now)
if err != nil {
return err
}
if !advertise {
return nil
}
messageSpec, err := a.protocol.BuildBundleAdvertiseMessage(a.privateKey, publicKey)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
_, err = a.sendMessageSpec(ctx, publicKey, messageSpec)
if err != nil {
return err
}
a.protocol.ConfirmBundleAdvertisement(publicKey, now)
return nil
}
// SendPublic sends a public message passing chat name to the transport layer.
//
// Be aware that this method returns a message ID using protocol.MessageID
// instead of Whisper message hash.
func (a *whisperAdapter) SendPublic(ctx context.Context, chatName, chatID string, data []byte, clock int64) ([]byte, error) {
logger := a.logger.With(zap.String("site", "SendPublic"))
logger.Debug("sending a public message", zap.String("chat-name", chatName))
message := protocol.CreatePublicTextMessage(data, clock, chatName)
encodedMessage, err := a.encodeMessage(message)
if err != nil {
return nil, errors.Wrap(err, "failed to encode message")
}
newMessage := whisper.NewMessage{
TTL: whisperTTL,
Payload: encodedMessage,
PowTarget: whisperPoW,
PowTime: whisperPoWTime,
}
_, err = a.transport.SendPublic(ctx, newMessage, chatName)
if err != nil {
return nil, err
}
return protocol.MessageID(&a.privateKey.PublicKey, encodedMessage), nil
}
// SendPublicRaw takes encoded data, encrypts it and sends through the wire.
// DEPRECATED
func (a *whisperAdapter) SendPublicRaw(ctx context.Context, chatName string, data []byte) ([]byte, whisper.NewMessage, error) {
newMessage := whisper.NewMessage{
TTL: whisperTTL,
Payload: data,
PowTarget: whisperPoW,
PowTime: whisperPoWTime,
}
hash, err := a.transport.SendPublic(ctx, newMessage, chatName)
return hash, newMessage, err
}
func (a *whisperAdapter) SendContactCode(ctx context.Context, messageSpec *encryption.ProtocolMessageSpec) ([]byte, error) {
newMessage, err := a.messageSpecToWhisper(messageSpec)
if err != nil {
return nil, err
}
return a.transport.SendPublic(ctx, newMessage, filter.ContactCodeTopic(&a.privateKey.PublicKey))
}
func (a *whisperAdapter) encodeMessage(message protocol.Message) ([]byte, error) {
encodedMessage, err := protocol.EncodeMessage(message)
if err != nil {
return nil, errors.Wrap(err, "failed to encode message")
}
if a.featureFlags.sendV1Messages {
encodedMessage, err = protocol.WrapMessageV1(encodedMessage, a.privateKey)
if err != nil {
return nil, errors.Wrap(err, "failed to wrap message")
}
}
return encodedMessage, nil
}
// SendPrivate sends a one-to-one message. It needs to return it
// because the registered Whisper filter handles only incoming messages
// and our own messages need to be handled manually.
//
// This might be not true if a shared secret is used because it relies on
// symmetric encryption.
//
// Be aware that this method returns a message ID using protocol.MessageID
// instead of Whisper message hash.
func (a *whisperAdapter) SendPrivate(
ctx context.Context,
publicKey *ecdsa.PublicKey,
chatID string,
data []byte,
clock int64,
) ([]byte, *protocol.Message, error) {
logger := a.logger.With(zap.String("site", "SendPrivate"))
logger.Debug("sending a private message", zap.Binary("public-key", crypto.FromECDSAPub(publicKey)))
message := protocol.CreatePrivateTextMessage(data, clock, chatID)
encodedMessage, err := a.encodeMessage(message)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to encode message")
}
messageSpec, err := a.protocol.BuildDirectMessage(a.privateKey, publicKey, encodedMessage)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to encrypt message")
}
_, err = a.sendMessageSpec(ctx, publicKey, messageSpec)
if err != nil {
return nil, nil, err
}
return protocol.MessageID(&a.privateKey.PublicKey, encodedMessage), &message, nil
}
// SendPrivateRaw takes encoded data, encrypts it and sends through the wire.
// DEPRECATED
func (a *whisperAdapter) SendPrivateRaw(
ctx context.Context,
publicKey *ecdsa.PublicKey,
data []byte,
) ([]byte, whisper.NewMessage, error) {
a.logger.Debug(
"sending a private message",
zap.Binary("public-key", crypto.FromECDSAPub(publicKey)),
zap.String("site", "SendPrivateRaw"),
)
var newMessage whisper.NewMessage
messageSpec, err := a.protocol.BuildDirectMessage(a.privateKey, publicKey, data)
if err != nil {
return nil, newMessage, errors.Wrap(err, "failed to encrypt message")
}
newMessage, err = a.messageSpecToWhisper(messageSpec)
if err != nil {
return nil, newMessage, errors.Wrap(err, "failed to convert ProtocolMessageSpec to whisper.NewMessage")
}
hash, err := a.sendMessageSpec(ctx, publicKey, messageSpec)
return hash, newMessage, err
}
func (a *whisperAdapter) sendMessageSpec(ctx context.Context, publicKey *ecdsa.PublicKey, messageSpec *encryption.ProtocolMessageSpec) ([]byte, error) {
newMessage, err := a.messageSpecToWhisper(messageSpec)
if err != nil {
return nil, err
}
logger := a.logger.With(zap.String("site", "sendMessageSpec"))
switch {
case messageSpec.SharedSecret != nil:
logger.Debug("sending using shared secret")
return a.transport.SendPrivateWithSharedSecret(ctx, newMessage, publicKey, messageSpec.SharedSecret)
case messageSpec.PartitionedTopicMode() == encryption.PartitionTopicV1:
logger.Debug("sending partitioned topic")
return a.transport.SendPrivateWithPartitioned(ctx, newMessage, publicKey)
case !a.featureFlags.genericDiscoveryTopicEnabled:
logger.Debug("sending partitioned topic (generic discovery topic disabled)")
return a.transport.SendPrivateWithPartitioned(ctx, newMessage, publicKey)
default:
logger.Debug("sending using discovery topic")
return a.transport.SendPrivateOnDiscovery(ctx, newMessage, publicKey)
}
}
func (a *whisperAdapter) messageSpecToWhisper(spec *encryption.ProtocolMessageSpec) (whisper.NewMessage, error) {
var newMessage whisper.NewMessage
payload, err := proto.Marshal(spec.Message)
if err != nil {
return newMessage, err
}
newMessage = whisper.NewMessage{
TTL: whisperTTL,
Payload: payload,
PowTarget: whisperPoW,
PowTime: whisperPoWTime,
}
return newMessage, nil
}
func (a *whisperAdapter) handleSharedSecrets(secrets []*sharedsecret.Secret) error {
logger := a.logger.With(zap.String("site", "handleSharedSecrets"))
for _, secret := range secrets {
logger.Debug("received shared secret", zap.Binary("identity", crypto.FromECDSAPub(secret.Identity)))
fSecret := filter.NegotiatedSecret{
PublicKey: secret.Identity,
Key: secret.Key,
}
if err := a.transport.ProcessNegotiatedSecret(fSecret); err != nil {
return err
}
}
return nil
}
// isPubKeyEqual checks that two public keys are equal
func isPubKeyEqual(a, b *ecdsa.PublicKey) bool {
// the curve is always the same, just compare the points
return a.X.Cmp(b.X) == 0 && a.Y.Cmp(b.Y) == 0
}

View File

@ -0,0 +1,9 @@
package statusproto
import "crypto/ecdsa"
type Chat interface {
ID() string
PublicName() string
PublicKey() *ecdsa.PublicKey
}

View File

@ -0,0 +1,190 @@
package crypto
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/sha256"
"fmt"
"io"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/ecies"
dr "github.com/status-im/doubleratchet"
"golang.org/x/crypto/hkdf"
)
// EthereumCrypto is an implementation of Crypto with cryptographic primitives recommended
// by the Double Ratchet Algorithm specification. However, some details are different,
// see function comments for details.
type EthereumCrypto struct{}
// See the Crypto interface.
func (c EthereumCrypto) GenerateDH() (dr.DHPair, error) {
keys, err := crypto.GenerateKey()
if err != nil {
return nil, err
}
var publicKey [32]byte
copy(publicKey[:], crypto.CompressPubkey(&keys.PublicKey)[:32])
var privateKey [32]byte
copy(privateKey[:], crypto.FromECDSA(keys))
return DHPair{
PrvKey: privateKey,
PubKey: publicKey,
}, nil
}
// See the Crypto interface.
func (c EthereumCrypto) DH(dhPair dr.DHPair, dhPub dr.Key) dr.Key {
tmpKey := dhPair.PrivateKey()
privateKey, err := crypto.ToECDSA(tmpKey[:])
eciesPrivate := ecies.ImportECDSA(privateKey)
var a [32]byte
if err != nil {
return a
}
publicKey, err := crypto.DecompressPubkey(dhPub[:])
if err != nil {
return a
}
eciesPublic := ecies.ImportECDSAPublic(publicKey)
key, err := eciesPrivate.GenerateShared(
eciesPublic,
16,
16,
)
if err != nil {
return a
}
copy(a[:], key)
return a
}
// See the Crypto interface.
func (c EthereumCrypto) KdfRK(rk, dhOut dr.Key) (rootKey, chainKey, headerKey dr.Key) {
var (
// We can use a non-secret constant as the last argument
r = hkdf.New(sha256.New, dhOut[:], rk[:], []byte("rsZUpEuXUqqwXBvSy3EcievAh4cMj6QL"))
buf = make([]byte, 96)
)
// The only error here is an entropy limit which won't be reached for such a short buffer.
_, _ = io.ReadFull(r, buf)
copy(rootKey[:], buf[:32])
copy(chainKey[:], buf[32:64])
copy(headerKey[:], buf[64:96])
return
}
// See the Crypto interface.
func (c EthereumCrypto) KdfCK(ck dr.Key) (chainKey dr.Key, msgKey dr.Key) {
const (
ckInput = 15
mkInput = 16
)
h := hmac.New(sha256.New, ck[:])
_, _ = h.Write([]byte{ckInput})
copy(chainKey[:], h.Sum(nil))
h.Reset()
_, _ = h.Write([]byte{mkInput})
copy(msgKey[:], h.Sum(nil))
return chainKey, msgKey
}
// Encrypt uses a slightly different approach than in the algorithm specification:
// it uses AES-256-CTR instead of AES-256-CBC for security, ciphertext length and implementation
// complexity considerations.
func (c EthereumCrypto) Encrypt(mk dr.Key, plaintext, ad []byte) []byte {
encKey, authKey, iv := c.deriveEncKeys(mk)
ciphertext := make([]byte, aes.BlockSize+len(plaintext))
copy(ciphertext, iv[:])
var (
block, _ = aes.NewCipher(encKey[:]) // No error will occur here as encKey is guaranteed to be 32 bytes.
stream = cipher.NewCTR(block, iv[:])
)
stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext)
return append(ciphertext, c.computeSignature(authKey[:], ciphertext, ad)...)
}
// See the Crypto interface.
func (c EthereumCrypto) Decrypt(mk dr.Key, authCiphertext, ad []byte) ([]byte, error) {
var (
l = len(authCiphertext)
ciphertext = authCiphertext[:l-sha256.Size]
signature = authCiphertext[l-sha256.Size:]
)
// Check the signature.
encKey, authKey, _ := c.deriveEncKeys(mk)
if s := c.computeSignature(authKey[:], ciphertext, ad); !bytes.Equal(s, signature) {
return nil, fmt.Errorf("invalid signature")
}
// Decrypt.
var (
block, _ = aes.NewCipher(encKey[:]) // No error will occur here as encKey is guaranteed to be 32 bytes.
stream = cipher.NewCTR(block, ciphertext[:aes.BlockSize])
plaintext = make([]byte, len(ciphertext[aes.BlockSize:]))
)
stream.XORKeyStream(plaintext, ciphertext[aes.BlockSize:])
return plaintext, nil
}
// deriveEncKeys derive keys for message encryption and decryption. Returns (encKey, authKey, iv, err).
func (c EthereumCrypto) deriveEncKeys(mk dr.Key) (encKey dr.Key, authKey dr.Key, iv [16]byte) {
// First, derive encryption and authentication key out of mk.
salt := make([]byte, 32)
var (
r = hkdf.New(sha256.New, mk[:], salt, []byte("pcwSByyx2CRdryCffXJwy7xgVZWtW5Sh"))
buf = make([]byte, 80)
)
// The only error here is an entropy limit which won't be reached for such a short buffer.
_, _ = io.ReadFull(r, buf)
copy(encKey[:], buf[0:32])
copy(authKey[:], buf[32:64])
copy(iv[:], buf[64:80])
return
}
func (c EthereumCrypto) computeSignature(authKey, ciphertext, associatedData []byte) []byte {
h := hmac.New(sha256.New, authKey)
_, _ = h.Write(associatedData)
_, _ = h.Write(ciphertext)
return h.Sum(nil)
}
type DHPair struct {
PrvKey dr.Key
PubKey dr.Key
}
func (p DHPair) PrivateKey() dr.Key {
return p.PrvKey
}
func (p DHPair) PublicKey() dr.Key {
return p.PubKey
}

View File

@ -1,24 +1,26 @@
package chat
package encryption
import (
"crypto/ecdsa"
"encoding/hex"
"errors"
"fmt"
"sync"
"time"
ecrypto "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/ecies"
"github.com/ethereum/go-ethereum/log"
dr "github.com/status-im/doubleratchet"
"go.uber.org/zap"
"github.com/status-im/status-go/messaging/chat/protobuf"
"github.com/status-im/status-go/messaging/crypto"
"github.com/status-im/status-go/messaging/multidevice"
"github.com/status-im/status-protocol-go/crypto"
migrations "github.com/status-im/status-protocol-go/encryption/internal/sqlite"
"github.com/status-im/status-protocol-go/encryption/multidevice"
"github.com/status-im/status-protocol-go/sqlite"
)
var (
ErrSessionNotFound = errors.New("session not found")
errSessionNotFound = errors.New("session not found")
ErrDeviceNotFound = errors.New("device not found")
// ErrNotPairedDevice means that we received a message signed with our public key
// but from a device that has not been paired.
@ -30,21 +32,21 @@ var (
// If we have no bundles, we use a constant so that the message can reach any device.
const noInstallationID = "none"
type ConfirmationData struct {
type confirmationData struct {
header *dr.MessageHeader
drInfo *RatchetInfo
}
// EncryptionService defines a service that is responsible for the encryption aspect of the protocol.
type EncryptionService struct {
log log.Logger
persistence Persistence
config EncryptionServiceConfig
messageIDs map[string]*ConfirmationData
// encryptor defines a service that is responsible for the encryption aspect of the protocol.
type encryptor struct {
persistence *sqlitePersistence
config encryptorConfig
messageIDs map[string]*confirmationData
mutex sync.Mutex
logger *zap.Logger
}
type EncryptionServiceConfig struct {
type encryptorConfig struct {
InstallationID string
// Max number of installations we keep synchronized.
MaxInstallations int
@ -56,34 +58,48 @@ type EncryptionServiceConfig struct {
MaxMessageKeysPerSession int
// How long before we refresh the interval in milliseconds
BundleRefreshInterval int64
// The logging object
Logger *zap.Logger
}
// DefaultEncryptionServiceConfig returns the default values used by the encryption service
func DefaultEncryptionServiceConfig(installationID string) EncryptionServiceConfig {
return EncryptionServiceConfig{
// defaultEncryptorConfig returns the default values used by the encryption service
func defaultEncryptorConfig(installationID string, logger *zap.Logger) encryptorConfig {
if logger == nil {
logger = zap.NewNop()
}
return encryptorConfig{
MaxInstallations: 3,
MaxSkip: 1000,
MaxKeep: 3000,
MaxMessageKeysPerSession: 2000,
BundleRefreshInterval: 24 * 60 * 60 * 1000,
InstallationID: installationID,
Logger: logger,
}
}
// NewEncryptionService creates a new EncryptionService instance.
func NewEncryptionService(p Persistence, config EncryptionServiceConfig) *EncryptionService {
logger := log.New("package", "status-go/services/sshext.chat")
logger.Info("Initialized encryption service", "installationID", config.InstallationID)
return &EncryptionService{
log: logger,
persistence: p,
// newEncryptor creates a new EncryptionService instance.
func newEncryptor(dbPath, dbKey string, config encryptorConfig) (*encryptor, error) {
db, err := sqlite.Open(dbPath, dbKey, sqlite.MigrationConfig{
AssetNames: migrations.AssetNames(),
AssetGetter: func(name string) ([]byte, error) {
return migrations.Asset(name)
},
})
if err != nil {
return nil, err
}
return &encryptor{
persistence: newSQLitePersistence(db),
config: config,
mutex: sync.Mutex{},
messageIDs: make(map[string]*ConfirmationData),
}
messageIDs: make(map[string]*confirmationData),
logger: config.Logger.With(zap.Namespace("encryptor")),
}, nil
}
func (s *EncryptionService) keyFromActiveX3DH(theirIdentityKey []byte, theirSignedPreKey []byte, myIdentityKey *ecdsa.PrivateKey) ([]byte, *ecdsa.PublicKey, error) {
func (s *encryptor) keyFromActiveX3DH(theirIdentityKey []byte, theirSignedPreKey []byte, myIdentityKey *ecdsa.PrivateKey) ([]byte, *ecdsa.PublicKey, error) {
sharedKey, ephemeralPubKey, err := PerformActiveX3DH(theirIdentityKey, theirSignedPreKey, myIdentityKey)
if err != nil {
return nil, nil, err
@ -92,18 +108,17 @@ func (s *EncryptionService) keyFromActiveX3DH(theirIdentityKey []byte, theirSign
return sharedKey, ephemeralPubKey, nil
}
func (s *EncryptionService) getDRSession(id []byte) (dr.Session, error) {
sessionStorage := s.persistence.GetSessionStorage()
func (s *encryptor) getDRSession(id []byte) (dr.Session, error) {
sessionStorage := s.persistence.SessionStorage()
return dr.Load(
id,
sessionStorage,
dr.WithKeysStorage(s.persistence.GetKeysStorage()),
dr.WithKeysStorage(s.persistence.KeysStorage()),
dr.WithMaxSkip(s.config.MaxSkip),
dr.WithMaxKeep(s.config.MaxKeep),
dr.WithMaxMessageKeysPerSession(s.config.MaxMessageKeysPerSession),
dr.WithCrypto(crypto.EthereumCrypto{}),
)
}
func confirmationIDString(id []byte) string {
@ -111,33 +126,32 @@ func confirmationIDString(id []byte) string {
}
// ConfirmMessagesProcessed confirms and deletes message keys for the given messages
func (s *EncryptionService) ConfirmMessagesProcessed(messageIDs [][]byte) error {
func (s *encryptor) ConfirmMessageProcessed(messageID []byte) error {
s.mutex.Lock()
defer s.mutex.Unlock()
for _, idByte := range messageIDs {
id := confirmationIDString(idByte)
confirmationData, ok := s.messageIDs[id]
if !ok {
s.log.Debug("Could not confirm message", "messageID", id)
continue
}
// Load session from store first
session, err := s.getDRSession(confirmationData.drInfo.ID)
if err != nil {
return err
}
if err := session.DeleteMk(confirmationData.header.DH, confirmationData.header.N); err != nil {
return err
}
id := confirmationIDString(messageID)
confirmationData, ok := s.messageIDs[id]
if !ok {
s.logger.Debug("could not confirm message", zap.String("messageID", id))
return fmt.Errorf("message with ID %#x not found", messageID)
}
// Load session from store first
session, err := s.getDRSession(confirmationData.drInfo.ID)
if err != nil {
return err
}
if err := session.DeleteMk(confirmationData.header.DH, confirmationData.header.N); err != nil {
return err
}
return nil
}
// CreateBundle retrieves or creates an X3DH bundle given a private key
func (s *EncryptionService) CreateBundle(privateKey *ecdsa.PrivateKey, installations []*multidevice.Installation) (*protobuf.Bundle, error) {
func (s *encryptor) CreateBundle(privateKey *ecdsa.PrivateKey, installations []*multidevice.Installation) (*Bundle, error) {
ourIdentityKeyC := ecrypto.CompressPubkey(&privateKey.PublicKey)
bundleContainer, err := s.persistence.GetAnyPrivateBundle(ourIdentityKeyC, installations)
@ -145,8 +159,10 @@ func (s *EncryptionService) CreateBundle(privateKey *ecdsa.PrivateKey, installat
return nil, err
}
expired := bundleContainer != nil && bundleContainer.GetBundle().Timestamp < time.Now().Add(-1*time.Duration(s.config.BundleRefreshInterval)*time.Millisecond).UnixNano()
// If the bundle has expired we create a new one
if bundleContainer != nil && bundleContainer.GetBundle().Timestamp < time.Now().Add(-1*time.Duration(s.config.BundleRefreshInterval)*time.Millisecond).UnixNano() {
if expired {
// Mark sessions has expired
if err := s.persistence.MarkBundleExpired(bundleContainer.GetBundle().GetIdentity()); err != nil {
return nil, err
@ -175,7 +191,7 @@ func (s *EncryptionService) CreateBundle(privateKey *ecdsa.PrivateKey, installat
}
// DecryptWithDH decrypts message sent with a DH key exchange, and throws away the key after decryption
func (s *EncryptionService) DecryptWithDH(myIdentityKey *ecdsa.PrivateKey, theirEphemeralKey *ecdsa.PublicKey, payload []byte) ([]byte, error) {
func (s *encryptor) DecryptWithDH(myIdentityKey *ecdsa.PrivateKey, theirEphemeralKey *ecdsa.PublicKey, payload []byte) ([]byte, error) {
key, err := PerformDH(
ecies.ImportECDSA(myIdentityKey),
ecies.ImportECDSAPublic(theirEphemeralKey),
@ -189,20 +205,20 @@ func (s *EncryptionService) DecryptWithDH(myIdentityKey *ecdsa.PrivateKey, their
}
// keyFromPassiveX3DH decrypts message sent with a X3DH key exchange, storing the key for future exchanges
func (s *EncryptionService) keyFromPassiveX3DH(myIdentityKey *ecdsa.PrivateKey, theirIdentityKey *ecdsa.PublicKey, theirEphemeralKey *ecdsa.PublicKey, ourBundleID []byte) ([]byte, error) {
func (s *encryptor) keyFromPassiveX3DH(myIdentityKey *ecdsa.PrivateKey, theirIdentityKey *ecdsa.PublicKey, theirEphemeralKey *ecdsa.PublicKey, ourBundleID []byte) ([]byte, error) {
bundlePrivateKey, err := s.persistence.GetPrivateKeyBundle(ourBundleID)
if err != nil {
s.log.Error("Could not get private bundle", "err", err)
s.logger.Error("could not get private bundle", zap.Error(err))
return nil, err
}
if bundlePrivateKey == nil {
return nil, ErrSessionNotFound
return nil, errSessionNotFound
}
signedPreKey, err := ecrypto.ToECDSA(bundlePrivateKey)
if err != nil {
s.log.Error("Could not convert to ecdsa", "err", err)
s.logger.Error("could not convert to ecdsa", zap.Error(err))
return nil, err
}
@ -213,19 +229,19 @@ func (s *EncryptionService) keyFromPassiveX3DH(myIdentityKey *ecdsa.PrivateKey,
myIdentityKey,
)
if err != nil {
s.log.Error("Could not perform passive x3dh", "err", err)
s.logger.Error("could not perform passive x3dh", zap.Error(err))
return nil, err
}
return key, nil
}
// ProcessPublicBundle persists a bundle
func (s *EncryptionService) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey, b *protobuf.Bundle) error {
func (s *encryptor) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey, b *Bundle) error {
return s.persistence.AddPublicBundle(b)
}
// DecryptPayload decrypts the payload of a DirectMessageProtocol, given an identity private key and the sender's public key
func (s *EncryptionService) DecryptPayload(myIdentityKey *ecdsa.PrivateKey, theirIdentityKey *ecdsa.PublicKey, theirInstallationID string, msgs map[string]*protobuf.DirectMessageProtocol, messageID []byte) ([]byte, error) {
func (s *encryptor) DecryptPayload(myIdentityKey *ecdsa.PrivateKey, theirIdentityKey *ecdsa.PublicKey, theirInstallationID string, msgs map[string]*DirectMessageProtocol, messageID []byte) ([]byte, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
@ -280,22 +296,22 @@ func (s *EncryptionService) DecryptPayload(myIdentityKey *ecdsa.PrivateKey, thei
drInfo, err := s.persistence.GetRatchetInfo(drHeader.GetId(), theirIdentityKeyC, theirInstallationID)
if err != nil {
s.log.Error("Could not get ratchet info", "err", err)
s.logger.Error("could not get ratchet info", zap.Error(err))
return nil, err
}
// We mark the exchange as successful so we stop sending x3dh header
if err = s.persistence.RatchetInfoConfirmed(drHeader.GetId(), theirIdentityKeyC, theirInstallationID); err != nil {
s.log.Error("Could not confirm ratchet info", "err", err)
s.logger.Error("could not confirm ratchet info", zap.Error(err))
return nil, err
}
if drInfo == nil {
s.log.Error("Could not find a session")
return nil, ErrSessionNotFound
s.logger.Error("could not find a session")
return nil, errSessionNotFound
}
confirmationData := &ConfirmationData{
confirmationData := &confirmationData{
header: &drMessage.Header,
drInfo: drInfo,
}
@ -316,7 +332,7 @@ func (s *EncryptionService) DecryptPayload(myIdentityKey *ecdsa.PrivateKey, thei
return nil, errors.New("no key specified")
}
func (s *EncryptionService) createNewSession(drInfo *RatchetInfo, sk [32]byte, keyPair crypto.DHPair) (dr.Session, error) {
func (s *encryptor) createNewSession(drInfo *RatchetInfo, sk [32]byte, keyPair crypto.DHPair) (dr.Session, error) {
var err error
var session dr.Session
@ -325,8 +341,8 @@ func (s *EncryptionService) createNewSession(drInfo *RatchetInfo, sk [32]byte, k
drInfo.ID,
sk,
keyPair,
s.persistence.GetSessionStorage(),
dr.WithKeysStorage(s.persistence.GetKeysStorage()),
s.persistence.SessionStorage(),
dr.WithKeysStorage(s.persistence.KeysStorage()),
dr.WithMaxSkip(s.config.MaxSkip),
dr.WithMaxKeep(s.config.MaxKeep),
dr.WithMaxMessageKeysPerSession(s.config.MaxMessageKeysPerSession),
@ -336,8 +352,8 @@ func (s *EncryptionService) createNewSession(drInfo *RatchetInfo, sk [32]byte, k
drInfo.ID,
sk,
keyPair.PubKey,
s.persistence.GetSessionStorage(),
dr.WithKeysStorage(s.persistence.GetKeysStorage()),
s.persistence.SessionStorage(),
dr.WithKeysStorage(s.persistence.KeysStorage()),
dr.WithMaxSkip(s.config.MaxSkip),
dr.WithMaxKeep(s.config.MaxKeep),
dr.WithMaxMessageKeysPerSession(s.config.MaxMessageKeysPerSession),
@ -347,7 +363,7 @@ func (s *EncryptionService) createNewSession(drInfo *RatchetInfo, sk [32]byte, k
return session, err
}
func (s *EncryptionService) encryptUsingDR(theirIdentityKey *ecdsa.PublicKey, drInfo *RatchetInfo, payload []byte) ([]byte, *protobuf.DRHeader, error) {
func (s *encryptor) encryptUsingDR(theirIdentityKey *ecdsa.PublicKey, drInfo *RatchetInfo, payload []byte) ([]byte, *DRHeader, error) {
var err error
var session dr.Session
@ -381,7 +397,7 @@ func (s *EncryptionService) encryptUsingDR(theirIdentityKey *ecdsa.PublicKey, dr
return nil, nil, err
}
header := &protobuf.DRHeader{
header := &DRHeader{
Id: drInfo.BundleID,
Key: response.Header.DH[:],
N: response.Header.N,
@ -391,7 +407,7 @@ func (s *EncryptionService) encryptUsingDR(theirIdentityKey *ecdsa.PublicKey, dr
return response.Ciphertext, header, nil
}
func (s *EncryptionService) decryptUsingDR(theirIdentityKey *ecdsa.PublicKey, drInfo *RatchetInfo, payload *dr.Message) ([]byte, error) {
func (s *encryptor) decryptUsingDR(theirIdentityKey *ecdsa.PublicKey, drInfo *RatchetInfo, payload *dr.Message) ([]byte, error) {
var err error
var session dr.Session
@ -425,7 +441,7 @@ func (s *EncryptionService) decryptUsingDR(theirIdentityKey *ecdsa.PublicKey, dr
return plaintext, nil
}
func (s *EncryptionService) encryptWithDH(theirIdentityKey *ecdsa.PublicKey, payload []byte) (*protobuf.DirectMessageProtocol, error) {
func (s *encryptor) encryptWithDH(theirIdentityKey *ecdsa.PublicKey, payload []byte) (*DirectMessageProtocol, error) {
symmetricKey, ourEphemeralKey, err := PerformActiveDH(theirIdentityKey)
if err != nil {
return nil, err
@ -436,16 +452,16 @@ func (s *EncryptionService) encryptWithDH(theirIdentityKey *ecdsa.PublicKey, pay
return nil, err
}
return &protobuf.DirectMessageProtocol{
DHHeader: &protobuf.DHHeader{
return &DirectMessageProtocol{
DHHeader: &DHHeader{
Key: ecrypto.CompressPubkey(ourEphemeralKey),
},
Payload: encryptedPayload,
}, nil
}
func (s *EncryptionService) EncryptPayloadWithDH(theirIdentityKey *ecdsa.PublicKey, payload []byte) (map[string]*protobuf.DirectMessageProtocol, error) {
response := make(map[string]*protobuf.DirectMessageProtocol)
func (s *encryptor) EncryptPayloadWithDH(theirIdentityKey *ecdsa.PublicKey, payload []byte) (map[string]*DirectMessageProtocol, error) {
response := make(map[string]*DirectMessageProtocol)
dmp, err := s.encryptWithDH(theirIdentityKey, payload)
if err != nil {
return nil, err
@ -456,37 +472,41 @@ func (s *EncryptionService) EncryptPayloadWithDH(theirIdentityKey *ecdsa.PublicK
}
// GetPublicBundle returns the active installations bundles for a given user
func (s *EncryptionService) GetPublicBundle(theirIdentityKey *ecdsa.PublicKey, installations []*multidevice.Installation) (*protobuf.Bundle, error) {
func (s *encryptor) GetPublicBundle(theirIdentityKey *ecdsa.PublicKey, installations []*multidevice.Installation) (*Bundle, error) {
return s.persistence.GetPublicBundle(theirIdentityKey, installations)
}
// EncryptPayload returns a new DirectMessageProtocol with a given payload encrypted, given a recipient's public key and the sender private identity key
func (s *EncryptionService) EncryptPayload(theirIdentityKey *ecdsa.PublicKey, myIdentityKey *ecdsa.PrivateKey, installations []*multidevice.Installation, payload []byte) (map[string]*protobuf.DirectMessageProtocol, []*multidevice.Installation, error) {
func (s *encryptor) EncryptPayload(theirIdentityKey *ecdsa.PublicKey, myIdentityKey *ecdsa.PrivateKey, installations []*multidevice.Installation, payload []byte) (map[string]*DirectMessageProtocol, []*multidevice.Installation, error) {
logger := s.logger.With(
zap.String("site", "EncryptPayload"),
zap.Binary("their-identity-key", ecrypto.FromECDSAPub(theirIdentityKey)))
logger.Debug("encrypting payload")
// Which installations we are sending the message to
var targetedInstallations []*multidevice.Installation
s.mutex.Lock()
defer s.mutex.Unlock()
s.log.Debug("Sending message", "theirKey", theirIdentityKey)
theirIdentityKeyC := ecrypto.CompressPubkey(theirIdentityKey)
// We don't have any, send a message with DH
if len(installations) == 0 {
s.log.Debug("no installations, sending to all devices")
logger.Debug("no installations, sending to all devices")
encryptedPayload, err := s.EncryptPayloadWithDH(theirIdentityKey, payload)
return encryptedPayload, targetedInstallations, err
}
response := make(map[string]*protobuf.DirectMessageProtocol)
theirIdentityKeyC := ecrypto.CompressPubkey(theirIdentityKey)
response := make(map[string]*DirectMessageProtocol)
for _, installation := range installations {
installationID := installation.ID
s.log.Debug("Processing installation", "installationID", installationID)
ilogger := logger.With(zap.String("installation-id", installationID))
ilogger.Debug("processing installation")
if s.config.InstallationID == installationID {
continue
}
bundle, err := s.persistence.GetPublicBundle(theirIdentityKey, []*multidevice.Installation{installation})
if err != nil {
return nil, nil, err
@ -501,19 +521,19 @@ func (s *EncryptionService) EncryptPayload(theirIdentityKey *ecdsa.PublicKey, my
targetedInstallations = append(targetedInstallations, installation)
if drInfo != nil {
s.log.Debug("Found DR info", "installationID", installationID)
ilogger.Debug("found DR info for installation")
encryptedPayload, drHeader, err := s.encryptUsingDR(theirIdentityKey, drInfo, payload)
if err != nil {
return nil, nil, err
}
dmp := protobuf.DirectMessageProtocol{
dmp := DirectMessageProtocol{
Payload: encryptedPayload,
DRHeader: drHeader,
}
if drInfo.EphemeralKey != nil {
dmp.X3DHHeader = &protobuf.X3DHHeader{
dmp.X3DHHeader = &X3DHHeader{
Key: drInfo.EphemeralKey,
Id: drInfo.BundleID,
}
@ -527,11 +547,12 @@ func (s *EncryptionService) EncryptPayload(theirIdentityKey *ecdsa.PublicKey, my
// This should not be nil at this point
if theirSignedPreKeyContainer == nil {
s.log.Warn("Could not find either a ratchet info or a bundle for installationId", "installationID", installationID)
ilogger.Warn("could not find DR info or bundle for installation")
continue
}
s.log.Debug("DR info not found, using bundle", "installationID", installationID)
ilogger.Debug("DR info not found, using bundle")
theirSignedPreKey := theirSignedPreKeyContainer.GetSignedPreKey()
@ -547,7 +568,7 @@ func (s *EncryptionService) EncryptPayload(theirIdentityKey *ecdsa.PublicKey, my
return nil, nil, err
}
x3dhHeader := &protobuf.X3DHHeader{
x3dhHeader := &X3DHHeader{
Key: ourEphemeralKeyC,
Id: theirSignedPreKey,
}
@ -563,7 +584,7 @@ func (s *EncryptionService) EncryptPayload(theirIdentityKey *ecdsa.PublicKey, my
return nil, nil, err
}
dmp := &protobuf.DirectMessageProtocol{
dmp := &DirectMessageProtocol{
Payload: encryptedPayload,
X3DHHeader: x3dhHeader,
DRHeader: drHeader,
@ -573,7 +594,14 @@ func (s *EncryptionService) EncryptPayload(theirIdentityKey *ecdsa.PublicKey, my
}
}
s.log.Debug("Built message", "theirKey", theirIdentityKey)
var installationIDs []string
for _, i := range targetedInstallations {
installationIDs = append(installationIDs, i.ID)
}
logger.Info(
"built a message",
zap.Strings("installation-ids", installationIDs),
)
return response, targetedInstallations, nil
}

View File

@ -1,30 +1,30 @@
// Code generated by go-bindata.
// Code generated by go-bindata. DO NOT EDIT.
// sources:
// 1536754952_initial_schema.down.sql
// 1536754952_initial_schema.up.sql
// 1539249977_update_ratchet_info.down.sql
// 1539249977_update_ratchet_info.up.sql
// 1540715431_add_version.down.sql
// 1540715431_add_version.up.sql
// 1541164797_add_installations.down.sql
// 1541164797_add_installations.up.sql
// 1558084410_add_secret.down.sql
// 1558084410_add_secret.up.sql
// 1558588866_add_version.up.sql
// 1559627659_add_contact_code.down.sql
// 1559627659_add_contact_code.up.sql
// 1561059285_add_whisper_keys.down.sql
// 1561059285_add_whisper_keys.up.sql
// 1561368210_add_installation_metadata.down.sql
// 1561368210_add_installation_metadata.up.sql
// static.go
// DO NOT EDIT!
// 1536754952_initial_schema.down.sql (83B)
// 1536754952_initial_schema.up.sql (962B)
// 1539249977_update_ratchet_info.down.sql (311B)
// 1539249977_update_ratchet_info.up.sql (368B)
// 1540715431_add_version.down.sql (127B)
// 1540715431_add_version.up.sql (265B)
// 1541164797_add_installations.down.sql (26B)
// 1541164797_add_installations.up.sql (216B)
// 1558084410_add_secret.down.sql (56B)
// 1558084410_add_secret.up.sql (301B)
// 1558588866_add_version.up.sql (57B)
// 1559627659_add_contact_code.down.sql (32B)
// 1559627659_add_contact_code.up.sql (198B)
// 1561059285_add_whisper_keys.down.sql (25B)
// 1561059285_add_whisper_keys.up.sql (112B)
// 1561368210_add_installation_metadata.down.sql (35B)
// 1561368210_add_installation_metadata.up.sql (267B)
// doc.go (377B)
package migrations
package sqlite
import (
"bytes"
"compress/gzip"
"crypto/sha256"
"fmt"
"io"
"io/ioutil"
@ -37,7 +37,7 @@ import (
func bindataRead(data []byte, name string) ([]byte, error) {
gz, err := gzip.NewReader(bytes.NewBuffer(data))
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
return nil, fmt.Errorf("read %q: %v", name, err)
}
var buf bytes.Buffer
@ -45,7 +45,7 @@ func bindataRead(data []byte, name string) ([]byte, error) {
clErr := gz.Close()
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
return nil, fmt.Errorf("read %q: %v", name, err)
}
if clErr != nil {
return nil, err
@ -55,8 +55,9 @@ func bindataRead(data []byte, name string) ([]byte, error) {
}
type asset struct {
bytes []byte
info os.FileInfo
bytes []byte
info os.FileInfo
digest [sha256.Size]byte
}
type bindataFileInfo struct {
@ -100,8 +101,8 @@ func _1536754952_initial_schemaDownSql() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "1536754952_initial_schema.down.sql", size: 83, mode: os.FileMode(420), modTime: time.Unix(1561038914, 0)}
a := &asset{bytes: bytes, info: info}
info := bindataFileInfo{name: "1536754952_initial_schema.down.sql", size: 83, mode: os.FileMode(0644), modTime: time.Unix(1562427968, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x44, 0xcf, 0x76, 0x71, 0x1f, 0x5e, 0x9a, 0x43, 0xd8, 0xcd, 0xb8, 0xc3, 0x70, 0xc3, 0x7f, 0xfc, 0x90, 0xb4, 0x25, 0x1e, 0xf4, 0x66, 0x20, 0xb8, 0x33, 0x7e, 0xb0, 0x76, 0x1f, 0xc, 0xc0, 0x75}}
return a, nil
}
@ -120,8 +121,8 @@ func _1536754952_initial_schemaUpSql() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "1536754952_initial_schema.up.sql", size: 962, mode: os.FileMode(420), modTime: time.Unix(1561038914, 0)}
a := &asset{bytes: bytes, info: info}
info := bindataFileInfo{name: "1536754952_initial_schema.up.sql", size: 962, mode: os.FileMode(0644), modTime: time.Unix(1562427968, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xea, 0x90, 0x5a, 0x59, 0x3e, 0x3, 0xe2, 0x3c, 0x81, 0x42, 0xcd, 0x4c, 0x9a, 0xe8, 0xda, 0x93, 0x2b, 0x70, 0xa4, 0xd5, 0x29, 0x3e, 0xd5, 0xc9, 0x27, 0xb6, 0xb7, 0x65, 0xff, 0x0, 0xcb, 0xde}}
return a, nil
}
@ -140,8 +141,8 @@ func _1539249977_update_ratchet_infoDownSql() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "1539249977_update_ratchet_info.down.sql", size: 311, mode: os.FileMode(420), modTime: time.Unix(1561038914, 0)}
a := &asset{bytes: bytes, info: info}
info := bindataFileInfo{name: "1539249977_update_ratchet_info.down.sql", size: 311, mode: os.FileMode(0644), modTime: time.Unix(1562427968, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x1, 0xa4, 0xeb, 0xa0, 0xe6, 0xa0, 0xd4, 0x48, 0xbb, 0xad, 0x6f, 0x7d, 0x67, 0x8c, 0xbd, 0x25, 0xde, 0x1f, 0x73, 0x9a, 0xbb, 0xa8, 0xc9, 0x30, 0xb7, 0xa9, 0x7c, 0xaf, 0xb5, 0x1, 0x61, 0xdd}}
return a, nil
}
@ -160,8 +161,8 @@ func _1539249977_update_ratchet_infoUpSql() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "1539249977_update_ratchet_info.up.sql", size: 368, mode: os.FileMode(420), modTime: time.Unix(1561038914, 0)}
a := &asset{bytes: bytes, info: info}
info := bindataFileInfo{name: "1539249977_update_ratchet_info.up.sql", size: 368, mode: os.FileMode(0644), modTime: time.Unix(1562427968, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xc, 0x8e, 0xbf, 0x6f, 0xa, 0xc0, 0xe1, 0x3c, 0x42, 0x28, 0x88, 0x1d, 0xdb, 0xba, 0x1c, 0x83, 0xec, 0xba, 0xd3, 0x5f, 0x5c, 0x77, 0x5e, 0xa7, 0x46, 0x36, 0xec, 0x69, 0xa, 0x4b, 0x17, 0x79}}
return a, nil
}
@ -180,8 +181,8 @@ func _1540715431_add_versionDownSql() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "1540715431_add_version.down.sql", size: 127, mode: os.FileMode(420), modTime: time.Unix(1561038914, 0)}
a := &asset{bytes: bytes, info: info}
info := bindataFileInfo{name: "1540715431_add_version.down.sql", size: 127, mode: os.FileMode(0644), modTime: time.Unix(1562427968, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xf5, 0x9, 0x4, 0xe3, 0x76, 0x2e, 0xb8, 0x9, 0x23, 0xf0, 0x70, 0x93, 0xc4, 0x50, 0xe, 0x9d, 0x84, 0x22, 0x8c, 0x94, 0xd3, 0x24, 0x9, 0x9a, 0xc1, 0xa1, 0x48, 0x45, 0xfd, 0x40, 0x6e, 0xe6}}
return a, nil
}
@ -200,8 +201,8 @@ func _1540715431_add_versionUpSql() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "1540715431_add_version.up.sql", size: 265, mode: os.FileMode(420), modTime: time.Unix(1561038914, 0)}
a := &asset{bytes: bytes, info: info}
info := bindataFileInfo{name: "1540715431_add_version.up.sql", size: 265, mode: os.FileMode(0644), modTime: time.Unix(1562427968, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xc7, 0x4c, 0x36, 0x96, 0xdf, 0x16, 0x10, 0xa6, 0x27, 0x1a, 0x79, 0x8b, 0x42, 0x83, 0x23, 0xc, 0x7e, 0xb6, 0x3d, 0x2, 0xda, 0xa4, 0xb4, 0xd, 0x27, 0x55, 0xba, 0xdc, 0xb2, 0x88, 0x8f, 0xa6}}
return a, nil
}
@ -220,8 +221,8 @@ func _1541164797_add_installationsDownSql() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "1541164797_add_installations.down.sql", size: 26, mode: os.FileMode(420), modTime: time.Unix(1561038914, 0)}
a := &asset{bytes: bytes, info: info}
info := bindataFileInfo{name: "1541164797_add_installations.down.sql", size: 26, mode: os.FileMode(0644), modTime: time.Unix(1562427968, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xf5, 0xfd, 0xe6, 0xd8, 0xca, 0x3b, 0x38, 0x18, 0xee, 0x0, 0x5f, 0x36, 0x9e, 0x1e, 0xd, 0x19, 0x3e, 0xb4, 0x73, 0x53, 0xe9, 0xa5, 0xac, 0xdd, 0xa1, 0x2f, 0xc7, 0x6c, 0xa8, 0xd9, 0xa, 0x88}}
return a, nil
}
@ -240,8 +241,8 @@ func _1541164797_add_installationsUpSql() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "1541164797_add_installations.up.sql", size: 216, mode: os.FileMode(420), modTime: time.Unix(1561038914, 0)}
a := &asset{bytes: bytes, info: info}
info := bindataFileInfo{name: "1541164797_add_installations.up.sql", size: 216, mode: os.FileMode(0644), modTime: time.Unix(1562427968, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x2d, 0x18, 0x26, 0xb8, 0x88, 0x47, 0xdb, 0x83, 0xcc, 0xb6, 0x9d, 0x1c, 0x1, 0xae, 0x2f, 0xde, 0x97, 0x82, 0x3, 0x30, 0xa8, 0x63, 0xa1, 0x78, 0x4b, 0xa5, 0x9, 0x8, 0x75, 0xa2, 0x57, 0x81}}
return a, nil
}
@ -260,8 +261,8 @@ func _1558084410_add_secretDownSql() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "1558084410_add_secret.down.sql", size: 56, mode: os.FileMode(420), modTime: time.Unix(1561038914, 0)}
a := &asset{bytes: bytes, info: info}
info := bindataFileInfo{name: "1558084410_add_secret.down.sql", size: 56, mode: os.FileMode(0644), modTime: time.Unix(1562427968, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x49, 0xb, 0x65, 0xdf, 0x59, 0xbf, 0xe9, 0x5, 0x5b, 0x6f, 0xd5, 0x3a, 0xb7, 0x57, 0xe8, 0x78, 0x38, 0x73, 0x53, 0x57, 0xf7, 0x24, 0x4, 0xe4, 0xa2, 0x49, 0x22, 0xa2, 0xc6, 0xfd, 0x80, 0xa4}}
return a, nil
}
@ -280,8 +281,8 @@ func _1558084410_add_secretUpSql() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "1558084410_add_secret.up.sql", size: 301, mode: os.FileMode(420), modTime: time.Unix(1561038914, 0)}
a := &asset{bytes: bytes, info: info}
info := bindataFileInfo{name: "1558084410_add_secret.up.sql", size: 301, mode: os.FileMode(0644), modTime: time.Unix(1562427968, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xf5, 0x32, 0x36, 0x8e, 0x47, 0xb0, 0x8f, 0xc1, 0xc6, 0xf7, 0xc6, 0x9f, 0x2d, 0x44, 0x75, 0x2b, 0x26, 0xec, 0x6, 0xa0, 0x7b, 0xa5, 0xbd, 0xc8, 0x76, 0x8a, 0x82, 0x68, 0x2, 0x42, 0xb5, 0xf4}}
return a, nil
}
@ -300,8 +301,8 @@ func _1558588866_add_versionUpSql() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "1558588866_add_version.up.sql", size: 57, mode: os.FileMode(420), modTime: time.Unix(1561038914, 0)}
a := &asset{bytes: bytes, info: info}
info := bindataFileInfo{name: "1558588866_add_version.up.sql", size: 57, mode: os.FileMode(0644), modTime: time.Unix(1562427968, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x2a, 0xea, 0x64, 0x39, 0x61, 0x20, 0x83, 0x83, 0xb, 0x2e, 0x79, 0x64, 0xb, 0x53, 0xfa, 0xfe, 0xc6, 0xf7, 0x67, 0x42, 0xd3, 0x4f, 0xdc, 0x7e, 0x30, 0x32, 0xe8, 0x14, 0x41, 0xe9, 0xe7, 0x3b}}
return a, nil
}
@ -320,8 +321,8 @@ func _1559627659_add_contact_codeDownSql() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "1559627659_add_contact_code.down.sql", size: 32, mode: os.FileMode(420), modTime: time.Unix(1561038914, 0)}
a := &asset{bytes: bytes, info: info}
info := bindataFileInfo{name: "1559627659_add_contact_code.down.sql", size: 32, mode: os.FileMode(0644), modTime: time.Unix(1562427968, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x5d, 0x64, 0x6d, 0xce, 0x24, 0x42, 0x20, 0x8d, 0x4f, 0x37, 0xaa, 0x9d, 0xc, 0x57, 0x98, 0xc1, 0xd1, 0x1a, 0x34, 0xcd, 0x9f, 0x8f, 0x34, 0x86, 0xb3, 0xd3, 0xdc, 0xf1, 0x7d, 0xe5, 0x1b, 0x6e}}
return a, nil
}
@ -340,8 +341,8 @@ func _1559627659_add_contact_codeUpSql() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "1559627659_add_contact_code.up.sql", size: 198, mode: os.FileMode(420), modTime: time.Unix(1561038914, 0)}
a := &asset{bytes: bytes, info: info}
info := bindataFileInfo{name: "1559627659_add_contact_code.up.sql", size: 198, mode: os.FileMode(0644), modTime: time.Unix(1562427968, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x16, 0xf6, 0xc2, 0x62, 0x9c, 0xd2, 0xc9, 0x1e, 0xd8, 0xea, 0xaa, 0xea, 0x95, 0x8f, 0x89, 0x6a, 0x85, 0x5d, 0x9d, 0x99, 0x78, 0x3c, 0x90, 0x66, 0x99, 0x3e, 0x4b, 0x19, 0x62, 0xfb, 0x31, 0x4d}}
return a, nil
}
@ -360,8 +361,8 @@ func _1561059285_add_whisper_keysDownSql() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "1561059285_add_whisper_keys.down.sql", size: 25, mode: os.FileMode(420), modTime: time.Unix(1561361219, 0)}
a := &asset{bytes: bytes, info: info}
info := bindataFileInfo{name: "1561059285_add_whisper_keys.down.sql", size: 25, mode: os.FileMode(0644), modTime: time.Unix(1562427968, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xb9, 0x31, 0x3f, 0xce, 0xfa, 0x44, 0x36, 0x1b, 0xb0, 0xec, 0x5d, 0xb, 0x90, 0xb, 0x21, 0x4f, 0xd5, 0xe5, 0x50, 0xed, 0xc7, 0x43, 0xdf, 0x83, 0xb4, 0x3a, 0xc1, 0x55, 0x2e, 0x53, 0x7c, 0x67}}
return a, nil
}
@ -380,8 +381,8 @@ func _1561059285_add_whisper_keysUpSql() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "1561059285_add_whisper_keys.up.sql", size: 112, mode: os.FileMode(420), modTime: time.Unix(1561361219, 0)}
a := &asset{bytes: bytes, info: info}
info := bindataFileInfo{name: "1561059285_add_whisper_keys.up.sql", size: 112, mode: os.FileMode(0644), modTime: time.Unix(1562427968, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x25, 0x41, 0xc, 0x92, 0xdd, 0x9e, 0xff, 0x5d, 0xd0, 0x93, 0xe4, 0x24, 0x50, 0x29, 0xcf, 0xc6, 0xf7, 0x49, 0x3c, 0x73, 0xd9, 0x8c, 0xfa, 0xf2, 0xcf, 0xf6, 0x6f, 0xbc, 0x31, 0xe6, 0xf7, 0xe2}}
return a, nil
}
@ -400,8 +401,8 @@ func _1561368210_add_installation_metadataDownSql() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "1561368210_add_installation_metadata.down.sql", size: 35, mode: os.FileMode(420), modTime: time.Unix(1561969196, 0)}
a := &asset{bytes: bytes, info: info}
info := bindataFileInfo{name: "1561368210_add_installation_metadata.down.sql", size: 35, mode: os.FileMode(0644), modTime: time.Unix(1562427968, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xa8, 0xde, 0x3f, 0xd2, 0x4a, 0x50, 0x98, 0x56, 0xe3, 0xc0, 0xcd, 0x9d, 0xb0, 0x34, 0x3b, 0xe5, 0x62, 0x18, 0xb5, 0x20, 0xc9, 0x3e, 0xdc, 0x6a, 0x40, 0x36, 0x66, 0xea, 0x51, 0x8c, 0x71, 0xf5}}
return a, nil
}
@ -420,28 +421,28 @@ func _1561368210_add_installation_metadataUpSql() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "1561368210_add_installation_metadata.up.sql", size: 267, mode: os.FileMode(420), modTime: time.Unix(1561969196, 0)}
a := &asset{bytes: bytes, info: info}
info := bindataFileInfo{name: "1561368210_add_installation_metadata.up.sql", size: 267, mode: os.FileMode(0644), modTime: time.Unix(1562427968, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xb4, 0x71, 0x8f, 0x29, 0xb1, 0xaa, 0xd6, 0xd1, 0x8c, 0x17, 0xef, 0x6c, 0xd5, 0x80, 0xb8, 0x2c, 0xc3, 0xfe, 0xec, 0x24, 0x4d, 0xc8, 0x25, 0xd3, 0xb4, 0xcd, 0xa9, 0xac, 0x63, 0x61, 0xb2, 0x9c}}
return a, nil
}
var _staticGo = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x54\x8c\x41\x6a\xc3\x40\x0c\x45\xf7\x73\x8a\xbf\x6c\xa1\x1e\xed\x7b\x82\x52\x12\x08\x24\x17\xd0\xd8\x42\x16\xc6\x23\x33\x52\xee\x9f\x4d\x42\xc8\xf2\xc1\x7b\x8f\x08\x17\x9e\x37\x56\x41\x24\xa7\xcd\x90\xbd\xc9\x12\x2f\xfa\xfa\xbf\xfe\xe0\xef\x76\x3e\x7d\x63\x48\xf8\x7d\xcc\x12\x18\xa6\x6b\xc2\x7a\x3a\x72\x15\x34\xeb\x3c\x4c\xa2\x1c\x1f\xa7\x52\x88\xd4\x7f\x55\xba\x0c\x4e\x81\xfa\xd4\xac\x2f\x9c\x8c\xe9\xd8\x14\xbb\xe9\xe0\x34\xef\x81\xc9\x51\x2b\xd5\x4a\xbb\x44\xb0\x5a\x57\x5a\x1a\xbd\x05\x7a\x86\x55\x1d\xb5\x3c\x02\x00\x00\xff\xff\x97\x9b\xee\x8f\xb4\x00\x00\x00")
var _docGo = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x84\x8f\x3b\x72\xc3\x30\x0c\x44\x7b\x9d\x62\xc7\x8d\x9b\x88\x6c\x52\xa5\x4b\x99\x3e\x17\x80\x49\x88\xc4\x98\x1f\x85\x80\xfc\xb9\x7d\x46\x4e\x66\xe2\x2e\xed\x0e\xde\xe2\xad\xf7\xf8\xcc\xa2\x58\xa4\x30\x44\xd1\x38\xb0\x2a\x8d\x3b\x4e\x1c\x68\x53\xc6\x21\x89\xe5\xed\xe4\x42\xaf\x5e\x8d\x6c\xd3\x59\xaa\xaf\x92\x06\x19\xfb\xcb\xeb\x61\xf2\x1e\x81\xda\xd1\x90\xa9\xc5\xc2\x8f\x2e\x85\x1a\x0d\x93\x96\x70\x15\xcb\x20\xac\x83\x17\xb9\x39\xbc\x1b\x0a\x93\x1a\x2c\x93\x1d\x15\x96\x19\x81\x94\xf7\x9a\xa5\x0f\xa4\x3e\x9f\xa4\x45\x32\x72\x7b\xf4\xb1\x3c\x25\xbb\x61\xa0\x52\x38\x62\x19\xbd\x3e\x58\xa5\xca\x88\x32\x38\x58\x1f\xf7\x17\x90\x2a\x1b\x1a\x55\xd6\x9d\xcf\x74\x61\xb4\xfe\xfb\x1e\xd4\xe2\xff\x8b\x70\xed\xe3\xac\x20\x05\xdf\x56\x0e\xc6\xd1\x4d\xd3\x4a\xe1\x4c\x89\xf1\x73\x27\xbd\xe9\x34\x79\x9f\xfa\x5b\xe2\xc6\x3b\xf9\xec\x39\xaf\xe7\x04\xfd\x2a\x62\x8c\xb9\xc3\x39\xff\x87\xb9\xd4\xe1\xa6\xef\x00\x00\x00\xff\xff\xcd\x86\x58\x5c\x79\x01\x00\x00")
func staticGoBytes() ([]byte, error) {
func docGoBytes() ([]byte, error) {
return bindataRead(
_staticGo,
"static.go",
_docGo,
"doc.go",
)
}
func staticGo() (*asset, error) {
bytes, err := staticGoBytes()
func docGo() (*asset, error) {
bytes, err := docGoBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "static.go", size: 180, mode: os.FileMode(420), modTime: time.Unix(1562216541, 0)}
a := &asset{bytes: bytes, info: info}
info := bindataFileInfo{name: "doc.go", size: 377, mode: os.FileMode(0644), modTime: time.Unix(1562955235, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x94, 0xd1, 0x5c, 0x73, 0x47, 0x65, 0xf9, 0x6e, 0xa0, 0xee, 0xb, 0x3d, 0xbe, 0xff, 0xef, 0xae, 0xc9, 0x46, 0x21, 0x85, 0x12, 0x46, 0xa1, 0x73, 0x74, 0xca, 0x71, 0xb1, 0xe1, 0x69, 0xe1, 0x82}}
return a, nil
}
@ -449,8 +450,8 @@ func staticGo() (*asset, error) {
// It returns an error if the asset could not be found or
// could not be loaded.
func Asset(name string) ([]byte, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
canonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[canonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
@ -460,6 +461,12 @@ func Asset(name string) ([]byte, error) {
return nil, fmt.Errorf("Asset %s not found", name)
}
// AssetString returns the asset contents as a string (instead of a []byte).
func AssetString(name string) (string, error) {
data, err := Asset(name)
return string(data), err
}
// MustAsset is like Asset but panics when Asset would return an error.
// It simplifies safe initialization of global variables.
func MustAsset(name string) []byte {
@ -471,12 +478,18 @@ func MustAsset(name string) []byte {
return a
}
// MustAssetString is like AssetString but panics when Asset would return an
// error. It simplifies safe initialization of global variables.
func MustAssetString(name string) string {
return string(MustAsset(name))
}
// AssetInfo loads and returns the asset info for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func AssetInfo(name string) (os.FileInfo, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
canonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[canonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
@ -486,6 +499,33 @@ func AssetInfo(name string) (os.FileInfo, error) {
return nil, fmt.Errorf("AssetInfo %s not found", name)
}
// AssetDigest returns the digest of the file with the given name. It returns an
// error if the asset could not be found or the digest could not be loaded.
func AssetDigest(name string) ([sha256.Size]byte, error) {
canonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[canonicalName]; ok {
a, err := f()
if err != nil {
return [sha256.Size]byte{}, fmt.Errorf("AssetDigest %s can't read by error: %v", name, err)
}
return a.digest, nil
}
return [sha256.Size]byte{}, fmt.Errorf("AssetDigest %s not found", name)
}
// Digests returns a map of all known files and their checksums.
func Digests() (map[string][sha256.Size]byte, error) {
mp := make(map[string][sha256.Size]byte, len(_bindata))
for name := range _bindata {
a, err := _bindata[name]()
if err != nil {
return nil, err
}
mp[name] = a.digest
}
return mp, nil
}
// AssetNames returns the names of the assets.
func AssetNames() []string {
names := make([]string, 0, len(_bindata))
@ -498,23 +538,40 @@ func AssetNames() []string {
// _bindata is a table, holding each asset generator, mapped to its name.
var _bindata = map[string]func() (*asset, error){
"1536754952_initial_schema.down.sql": _1536754952_initial_schemaDownSql,
"1536754952_initial_schema.up.sql": _1536754952_initial_schemaUpSql,
"1539249977_update_ratchet_info.down.sql": _1539249977_update_ratchet_infoDownSql,
"1539249977_update_ratchet_info.up.sql": _1539249977_update_ratchet_infoUpSql,
"1540715431_add_version.down.sql": _1540715431_add_versionDownSql,
"1540715431_add_version.up.sql": _1540715431_add_versionUpSql,
"1541164797_add_installations.down.sql": _1541164797_add_installationsDownSql,
"1541164797_add_installations.up.sql": _1541164797_add_installationsUpSql,
"1558084410_add_secret.down.sql": _1558084410_add_secretDownSql,
"1558084410_add_secret.up.sql": _1558084410_add_secretUpSql,
"1558588866_add_version.up.sql": _1558588866_add_versionUpSql,
"1559627659_add_contact_code.down.sql": _1559627659_add_contact_codeDownSql,
"1559627659_add_contact_code.up.sql": _1559627659_add_contact_codeUpSql,
"1561059285_add_whisper_keys.down.sql": _1561059285_add_whisper_keysDownSql,
"1561059285_add_whisper_keys.up.sql": _1561059285_add_whisper_keysUpSql,
"1561368210_add_installation_metadata.down.sql": _1561368210_add_installation_metadataDownSql,
"1561368210_add_installation_metadata.up.sql": _1561368210_add_installation_metadataUpSql,
"static.go": staticGo,
"doc.go": docGo,
}
// AssetDir returns the file names below a certain
@ -526,15 +583,15 @@ var _bindata = map[string]func() (*asset, error){
// img/
// a.png
// b.png
// then AssetDir("data") would return []string{"foo.txt", "img"}
// AssetDir("data/img") would return []string{"a.png", "b.png"}
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
// then AssetDir("data") would return []string{"foo.txt", "img"},
// AssetDir("data/img") would return []string{"a.png", "b.png"},
// AssetDir("foo.txt") and AssetDir("notexist") would return an error, and
// AssetDir("") will return []string{"data"}.
func AssetDir(name string) ([]string, error) {
node := _bintree
if len(name) != 0 {
cannonicalName := strings.Replace(name, "\\", "/", -1)
pathList := strings.Split(cannonicalName, "/")
canonicalName := strings.Replace(name, "\\", "/", -1)
pathList := strings.Split(canonicalName, "/")
for _, p := range pathList {
node = node.Children[p]
if node == nil {
@ -556,28 +613,29 @@ type bintree struct {
Func func() (*asset, error)
Children map[string]*bintree
}
var _bintree = &bintree{nil, map[string]*bintree{
"1536754952_initial_schema.down.sql": &bintree{_1536754952_initial_schemaDownSql, map[string]*bintree{}},
"1536754952_initial_schema.up.sql": &bintree{_1536754952_initial_schemaUpSql, map[string]*bintree{}},
"1539249977_update_ratchet_info.down.sql": &bintree{_1539249977_update_ratchet_infoDownSql, map[string]*bintree{}},
"1539249977_update_ratchet_info.up.sql": &bintree{_1539249977_update_ratchet_infoUpSql, map[string]*bintree{}},
"1540715431_add_version.down.sql": &bintree{_1540715431_add_versionDownSql, map[string]*bintree{}},
"1540715431_add_version.up.sql": &bintree{_1540715431_add_versionUpSql, map[string]*bintree{}},
"1541164797_add_installations.down.sql": &bintree{_1541164797_add_installationsDownSql, map[string]*bintree{}},
"1541164797_add_installations.up.sql": &bintree{_1541164797_add_installationsUpSql, map[string]*bintree{}},
"1558084410_add_secret.down.sql": &bintree{_1558084410_add_secretDownSql, map[string]*bintree{}},
"1558084410_add_secret.up.sql": &bintree{_1558084410_add_secretUpSql, map[string]*bintree{}},
"1558588866_add_version.up.sql": &bintree{_1558588866_add_versionUpSql, map[string]*bintree{}},
"1559627659_add_contact_code.down.sql": &bintree{_1559627659_add_contact_codeDownSql, map[string]*bintree{}},
"1559627659_add_contact_code.up.sql": &bintree{_1559627659_add_contact_codeUpSql, map[string]*bintree{}},
"1561059285_add_whisper_keys.down.sql": &bintree{_1561059285_add_whisper_keysDownSql, map[string]*bintree{}},
"1561059285_add_whisper_keys.up.sql": &bintree{_1561059285_add_whisper_keysUpSql, map[string]*bintree{}},
"1536754952_initial_schema.down.sql": &bintree{_1536754952_initial_schemaDownSql, map[string]*bintree{}},
"1536754952_initial_schema.up.sql": &bintree{_1536754952_initial_schemaUpSql, map[string]*bintree{}},
"1539249977_update_ratchet_info.down.sql": &bintree{_1539249977_update_ratchet_infoDownSql, map[string]*bintree{}},
"1539249977_update_ratchet_info.up.sql": &bintree{_1539249977_update_ratchet_infoUpSql, map[string]*bintree{}},
"1540715431_add_version.down.sql": &bintree{_1540715431_add_versionDownSql, map[string]*bintree{}},
"1540715431_add_version.up.sql": &bintree{_1540715431_add_versionUpSql, map[string]*bintree{}},
"1541164797_add_installations.down.sql": &bintree{_1541164797_add_installationsDownSql, map[string]*bintree{}},
"1541164797_add_installations.up.sql": &bintree{_1541164797_add_installationsUpSql, map[string]*bintree{}},
"1558084410_add_secret.down.sql": &bintree{_1558084410_add_secretDownSql, map[string]*bintree{}},
"1558084410_add_secret.up.sql": &bintree{_1558084410_add_secretUpSql, map[string]*bintree{}},
"1558588866_add_version.up.sql": &bintree{_1558588866_add_versionUpSql, map[string]*bintree{}},
"1559627659_add_contact_code.down.sql": &bintree{_1559627659_add_contact_codeDownSql, map[string]*bintree{}},
"1559627659_add_contact_code.up.sql": &bintree{_1559627659_add_contact_codeUpSql, map[string]*bintree{}},
"1561059285_add_whisper_keys.down.sql": &bintree{_1561059285_add_whisper_keysDownSql, map[string]*bintree{}},
"1561059285_add_whisper_keys.up.sql": &bintree{_1561059285_add_whisper_keysUpSql, map[string]*bintree{}},
"1561368210_add_installation_metadata.down.sql": &bintree{_1561368210_add_installation_metadataDownSql, map[string]*bintree{}},
"1561368210_add_installation_metadata.up.sql": &bintree{_1561368210_add_installation_metadataUpSql, map[string]*bintree{}},
"static.go": &bintree{staticGo, map[string]*bintree{}},
"1561368210_add_installation_metadata.up.sql": &bintree{_1561368210_add_installation_metadataUpSql, map[string]*bintree{}},
"doc.go": &bintree{docGo, map[string]*bintree{}},
}}
// RestoreAsset restores an asset under the given directory
// RestoreAsset restores an asset under the given directory.
func RestoreAsset(dir, name string) error {
data, err := Asset(name)
if err != nil {
@ -595,14 +653,10 @@ func RestoreAsset(dir, name string) error {
if err != nil {
return err
}
err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
if err != nil {
return err
}
return nil
return os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
}
// RestoreAssets restores an asset under the given directory recursively
// RestoreAssets restores an asset under the given directory recursively.
func RestoreAssets(dir, name string) error {
children, err := AssetDir(name)
// File
@ -620,7 +674,6 @@ func RestoreAssets(dir, name string) error {
}
func _filePath(dir, name string) string {
cannonicalName := strings.Replace(name, "\\", "/", -1)
return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
canonicalName := strings.Replace(name, "\\", "/", -1)
return filepath.Join(append([]string{dir}, strings.Split(canonicalName, "/")...)...)
}

View File

@ -2,6 +2,7 @@ package multidevice
import (
"crypto/ecdsa"
"database/sql"
"github.com/ethereum/go-ethereum/crypto"
)
@ -36,28 +37,28 @@ type Config struct {
InstallationID string
}
func New(config *Config, persistence Persistence) *Service {
return &Service{
config: config,
persistence: persistence,
}
}
type Service struct {
persistence Persistence
type Multidevice struct {
persistence *sqlitePersistence
config *Config
}
func (s *Service) InstallationID() string {
func New(db *sql.DB, config *Config) *Multidevice {
return &Multidevice{
config: config,
persistence: newSQLitePersistence(db),
}
}
func (s *Multidevice) InstallationID() string {
return s.config.InstallationID
}
func (s *Service) GetActiveInstallations(identity *ecdsa.PublicKey) ([]*Installation, error) {
func (s *Multidevice) GetActiveInstallations(identity *ecdsa.PublicKey) ([]*Installation, error) {
identityC := crypto.CompressPubkey(identity)
return s.persistence.GetActiveInstallations(s.config.MaxInstallations, identityC)
}
func (s *Service) GetOurActiveInstallations(identity *ecdsa.PublicKey) ([]*Installation, error) {
func (s *Multidevice) GetOurActiveInstallations(identity *ecdsa.PublicKey) ([]*Installation, error) {
identityC := crypto.CompressPubkey(identity)
installations, err := s.persistence.GetActiveInstallations(s.config.MaxInstallations-1, identityC)
if err != nil {
@ -72,7 +73,7 @@ func (s *Service) GetOurActiveInstallations(identity *ecdsa.PublicKey) ([]*Insta
return installations, nil
}
func (s *Service) GetOurInstallations(identity *ecdsa.PublicKey) ([]*Installation, error) {
func (s *Multidevice) GetOurInstallations(identity *ecdsa.PublicKey) ([]*Installation, error) {
var found bool
identityC := crypto.CompressPubkey(identity)
installations, err := s.persistence.GetInstallations(identityC)
@ -99,21 +100,21 @@ func (s *Service) GetOurInstallations(identity *ecdsa.PublicKey) ([]*Installatio
return installations, nil
}
func (s *Service) AddInstallations(identity []byte, timestamp int64, installations []*Installation, defaultEnabled bool) ([]*Installation, error) {
func (s *Multidevice) AddInstallations(identity []byte, timestamp int64, installations []*Installation, defaultEnabled bool) ([]*Installation, error) {
return s.persistence.AddInstallations(identity, timestamp, installations, defaultEnabled)
}
func (s *Service) SetInstallationMetadata(identity *ecdsa.PublicKey, installationID string, metadata *InstallationMetadata) error {
func (s *Multidevice) SetInstallationMetadata(identity *ecdsa.PublicKey, installationID string, metadata *InstallationMetadata) error {
identityC := crypto.CompressPubkey(identity)
return s.persistence.SetInstallationMetadata(identityC, installationID, metadata)
}
func (s *Service) EnableInstallation(identity *ecdsa.PublicKey, installationID string) error {
func (s *Multidevice) EnableInstallation(identity *ecdsa.PublicKey, installationID string) error {
identityC := crypto.CompressPubkey(identity)
return s.persistence.EnableInstallation(identityC, installationID)
}
func (s *Service) DisableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error {
func (s *Multidevice) DisableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error {
myIdentityKeyC := crypto.CompressPubkey(myIdentityKey)
return s.persistence.DisableInstallation(myIdentityKeyC, installationID)
}

View File

@ -1,21 +1,17 @@
package multidevice
import (
"database/sql"
)
import "database/sql"
// SQLLitePersistence represents a persistence service tied to an SQLite database
type SQLLitePersistence struct {
type sqlitePersistence struct {
db *sql.DB
}
// NewSQLLitePersistence creates a new SQLLitePersistence instance, given a path and a key
func NewSQLLitePersistence(db *sql.DB) *SQLLitePersistence {
return &SQLLitePersistence{db: db}
func newSQLitePersistence(db *sql.DB) *sqlitePersistence {
return &sqlitePersistence{db: db}
}
// GetActiveInstallations returns the active installations for a given identity
func (s *SQLLitePersistence) GetActiveInstallations(maxInstallations int, identity []byte) ([]*Installation, error) {
func (s *sqlitePersistence) GetActiveInstallations(maxInstallations int, identity []byte) ([]*Installation, error) {
stmt, err := s.db.Prepare(`SELECT installation_id, version
FROM installations
WHERE enabled = 1 AND identity = ?
@ -59,7 +55,7 @@ func (s *SQLLitePersistence) GetActiveInstallations(maxInstallations int, identi
// we both return the installations & the metadata
// metadata is currently stored in a separate table, as in some cases we
// might have metadata for a device, but no other information on the device
func (s *SQLLitePersistence) GetInstallations(identity []byte) ([]*Installation, error) {
func (s *sqlitePersistence) GetInstallations(identity []byte) ([]*Installation, error) {
installationMap := make(map[string]*Installation)
var installations []*Installation
@ -141,7 +137,7 @@ func (s *SQLLitePersistence) GetInstallations(identity []byte) ([]*Installation,
}
// AddInstallations adds the installations for a given identity, maintaining the enabled flag
func (s *SQLLitePersistence) AddInstallations(identity []byte, timestamp int64, installations []*Installation, defaultEnabled bool) ([]*Installation, error) {
func (s *sqlitePersistence) AddInstallations(identity []byte, timestamp int64, installations []*Installation, defaultEnabled bool) ([]*Installation, error) {
tx, err := s.db.Begin()
if err != nil {
return nil, err
@ -230,7 +226,7 @@ func (s *SQLLitePersistence) AddInstallations(identity []byte, timestamp int64,
}
// EnableInstallation enables the installation
func (s *SQLLitePersistence) EnableInstallation(identity []byte, installationID string) error {
func (s *sqlitePersistence) EnableInstallation(identity []byte, installationID string) error {
stmt, err := s.db.Prepare(`UPDATE installations
SET enabled = 1
WHERE identity = ? AND installation_id = ?`)
@ -244,8 +240,7 @@ func (s *SQLLitePersistence) EnableInstallation(identity []byte, installationID
}
// DisableInstallation disable the installation
func (s *SQLLitePersistence) DisableInstallation(identity []byte, installationID string) error {
func (s *sqlitePersistence) DisableInstallation(identity []byte, installationID string) error {
stmt, err := s.db.Prepare(`UPDATE installations
SET enabled = 0
WHERE identity = ? AND installation_id = ?`)
@ -258,7 +253,7 @@ func (s *SQLLitePersistence) DisableInstallation(identity []byte, installationID
}
// SetInstallationMetadata sets the metadata for a given installation
func (s *SQLLitePersistence) SetInstallationMetadata(identity []byte, installationID string, metadata *InstallationMetadata) error {
func (s *sqlitePersistence) SetInstallationMetadata(identity []byte, installationID string, metadata *InstallationMetadata) error {
stmt, err := s.db.Prepare(`INSERT INTO installation_metadata(name, device_type, fcm_token, identity, installation_id) VALUES(?,?,?,?,?)`)
if err != nil {
return err

View File

@ -1,4 +1,4 @@
package chat
package encryption
import (
"crypto/ecdsa"
@ -8,102 +8,51 @@ import (
"github.com/ethereum/go-ethereum/crypto"
dr "github.com/status-im/doubleratchet"
"github.com/status-im/status-go/messaging/chat/protobuf"
ecrypto "github.com/status-im/status-go/messaging/crypto"
msgdb "github.com/status-im/status-go/messaging/db"
"github.com/status-im/status-go/messaging/multidevice"
"github.com/status-im/status-go/messaging/sharedsecret"
ecrypto "github.com/status-im/status-protocol-go/crypto"
"github.com/status-im/status-protocol-go/encryption/multidevice"
)
// A safe max number of rows
// RatchetInfo holds the current ratchet state.
type RatchetInfo struct {
ID []byte
Sk []byte
PrivateKey []byte
PublicKey []byte
Identity []byte
BundleID []byte
EphemeralKey []byte
InstallationID string
}
// A safe max number of rows.
const maxNumberOfRows = 100000000
// SQLLitePersistence represents a persistence service tied to an SQLite database
type SQLLitePersistence struct {
DB *sql.DB
keysStorage dr.KeysStorage
sessionStorage dr.SessionStorage
secretStorage sharedsecret.Persistence
multideviceStorage multidevice.Persistence
type sqlitePersistence struct {
DB *sql.DB
keysStorage dr.KeysStorage
sessionStorage dr.SessionStorage
}
// SQLLiteKeysStorage represents a keys persistence service tied to an SQLite database
type SQLLiteKeysStorage struct {
db *sql.DB
}
// SQLLiteSessionStorage represents a session persistence service tied to an SQLite database
type SQLLiteSessionStorage struct {
db *sql.DB
}
// NewSQLLitePersistence creates a new SQLLitePersistence instance, given a path and a key
func NewSQLLitePersistence(path string, key string) (*SQLLitePersistence, error) {
s := &SQLLitePersistence{}
if err := s.Open(path, key); err != nil {
return nil, err
}
s.keysStorage = NewSQLLiteKeysStorage(s.DB)
s.sessionStorage = NewSQLLiteSessionStorage(s.DB)
s.secretStorage = sharedsecret.NewSQLLitePersistence(s.DB)
s.multideviceStorage = multidevice.NewSQLLitePersistence(s.DB)
return s, nil
}
// NewSQLLiteKeysStorage creates a new SQLLiteKeysStorage instance associated with the specified database
func NewSQLLiteKeysStorage(db *sql.DB) *SQLLiteKeysStorage {
return &SQLLiteKeysStorage{
db: db,
}
}
// NewSQLLiteSessionStorage creates a new SQLLiteSessionStorage instance associated with the specified database
func NewSQLLiteSessionStorage(db *sql.DB) *SQLLiteSessionStorage {
return &SQLLiteSessionStorage{
db: db,
func newSQLitePersistence(db *sql.DB) *sqlitePersistence {
return &sqlitePersistence{
DB: db,
keysStorage: newSQLiteKeysStorage(db),
sessionStorage: newSQLiteSessionStorage(db),
}
}
// GetKeysStorage returns the associated double ratchet KeysStorage object
func (s *SQLLitePersistence) GetKeysStorage() dr.KeysStorage {
func (s *sqlitePersistence) KeysStorage() dr.KeysStorage {
return s.keysStorage
}
// GetSessionStorage returns the associated double ratchet SessionStorage object
func (s *SQLLitePersistence) GetSessionStorage() dr.SessionStorage {
func (s *sqlitePersistence) SessionStorage() dr.SessionStorage {
return s.sessionStorage
}
// GetSharedSecretStorage returns the associated secretStorageObject
func (s *SQLLitePersistence) GetSharedSecretStorage() sharedsecret.Persistence {
return s.secretStorage
}
// GetMultideviceStorage returns the associated multideviceStorage
func (s *SQLLitePersistence) GetMultideviceStorage() multidevice.Persistence {
return s.multideviceStorage
}
// Open opens a file at the specified path
func (s *SQLLitePersistence) Open(path string, key string) error {
db, err := msgdb.Open(path, key, msgdb.KdfIterationsNumber)
if err != nil {
return err
}
s.DB = db
return nil
}
// AddPrivateBundle adds the specified BundleContainer to the database
func (s *SQLLitePersistence) AddPrivateBundle(bc *protobuf.BundleContainer) error {
func (s *sqlitePersistence) AddPrivateBundle(bc *BundleContainer) error {
tx, err := s.DB.Begin()
if err != nil {
return err
@ -157,7 +106,7 @@ func (s *SQLLitePersistence) AddPrivateBundle(bc *protobuf.BundleContainer) erro
}
// AddPublicBundle adds the specified Bundle to the database
func (s *SQLLitePersistence) AddPublicBundle(b *protobuf.Bundle) error {
func (s *sqlitePersistence) AddPublicBundle(b *Bundle) error {
tx, err := s.DB.Begin()
if err != nil {
@ -210,7 +159,7 @@ func (s *SQLLitePersistence) AddPublicBundle(b *protobuf.Bundle) error {
}
// GetAnyPrivateBundle retrieves any bundle from the database containing a private key
func (s *SQLLitePersistence) GetAnyPrivateBundle(myIdentityKey []byte, installations []*multidevice.Installation) (*protobuf.BundleContainer, error) {
func (s *sqlitePersistence) GetAnyPrivateBundle(myIdentityKey []byte, installations []*multidevice.Installation) (*BundleContainer, error) {
versions := make(map[string]uint32)
/* #nosec */
@ -246,11 +195,11 @@ func (s *SQLLitePersistence) GetAnyPrivateBundle(myIdentityKey []byte, installat
defer rows.Close()
bundle := &protobuf.Bundle{
SignedPreKeys: make(map[string]*protobuf.SignedPreKey),
bundle := &Bundle{
SignedPreKeys: make(map[string]*SignedPreKey),
}
bundleContainer := &protobuf.BundleContainer{
bundleContainer := &BundleContainer{
Bundle: bundle,
}
@ -274,7 +223,7 @@ func (s *SQLLitePersistence) GetAnyPrivateBundle(myIdentityKey []byte, installat
bundle.Timestamp = timestamp
}
bundle.SignedPreKeys[installationID] = &protobuf.SignedPreKey{
bundle.SignedPreKeys[installationID] = &SignedPreKey{
SignedPreKey: signedPreKey,
Version: version,
ProtocolVersion: versions[installationID],
@ -292,7 +241,7 @@ func (s *SQLLitePersistence) GetAnyPrivateBundle(myIdentityKey []byte, installat
}
// GetPrivateKeyBundle retrieves a private key for a bundle from the database
func (s *SQLLitePersistence) GetPrivateKeyBundle(bundleID []byte) ([]byte, error) {
func (s *sqlitePersistence) GetPrivateKeyBundle(bundleID []byte) ([]byte, error) {
stmt, err := s.DB.Prepare(`SELECT private_key
FROM bundles
WHERE signed_pre_key = ? LIMIT 1`)
@ -315,7 +264,7 @@ func (s *SQLLitePersistence) GetPrivateKeyBundle(bundleID []byte) ([]byte, error
}
// MarkBundleExpired expires any private bundle for a given identity
func (s *SQLLitePersistence) MarkBundleExpired(identity []byte) error {
func (s *sqlitePersistence) MarkBundleExpired(identity []byte) error {
stmt, err := s.DB.Prepare(`UPDATE bundles
SET expired = 1
WHERE identity = ? AND private_key IS NOT NULL`)
@ -330,7 +279,7 @@ func (s *SQLLitePersistence) MarkBundleExpired(identity []byte) error {
}
// GetPublicBundle retrieves an existing Bundle for the specified public key from the database
func (s *SQLLitePersistence) GetPublicBundle(publicKey *ecdsa.PublicKey, installations []*multidevice.Installation) (*protobuf.Bundle, error) {
func (s *sqlitePersistence) GetPublicBundle(publicKey *ecdsa.PublicKey, installations []*multidevice.Installation) (*Bundle, error) {
if len(installations) == 0 {
return nil, nil
@ -367,9 +316,9 @@ func (s *SQLLitePersistence) GetPublicBundle(publicKey *ecdsa.PublicKey, install
defer rows.Close()
bundle := &protobuf.Bundle{
bundle := &Bundle{
Identity: identity,
SignedPreKeys: make(map[string]*protobuf.SignedPreKey),
SignedPreKeys: make(map[string]*SignedPreKey),
}
for rows.Next() {
@ -386,7 +335,7 @@ func (s *SQLLitePersistence) GetPublicBundle(publicKey *ecdsa.PublicKey, install
return nil, err
}
bundle.SignedPreKeys[installationID] = &protobuf.SignedPreKey{
bundle.SignedPreKeys[installationID] = &SignedPreKey{
SignedPreKey: signedPreKey,
Version: version,
ProtocolVersion: versions[installationID],
@ -403,7 +352,7 @@ func (s *SQLLitePersistence) GetPublicBundle(publicKey *ecdsa.PublicKey, install
}
// AddRatchetInfo persists the specified ratchet info into the database
func (s *SQLLitePersistence) AddRatchetInfo(key []byte, identity []byte, bundleID []byte, ephemeralKey []byte, installationID string) error {
func (s *sqlitePersistence) AddRatchetInfo(key []byte, identity []byte, bundleID []byte, ephemeralKey []byte, installationID string) error {
stmt, err := s.DB.Prepare(`INSERT INTO ratchet_info_v2(symmetric_key, identity, bundle_id, ephemeral_key, installation_id)
VALUES(?, ?, ?, ?, ?)`)
if err != nil {
@ -423,7 +372,7 @@ func (s *SQLLitePersistence) AddRatchetInfo(key []byte, identity []byte, bundleI
}
// GetRatchetInfo retrieves the existing RatchetInfo for a specified bundle ID and interlocutor public key from the database
func (s *SQLLitePersistence) GetRatchetInfo(bundleID []byte, theirIdentity []byte, installationID string) (*RatchetInfo, error) {
func (s *sqlitePersistence) GetRatchetInfo(bundleID []byte, theirIdentity []byte, installationID string) (*RatchetInfo, error) {
stmt, err := s.DB.Prepare(`SELECT ratchet_info_v2.identity, ratchet_info_v2.symmetric_key, bundles.private_key, bundles.signed_pre_key, ratchet_info_v2.ephemeral_key, ratchet_info_v2.installation_id
FROM ratchet_info_v2 JOIN bundles ON bundle_id = signed_pre_key
WHERE ratchet_info_v2.identity = ? AND ratchet_info_v2.installation_id = ? AND bundle_id = ?
@ -457,7 +406,7 @@ func (s *SQLLitePersistence) GetRatchetInfo(bundleID []byte, theirIdentity []byt
}
// GetAnyRatchetInfo retrieves any existing RatchetInfo for a specified interlocutor public key from the database
func (s *SQLLitePersistence) GetAnyRatchetInfo(identity []byte, installationID string) (*RatchetInfo, error) {
func (s *sqlitePersistence) GetAnyRatchetInfo(identity []byte, installationID string) (*RatchetInfo, error) {
stmt, err := s.DB.Prepare(`SELECT symmetric_key, bundles.private_key, signed_pre_key, bundle_id, ephemeral_key
FROM ratchet_info_v2 JOIN bundles ON bundle_id = signed_pre_key
WHERE expired = 0 AND ratchet_info_v2.identity = ? AND ratchet_info_v2.installation_id = ?
@ -492,7 +441,7 @@ func (s *SQLLitePersistence) GetAnyRatchetInfo(identity []byte, installationID s
// RatchetInfoConfirmed clears the ephemeral key in the RatchetInfo
// associated with the specified bundle ID and interlocutor identity public key
func (s *SQLLitePersistence) RatchetInfoConfirmed(bundleID []byte, theirIdentity []byte, installationID string) error {
func (s *sqlitePersistence) RatchetInfoConfirmed(bundleID []byte, theirIdentity []byte, installationID string) error {
stmt, err := s.DB.Prepare(`UPDATE ratchet_info_v2
SET ephemeral_key = NULL
WHERE identity = ? AND bundle_id = ? AND installation_id = ?`)
@ -510,8 +459,18 @@ func (s *SQLLitePersistence) RatchetInfoConfirmed(bundleID []byte, theirIdentity
return err
}
type sqliteKeysStorage struct {
db *sql.DB
}
func newSQLiteKeysStorage(db *sql.DB) *sqliteKeysStorage {
return &sqliteKeysStorage{
db: db,
}
}
// Get retrieves the message key for a specified public key and message number
func (s *SQLLiteKeysStorage) Get(pubKey dr.Key, msgNum uint) (dr.Key, bool, error) {
func (s *sqliteKeysStorage) Get(pubKey dr.Key, msgNum uint) (dr.Key, bool, error) {
var keyBytes []byte
var key [32]byte
stmt, err := s.db.Prepare(`SELECT message_key
@ -537,7 +496,7 @@ func (s *SQLLiteKeysStorage) Get(pubKey dr.Key, msgNum uint) (dr.Key, bool, erro
}
// Put stores a key with the specified public key, message number and message key
func (s *SQLLiteKeysStorage) Put(sessionID []byte, pubKey dr.Key, msgNum uint, mk dr.Key, seqNum uint) error {
func (s *sqliteKeysStorage) Put(sessionID []byte, pubKey dr.Key, msgNum uint, mk dr.Key, seqNum uint) error {
stmt, err := s.db.Prepare(`INSERT INTO keys(session_id, public_key, msg_num, message_key, seq_num)
VALUES(?, ?, ?, ?, ?)`)
if err != nil {
@ -557,7 +516,7 @@ func (s *SQLLiteKeysStorage) Put(sessionID []byte, pubKey dr.Key, msgNum uint, m
}
// DeleteOldMks caps remove any key < seq_num, included
func (s *SQLLiteKeysStorage) DeleteOldMks(sessionID []byte, deleteUntil uint) error {
func (s *sqliteKeysStorage) DeleteOldMks(sessionID []byte, deleteUntil uint) error {
stmt, err := s.db.Prepare(`DELETE FROM keys
WHERE session_id = ? AND seq_num <= ?`)
if err != nil {
@ -574,7 +533,7 @@ func (s *SQLLiteKeysStorage) DeleteOldMks(sessionID []byte, deleteUntil uint) er
}
// TruncateMks caps the number of keys to maxKeysPerSession deleting them in FIFO fashion
func (s *SQLLiteKeysStorage) TruncateMks(sessionID []byte, maxKeysPerSession int) error {
func (s *sqliteKeysStorage) TruncateMks(sessionID []byte, maxKeysPerSession int) error {
stmt, err := s.db.Prepare(`DELETE FROM keys
WHERE rowid IN (SELECT rowid FROM keys WHERE session_id = ? ORDER BY seq_num DESC LIMIT ? OFFSET ?)`)
if err != nil {
@ -593,7 +552,7 @@ func (s *SQLLiteKeysStorage) TruncateMks(sessionID []byte, maxKeysPerSession int
}
// DeleteMk deletes the key with the specified public key and message key
func (s *SQLLiteKeysStorage) DeleteMk(pubKey dr.Key, msgNum uint) error {
func (s *sqliteKeysStorage) DeleteMk(pubKey dr.Key, msgNum uint) error {
stmt, err := s.db.Prepare(`DELETE FROM keys
WHERE public_key = ? AND msg_num = ?`)
if err != nil {
@ -610,7 +569,7 @@ func (s *SQLLiteKeysStorage) DeleteMk(pubKey dr.Key, msgNum uint) error {
}
// Count returns the count of keys with the specified public key
func (s *SQLLiteKeysStorage) Count(pubKey dr.Key) (uint, error) {
func (s *sqliteKeysStorage) Count(pubKey dr.Key) (uint, error) {
stmt, err := s.db.Prepare(`SELECT COUNT(1)
FROM keys
WHERE public_key = ?`)
@ -629,7 +588,7 @@ func (s *SQLLiteKeysStorage) Count(pubKey dr.Key) (uint, error) {
}
// CountAll returns the count of keys with the specified public key
func (s *SQLLiteKeysStorage) CountAll() (uint, error) {
func (s *sqliteKeysStorage) CountAll() (uint, error) {
stmt, err := s.db.Prepare(`SELECT COUNT(1)
FROM keys`)
if err != nil {
@ -647,12 +606,22 @@ func (s *SQLLiteKeysStorage) CountAll() (uint, error) {
}
// All returns nil
func (s *SQLLiteKeysStorage) All() (map[dr.Key]map[uint]dr.Key, error) {
func (s *sqliteKeysStorage) All() (map[dr.Key]map[uint]dr.Key, error) {
return nil, nil
}
type sqliteSessionStorage struct {
db *sql.DB
}
func newSQLiteSessionStorage(db *sql.DB) *sqliteSessionStorage {
return &sqliteSessionStorage{
db: db,
}
}
// Save persists the specified double ratchet state
func (s *SQLLiteSessionStorage) Save(id []byte, state *dr.State) error {
func (s *sqliteSessionStorage) Save(id []byte, state *dr.State) error {
dhr := state.DHr[:]
dhs := state.DHs
dhsPublic := dhs.PublicKey()
@ -695,7 +664,7 @@ func (s *SQLLiteSessionStorage) Save(id []byte, state *dr.State) error {
}
// Load retrieves the double ratchet state for a given ID
func (s *SQLLiteSessionStorage) Load(id []byte) (*dr.State, error) {
func (s *sqliteSessionStorage) Load(id []byte) (*dr.State, error) {
stmt, err := s.db.Prepare(`SELECT dhr, dhs_public, dhs_private, root_chain_key, send_chain_key, send_chain_n, recv_chain_key, recv_chain_n, pn, step, keys_count
FROM sessions
WHERE id = ?`)

View File

@ -0,0 +1,533 @@
package encryption
import (
"bytes"
"crypto/ecdsa"
"fmt"
"go.uber.org/zap"
"github.com/ethereum/go-ethereum/crypto"
"github.com/pkg/errors"
"github.com/status-im/status-protocol-go/encryption/multidevice"
"github.com/status-im/status-protocol-go/encryption/publisher"
"github.com/status-im/status-protocol-go/encryption/sharedsecret"
)
//go:generate protoc --go_out=. ./protocol_message.proto
const (
protocolVersion = 1
sharedSecretNegotiationVersion = 1
partitionedTopicMinVersion = 1
defaultMinVersion = 0
)
type PartitionTopicMode int
const (
PartitionTopicNoSupport PartitionTopicMode = iota
PartitionTopicV1
)
type ProtocolMessageSpec struct {
Message *ProtocolMessage
// Installations is the targeted devices
Installations []*multidevice.Installation
// SharedSecret is a shared secret established among the installations
SharedSecret []byte
// Public means that the spec contains a public wrapped message
Public bool
}
func (p *ProtocolMessageSpec) MinVersion() uint32 {
if len(p.Installations) == 0 {
return defaultMinVersion
}
version := p.Installations[0].Version
for _, installation := range p.Installations[1:] {
if installation.Version < version {
version = installation.Version
}
}
return version
}
func (p *ProtocolMessageSpec) PartitionedTopicMode() PartitionTopicMode {
if p.MinVersion() >= partitionedTopicMinVersion {
return PartitionTopicV1
}
return PartitionTopicNoSupport
}
type Protocol struct {
encryptor *encryptor
secret *sharedsecret.SharedSecret
multidevice *multidevice.Multidevice
publisher *publisher.Publisher
onAddedBundlesHandler func([]*multidevice.Installation)
onNewSharedSecretHandler func([]*sharedsecret.Secret)
onSendContactCodeHandler func(*ProtocolMessageSpec)
logger *zap.Logger
}
var (
// ErrNoPayload means that there was no payload found in the received protocol message.
ErrNoPayload = errors.New("no payload")
)
// New creates a new ProtocolService instance
func New(
dbPath string,
dbKey string,
installationID string,
addedBundlesHandler func([]*multidevice.Installation),
onNewSharedSecretHandler func([]*sharedsecret.Secret),
onSendContactCodeHandler func(*ProtocolMessageSpec),
logger *zap.Logger,
) (*Protocol, error) {
return NewWithEncryptorConfig(
dbPath,
dbKey,
installationID,
defaultEncryptorConfig(installationID, logger),
addedBundlesHandler,
onNewSharedSecretHandler,
onSendContactCodeHandler,
logger,
)
}
func NewWithEncryptorConfig(
dbPath string,
dbKey string,
installationID string,
encryptorConfig encryptorConfig,
addedBundlesHandler func([]*multidevice.Installation),
onNewSharedSecretHandler func([]*sharedsecret.Secret),
onSendContactCodeHandler func(*ProtocolMessageSpec),
logger *zap.Logger,
) (*Protocol, error) {
encryptor, err := newEncryptor(dbPath, dbKey, encryptorConfig)
if err != nil {
return nil, err
}
// DB and migrations are shared between encryption package
// and its sub-packages.
db := encryptor.persistence.DB
return &Protocol{
encryptor: encryptor,
secret: sharedsecret.New(db, logger),
multidevice: multidevice.New(db, &multidevice.Config{
MaxInstallations: 3,
ProtocolVersion: protocolVersion,
InstallationID: installationID,
}),
publisher: publisher.New(db, logger),
onAddedBundlesHandler: addedBundlesHandler,
onNewSharedSecretHandler: onNewSharedSecretHandler,
onSendContactCodeHandler: onSendContactCodeHandler,
logger: logger.With(zap.Namespace("Protocol")),
}, nil
}
func (p *Protocol) Start(myIdentity *ecdsa.PrivateKey) error {
// Propagate currently cached shared secrets.
secrets, err := p.secret.All()
if err != nil {
return errors.Wrap(err, "failed to get all secrets")
}
p.onNewSharedSecretHandler(secrets)
// Handle Publisher system messages.
publisherCh := p.publisher.Start()
go func() {
for range publisherCh {
messageSpec, err := p.buildContactCodeMessage(myIdentity)
if err != nil {
p.logger.Error("failed to build contact code message",
zap.String("site", "Start"),
zap.Error(err))
continue
}
p.onSendContactCodeHandler(messageSpec)
}
}()
return nil
}
func (p *Protocol) addBundle(myIdentityKey *ecdsa.PrivateKey, msg *ProtocolMessage, sendSingle bool) error {
logger := p.logger.With(zap.String("site", "addBundle"))
// Get a bundle
installations, err := p.multidevice.GetOurActiveInstallations(&myIdentityKey.PublicKey)
if err != nil {
return err
}
logger.Info("adding bundle to the message",
zap.Any("installations", installations),
)
bundle, err := p.encryptor.CreateBundle(myIdentityKey, installations)
if err != nil {
return err
}
if sendSingle {
// DEPRECATED: This is only for backward compatibility, remove once not
// an issue anymore
msg.Bundle = bundle
} else {
msg.Bundles = []*Bundle{bundle}
}
return nil
}
// BuildPublicMessage marshals a public chat message given the user identity private key and a payload
func (p *Protocol) BuildPublicMessage(myIdentityKey *ecdsa.PrivateKey, payload []byte) (*ProtocolMessageSpec, error) {
// Build message not encrypted
message := &ProtocolMessage{
InstallationId: p.encryptor.config.InstallationID,
PublicMessage: payload,
}
err := p.addBundle(myIdentityKey, message, false)
if err != nil {
return nil, err
}
return &ProtocolMessageSpec{Message: message, Public: true}, nil
}
// buildContactCodeMessage creates a contact code message. It's a public message
// without any data but it carries bundle information.
func (p *Protocol) buildContactCodeMessage(myIdentityKey *ecdsa.PrivateKey) (*ProtocolMessageSpec, error) {
return p.BuildPublicMessage(myIdentityKey, nil)
}
// BuildDirectMessage returns a 1:1 chat message and optionally a negotiated topic given the user identity private key, the recipient's public key, and a payload
func (p *Protocol) BuildDirectMessage(myIdentityKey *ecdsa.PrivateKey, publicKey *ecdsa.PublicKey, payload []byte) (*ProtocolMessageSpec, error) {
logger := p.logger.With(
zap.String("site", "BuildDirectMessage"),
zap.Binary("public-key", crypto.FromECDSAPub(publicKey)),
)
logger.Debug("building direct message")
// Get recipients installations.
activeInstallations, err := p.multidevice.GetActiveInstallations(publicKey)
if err != nil {
return nil, err
}
// Encrypt payload
directMessage, installations, err := p.encryptor.EncryptPayload(publicKey, myIdentityKey, activeInstallations, payload)
if err != nil {
return nil, err
}
// Build message
message := &ProtocolMessage{
InstallationId: p.encryptor.config.InstallationID,
DirectMessage: directMessage,
}
err = p.addBundle(myIdentityKey, message, true)
if err != nil {
return nil, err
}
// Check who we are sending the message to, and see if we have a shared secret
// across devices
var installationIDs []string
for installationID := range message.GetDirectMessage() {
if installationID != noInstallationID {
installationIDs = append(installationIDs, installationID)
}
}
sharedSecret, agreed, err := p.secret.Agreed(myIdentityKey, p.encryptor.config.InstallationID, publicKey, installationIDs)
if err != nil {
return nil, err
}
logger.Debug("shared secret agreement",
zap.Bool("has-shared-secret", sharedSecret != nil),
zap.Bool("agreed", agreed))
// Call handler
if sharedSecret != nil {
p.onNewSharedSecretHandler([]*sharedsecret.Secret{sharedSecret})
}
spec := &ProtocolMessageSpec{
Message: message,
Installations: installations,
}
if agreed {
spec.SharedSecret = sharedSecret.Key
}
return spec, nil
}
// BuildDHMessage builds a message with DH encryption so that it can be decrypted by any other device.
func (p *Protocol) BuildDHMessage(myIdentityKey *ecdsa.PrivateKey, destination *ecdsa.PublicKey, payload []byte) (*ProtocolMessageSpec, error) {
// Encrypt payload
encryptionResponse, err := p.encryptor.EncryptPayloadWithDH(destination, payload)
if err != nil {
return nil, err
}
// Build message
message := &ProtocolMessage{
InstallationId: p.encryptor.config.InstallationID,
DirectMessage: encryptionResponse,
}
err = p.addBundle(myIdentityKey, message, true)
if err != nil {
return nil, err
}
return &ProtocolMessageSpec{Message: message}, nil
}
// ProcessPublicBundle processes a received X3DH bundle.
func (p *Protocol) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey, bundle *Bundle) ([]*multidevice.Installation, error) {
logger := p.logger.With(zap.String("site", "ProcessPublicBundle"))
logger.Debug("processing public bundle")
if err := p.encryptor.ProcessPublicBundle(myIdentityKey, bundle); err != nil {
return nil, err
}
installations, enabled, err := p.recoverInstallationsFromBundle(myIdentityKey, bundle)
if err != nil {
return nil, err
}
logger.Debug("recovered installations",
zap.Int("installations", len(installations)),
zap.Bool("enabled", enabled))
// TODO(adam): why do we add installations using identity obtained from GetIdentity()
// instead of the output of crypto.CompressPubkey()? I tried the second option
// and the unit tests TestTopic and TestMaxDevices fail.
identityFromBundle := bundle.GetIdentity()
theirIdentity, err := ExtractIdentity(bundle)
if err != nil {
logger.Panic("unrecoverable error extracting identity", zap.Error(err))
}
compressedIdentity := crypto.CompressPubkey(theirIdentity)
if !bytes.Equal(identityFromBundle, compressedIdentity) {
logger.Panic("identity from bundle and compressed are not equal")
}
return p.multidevice.AddInstallations(bundle.GetIdentity(), bundle.GetTimestamp(), installations, enabled)
}
// recoverInstallationsFromBundle extracts installations from the bundle.
// It returns extracted installations and true if the installations
// are ours, i.e. the bundle was created by our identity key.
func (p *Protocol) recoverInstallationsFromBundle(myIdentityKey *ecdsa.PrivateKey, bundle *Bundle) ([]*multidevice.Installation, bool, error) {
var installations []*multidevice.Installation
theirIdentity, err := ExtractIdentity(bundle)
if err != nil {
return nil, false, err
}
logger := p.logger.With(zap.String("site", "recoverInstallationsFromBundle"))
myIdentityStr := fmt.Sprintf("0x%x", crypto.FromECDSAPub(&myIdentityKey.PublicKey))
theirIdentityStr := fmt.Sprintf("0x%x", crypto.FromECDSAPub(theirIdentity))
// Any device from other peers will be considered enabled, ours needs to
// be explicitly enabled.
enabled := theirIdentityStr != myIdentityStr
signedPreKeys := bundle.GetSignedPreKeys()
for installationID, signedPreKey := range signedPreKeys {
logger.Info("recovered installation", zap.String("installation-id", installationID))
if installationID != p.multidevice.InstallationID() {
installations = append(installations, &multidevice.Installation{
Identity: theirIdentityStr,
ID: installationID,
Version: signedPreKey.GetProtocolVersion(),
})
}
}
return installations, enabled, nil
}
// GetBundle retrieves or creates a X3DH bundle, given a private identity key.
func (p *Protocol) GetBundle(myIdentityKey *ecdsa.PrivateKey) (*Bundle, error) {
installations, err := p.multidevice.GetOurActiveInstallations(&myIdentityKey.PublicKey)
if err != nil {
return nil, err
}
return p.encryptor.CreateBundle(myIdentityKey, installations)
}
// EnableInstallation enables an installation for multi-device sync.
func (p *Protocol) EnableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error {
return p.multidevice.EnableInstallation(myIdentityKey, installationID)
}
// DisableInstallation disables an installation for multi-device sync.
func (p *Protocol) DisableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error {
return p.multidevice.DisableInstallation(myIdentityKey, installationID)
}
// GetOurInstallations returns all the installations available given an identity
func (p *Protocol) GetOurInstallations(myIdentityKey *ecdsa.PublicKey) ([]*multidevice.Installation, error) {
return p.multidevice.GetOurInstallations(myIdentityKey)
}
// SetInstallationMetadata sets the metadata for our own installation
func (p *Protocol) SetInstallationMetadata(myIdentityKey *ecdsa.PublicKey, installationID string, data *multidevice.InstallationMetadata) error {
return p.multidevice.SetInstallationMetadata(myIdentityKey, installationID, data)
}
// GetPublicBundle retrieves a public bundle given an identity
func (p *Protocol) GetPublicBundle(theirIdentityKey *ecdsa.PublicKey) (*Bundle, error) {
installations, err := p.multidevice.GetActiveInstallations(theirIdentityKey)
if err != nil {
return nil, err
}
return p.encryptor.GetPublicBundle(theirIdentityKey, installations)
}
// ConfirmMessageProcessed confirms and deletes message keys for the given messages
func (p *Protocol) ConfirmMessageProcessed(messageID []byte) error {
return p.encryptor.ConfirmMessageProcessed(messageID)
}
// HandleMessage unmarshals a message and processes it, decrypting it if it is a 1:1 message.
func (p *Protocol) HandleMessage(
myIdentityKey *ecdsa.PrivateKey,
theirPublicKey *ecdsa.PublicKey,
protocolMessage *ProtocolMessage,
messageID []byte,
) ([]byte, error) {
logger := p.logger.With(zap.String("site", "HandleMessage"))
logger.Debug("received a protocol message", zap.Binary("sender-public-key", crypto.FromECDSAPub(theirPublicKey)))
if p.encryptor == nil {
return nil, errors.New("encryption service not initialized")
}
// Process bundle, deprecated, here for backward compatibility
if bundle := protocolMessage.GetBundle(); bundle != nil {
// Should we stop processing if the bundle cannot be verified?
addedBundles, err := p.ProcessPublicBundle(myIdentityKey, bundle)
if err != nil {
return nil, err
}
p.onAddedBundlesHandler(addedBundles)
}
// Process bundles
for _, bundle := range protocolMessage.GetBundles() {
// Should we stop processing if the bundle cannot be verified?
addedBundles, err := p.ProcessPublicBundle(myIdentityKey, bundle)
if err != nil {
return nil, err
}
p.onAddedBundlesHandler(addedBundles)
}
// Check if it's a public message
if publicMessage := protocolMessage.GetPublicMessage(); publicMessage != nil {
logger.Debug("received a public message in direct message")
// Nothing to do, as already in cleartext
return publicMessage, nil
}
// Decrypt message
if directMessage := protocolMessage.GetDirectMessage(); directMessage != nil {
logger.Debug("processing direct message")
message, err := p.encryptor.DecryptPayload(
myIdentityKey,
theirPublicKey,
protocolMessage.GetInstallationId(),
directMessage,
messageID,
)
if err != nil {
return nil, err
}
// Handle protocol negotiation for compatible clients
bundles := append(protocolMessage.GetBundles(), protocolMessage.GetBundle())
version := getProtocolVersion(bundles, protocolMessage.GetInstallationId())
logger.Debug("direct message version", zap.Uint32("version", version))
if version >= sharedSecretNegotiationVersion {
logger.Debug("negotiating shared secret",
zap.Binary("public-key", crypto.FromECDSAPub(theirPublicKey)))
sharedSecret, err := p.secret.Generate(myIdentityKey, theirPublicKey, protocolMessage.GetInstallationId())
if err != nil {
return nil, err
}
p.onNewSharedSecretHandler([]*sharedsecret.Secret{sharedSecret})
}
return message, nil
}
// Return error
return nil, ErrNoPayload
}
func (p *Protocol) ShouldAdvertiseBundle(publicKey *ecdsa.PublicKey, time int64) (bool, error) {
return p.publisher.ShouldAdvertiseBundle(publicKey, time)
}
func (p *Protocol) ConfirmBundleAdvertisement(publicKey *ecdsa.PublicKey, time int64) {
p.publisher.SetLastAck(publicKey, time)
}
func (p *Protocol) BuildBundleAdvertiseMessage(myIdentityKey *ecdsa.PrivateKey, publicKey *ecdsa.PublicKey) (*ProtocolMessageSpec, error) {
return p.BuildDHMessage(myIdentityKey, publicKey, nil)
}
func getProtocolVersion(bundles []*Bundle, installationID string) uint32 {
if installationID == "" {
return defaultMinVersion
}
for _, bundle := range bundles {
if bundle != nil {
signedPreKeys := bundle.GetSignedPreKeys()
if signedPreKeys == nil {
continue
}
signedPreKey := signedPreKeys[installationID]
if signedPreKey == nil {
return defaultMinVersion
}
return signedPreKey.GetProtocolVersion()
}
}
return defaultMinVersion
}

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: encryption.proto
// source: protocol_message.proto
package protobuf
package encryption
import (
fmt "fmt"
@ -18,7 +18,7 @@ var _ = math.Inf
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
type SignedPreKey struct {
SignedPreKey []byte `protobuf:"bytes,1,opt,name=signed_pre_key,json=signedPreKey,proto3" json:"signed_pre_key,omitempty"`
@ -33,7 +33,7 @@ func (m *SignedPreKey) Reset() { *m = SignedPreKey{} }
func (m *SignedPreKey) String() string { return proto.CompactTextString(m) }
func (*SignedPreKey) ProtoMessage() {}
func (*SignedPreKey) Descriptor() ([]byte, []int) {
return fileDescriptor_8293a649ce9418c6, []int{0}
return fileDescriptor_4e37b52004a72e16, []int{0}
}
func (m *SignedPreKey) XXX_Unmarshal(b []byte) error {
@ -94,7 +94,7 @@ func (m *Bundle) Reset() { *m = Bundle{} }
func (m *Bundle) String() string { return proto.CompactTextString(m) }
func (*Bundle) ProtoMessage() {}
func (*Bundle) Descriptor() ([]byte, []int) {
return fileDescriptor_8293a649ce9418c6, []int{1}
return fileDescriptor_4e37b52004a72e16, []int{1}
}
func (m *Bundle) XXX_Unmarshal(b []byte) error {
@ -157,7 +157,7 @@ func (m *BundleContainer) Reset() { *m = BundleContainer{} }
func (m *BundleContainer) String() string { return proto.CompactTextString(m) }
func (*BundleContainer) ProtoMessage() {}
func (*BundleContainer) Descriptor() ([]byte, []int) {
return fileDescriptor_8293a649ce9418c6, []int{2}
return fileDescriptor_4e37b52004a72e16, []int{2}
}
func (m *BundleContainer) XXX_Unmarshal(b []byte) error {
@ -210,7 +210,7 @@ func (m *DRHeader) Reset() { *m = DRHeader{} }
func (m *DRHeader) String() string { return proto.CompactTextString(m) }
func (*DRHeader) ProtoMessage() {}
func (*DRHeader) Descriptor() ([]byte, []int) {
return fileDescriptor_8293a649ce9418c6, []int{3}
return fileDescriptor_4e37b52004a72e16, []int{3}
}
func (m *DRHeader) XXX_Unmarshal(b []byte) error {
@ -271,7 +271,7 @@ func (m *DHHeader) Reset() { *m = DHHeader{} }
func (m *DHHeader) String() string { return proto.CompactTextString(m) }
func (*DHHeader) ProtoMessage() {}
func (*DHHeader) Descriptor() ([]byte, []int) {
return fileDescriptor_8293a649ce9418c6, []int{4}
return fileDescriptor_4e37b52004a72e16, []int{4}
}
func (m *DHHeader) XXX_Unmarshal(b []byte) error {
@ -313,7 +313,7 @@ func (m *X3DHHeader) Reset() { *m = X3DHHeader{} }
func (m *X3DHHeader) String() string { return proto.CompactTextString(m) }
func (*X3DHHeader) ProtoMessage() {}
func (*X3DHHeader) Descriptor() ([]byte, []int) {
return fileDescriptor_8293a649ce9418c6, []int{5}
return fileDescriptor_4e37b52004a72e16, []int{5}
}
func (m *X3DHHeader) XXX_Unmarshal(b []byte) error {
@ -364,7 +364,7 @@ func (m *DirectMessageProtocol) Reset() { *m = DirectMessageProtocol{} }
func (m *DirectMessageProtocol) String() string { return proto.CompactTextString(m) }
func (*DirectMessageProtocol) ProtoMessage() {}
func (*DirectMessageProtocol) Descriptor() ([]byte, []int) {
return fileDescriptor_8293a649ce9418c6, []int{6}
return fileDescriptor_4e37b52004a72e16, []int{6}
}
func (m *DirectMessageProtocol) XXX_Unmarshal(b []byte) error {
@ -434,7 +434,7 @@ func (m *ProtocolMessage) Reset() { *m = ProtocolMessage{} }
func (m *ProtocolMessage) String() string { return proto.CompactTextString(m) }
func (*ProtocolMessage) ProtoMessage() {}
func (*ProtocolMessage) Descriptor() ([]byte, []int) {
return fileDescriptor_8293a649ce9418c6, []int{7}
return fileDescriptor_4e37b52004a72e16, []int{7}
}
func (m *ProtocolMessage) XXX_Unmarshal(b []byte) error {
@ -491,56 +491,56 @@ func (m *ProtocolMessage) GetPublicMessage() []byte {
}
func init() {
proto.RegisterType((*SignedPreKey)(nil), "protobuf.SignedPreKey")
proto.RegisterType((*Bundle)(nil), "protobuf.Bundle")
proto.RegisterMapType((map[string]*SignedPreKey)(nil), "protobuf.Bundle.SignedPreKeysEntry")
proto.RegisterType((*BundleContainer)(nil), "protobuf.BundleContainer")
proto.RegisterType((*DRHeader)(nil), "protobuf.DRHeader")
proto.RegisterType((*DHHeader)(nil), "protobuf.DHHeader")
proto.RegisterType((*X3DHHeader)(nil), "protobuf.X3DHHeader")
proto.RegisterType((*DirectMessageProtocol)(nil), "protobuf.DirectMessageProtocol")
proto.RegisterType((*ProtocolMessage)(nil), "protobuf.ProtocolMessage")
proto.RegisterMapType((map[string]*DirectMessageProtocol)(nil), "protobuf.ProtocolMessage.DirectMessageEntry")
proto.RegisterType((*SignedPreKey)(nil), "encryption.SignedPreKey")
proto.RegisterType((*Bundle)(nil), "encryption.Bundle")
proto.RegisterMapType((map[string]*SignedPreKey)(nil), "encryption.Bundle.SignedPreKeysEntry")
proto.RegisterType((*BundleContainer)(nil), "encryption.BundleContainer")
proto.RegisterType((*DRHeader)(nil), "encryption.DRHeader")
proto.RegisterType((*DHHeader)(nil), "encryption.DHHeader")
proto.RegisterType((*X3DHHeader)(nil), "encryption.X3DHHeader")
proto.RegisterType((*DirectMessageProtocol)(nil), "encryption.DirectMessageProtocol")
proto.RegisterType((*ProtocolMessage)(nil), "encryption.ProtocolMessage")
proto.RegisterMapType((map[string]*DirectMessageProtocol)(nil), "encryption.ProtocolMessage.DirectMessageEntry")
}
func init() { proto.RegisterFile("encryption.proto", fileDescriptor_8293a649ce9418c6) }
func init() { proto.RegisterFile("protocol_message.proto", fileDescriptor_4e37b52004a72e16) }
var fileDescriptor_8293a649ce9418c6 = []byte{
// 566 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x53, 0xdd, 0x6a, 0xdb, 0x4c,
0x10, 0x45, 0x52, 0xe2, 0x9f, 0xb1, 0xfc, 0xc3, 0x7e, 0x5f, 0x83, 0x30, 0x81, 0x1a, 0xb5, 0xa5,
0x6e, 0x09, 0x2e, 0xd8, 0x0d, 0x94, 0x5e, 0xb6, 0x2e, 0xb8, 0x09, 0x85, 0xb0, 0x81, 0x92, 0x3b,
0xb1, 0xb6, 0x36, 0xe9, 0x52, 0x79, 0x25, 0x76, 0xd7, 0x06, 0x3d, 0x41, 0xdf, 0xad, 0x2f, 0xd3,
0x57, 0x28, 0x5a, 0x69, 0xad, 0xb5, 0x9d, 0x5c, 0xf4, 0xca, 0x9e, 0xb3, 0x73, 0xce, 0xcc, 0x9c,
0xd1, 0xc0, 0x80, 0xf2, 0x95, 0xc8, 0x33, 0xc5, 0x52, 0x3e, 0xc9, 0x44, 0xaa, 0x52, 0xd4, 0xd2,
0x3f, 0xcb, 0xcd, 0x7d, 0x98, 0x83, 0x7f, 0xcb, 0x1e, 0x38, 0x8d, 0x6f, 0x04, 0xbd, 0xa6, 0x39,
0x7a, 0x09, 0x3d, 0xa9, 0xe3, 0x28, 0x13, 0x34, 0xfa, 0x49, 0xf3, 0xc0, 0x19, 0x39, 0x63, 0x1f,
0xfb, 0xd2, 0xce, 0x0a, 0xa0, 0xb9, 0xa5, 0x42, 0xb2, 0x94, 0x07, 0xee, 0xc8, 0x19, 0x77, 0xb1,
0x09, 0xd1, 0x1b, 0x18, 0x68, 0xed, 0x55, 0x9a, 0x44, 0x26, 0xc5, 0xd3, 0x29, 0x7d, 0x83, 0x7f,
0x2f, 0xe1, 0xf0, 0x97, 0x0b, 0x8d, 0x4f, 0x1b, 0x1e, 0x27, 0x14, 0x0d, 0xa1, 0xc5, 0x62, 0xca,
0x15, 0x53, 0xa6, 0xde, 0x2e, 0x46, 0xd7, 0xd0, 0xdf, 0xef, 0x48, 0x06, 0xee, 0xc8, 0x1b, 0x77,
0xa6, 0x2f, 0x26, 0x66, 0x8a, 0x49, 0x29, 0x33, 0xb1, 0x27, 0x91, 0x5f, 0xb8, 0x12, 0x39, 0xee,
0xda, 0x7d, 0x4b, 0x74, 0x0e, 0xed, 0x02, 0x20, 0x6a, 0x23, 0x68, 0x70, 0xa2, 0x2b, 0xd5, 0x40,
0xf1, 0xaa, 0xd8, 0x9a, 0x4a, 0x45, 0xd6, 0x59, 0x70, 0x3a, 0x72, 0xc6, 0x1e, 0xae, 0x81, 0xe1,
0x1d, 0xa0, 0xe3, 0x02, 0x68, 0x00, 0x9e, 0x71, 0xa9, 0x8d, 0x8b, 0xbf, 0xe8, 0x02, 0x4e, 0xb7,
0x24, 0xd9, 0x50, 0x6d, 0x4d, 0x67, 0x7a, 0x56, 0xb7, 0x69, 0xd3, 0x71, 0x99, 0xf4, 0xd1, 0xfd,
0xe0, 0x84, 0x5b, 0xe8, 0x97, 0x13, 0x7c, 0x4e, 0xb9, 0x22, 0x8c, 0x53, 0x81, 0xc6, 0xd0, 0x58,
0x6a, 0x48, 0x2b, 0x77, 0xa6, 0x83, 0xc3, 0x61, 0x71, 0xf5, 0x8e, 0x66, 0x70, 0x96, 0x09, 0xb6,
0x25, 0x8a, 0x46, 0x07, 0x9b, 0x73, 0xf5, 0x7c, 0xff, 0x55, 0xaf, 0x76, 0xf1, 0xab, 0x93, 0x96,
0x37, 0x38, 0x09, 0xaf, 0xa0, 0x35, 0xc7, 0x0b, 0x4a, 0x62, 0x2a, 0xec, 0x39, 0xfc, 0x72, 0x0e,
0x1f, 0x1c, 0xb3, 0x5e, 0x87, 0xa3, 0x1e, 0xb8, 0x99, 0x59, 0xa5, 0x9b, 0xe9, 0x98, 0xc5, 0x95,
0x85, 0x2e, 0x8b, 0xc3, 0x73, 0x68, 0xcd, 0x17, 0x4f, 0x69, 0x85, 0xef, 0x01, 0xee, 0x66, 0x4f,
0xbf, 0x1f, 0xaa, 0x55, 0xfd, 0xfd, 0x76, 0xe0, 0xd9, 0x9c, 0x09, 0xba, 0x52, 0xdf, 0xa8, 0x94,
0xe4, 0x81, 0xde, 0x54, 0x9f, 0x10, 0xba, 0x84, 0x4e, 0xa1, 0x17, 0xfd, 0xd0, 0x82, 0x95, 0x47,
0xff, 0xd7, 0x1e, 0xd5, 0xc5, 0xb0, 0x5d, 0xf8, 0x1d, 0xb4, 0xe7, 0xd8, 0x90, 0xca, 0xf5, 0xa0,
0x9a, 0x64, 0xbc, 0xc0, 0xb5, 0x2b, 0x05, 0x61, 0x57, 0x85, 0x1e, 0x11, 0x16, 0x3b, 0x82, 0xa9,
0x10, 0x40, 0x33, 0x23, 0x79, 0x92, 0x92, 0x58, 0x7b, 0xe5, 0x63, 0x13, 0x86, 0x7f, 0x5c, 0xe8,
0x9b, 0xfe, 0xab, 0x71, 0xfe, 0x61, 0xcb, 0xaf, 0xa1, 0xcf, 0xb8, 0x54, 0x24, 0x49, 0x48, 0x71,
0xc7, 0x11, 0x8b, 0x75, 0xff, 0x6d, 0xdc, 0xb3, 0xe1, 0xaf, 0x31, 0x7a, 0x0b, 0xcd, 0x92, 0x22,
0x03, 0x4f, 0x9f, 0xc9, 0xb1, 0xa6, 0x49, 0x40, 0xb7, 0xd0, 0x8b, 0xb5, 0xbd, 0xd1, 0xba, 0x6c,
0x28, 0xa0, 0x9a, 0x72, 0x51, 0x53, 0x0e, 0x3a, 0x9e, 0xec, 0xad, 0xa3, 0x3a, 0xb1, 0xd8, 0xc6,
0xd0, 0x2b, 0xe8, 0x65, 0x9b, 0x65, 0xc2, 0x56, 0x3b, 0xd1, 0x7b, 0x6d, 0x44, 0xb7, 0x44, 0xab,
0xb4, 0x21, 0x01, 0x74, 0xac, 0xf5, 0xc8, 0x35, 0x5d, 0xee, 0x5f, 0xd3, 0x73, 0xcb, 0xfd, 0xc7,
0xbe, 0x0c, 0xeb, 0xac, 0x96, 0x0d, 0x9d, 0x3a, 0xfb, 0x1b, 0x00, 0x00, 0xff, 0xff, 0xcd, 0x2d,
0x0e, 0xc8, 0x00, 0x05, 0x00, 0x00,
var fileDescriptor_4e37b52004a72e16 = []byte{
// 565 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x54, 0x5d, 0x8b, 0xd3, 0x40,
0x14, 0x25, 0xc9, 0xee, 0xb6, 0xbd, 0x4d, 0x3f, 0x18, 0x75, 0x09, 0x65, 0x1f, 0x6a, 0x70, 0xb1,
0x8a, 0x04, 0x6c, 0x85, 0x15, 0x1f, 0xb5, 0x42, 0x5d, 0x59, 0x58, 0x46, 0x14, 0xf1, 0x25, 0x4c,
0x3b, 0xe3, 0x3a, 0x98, 0x4e, 0x42, 0x66, 0x5a, 0xcc, 0x6f, 0xf0, 0xcf, 0xf9, 0x6f, 0x7c, 0x95,
0x4c, 0x32, 0xed, 0xf4, 0x63, 0x1f, 0x7c, 0xcb, 0x9c, 0xb9, 0xf7, 0xdc, 0x7b, 0xce, 0x9d, 0x1b,
0x38, 0xcf, 0xf2, 0x54, 0xa5, 0x8b, 0x34, 0x89, 0x97, 0x4c, 0x4a, 0x72, 0xc7, 0x22, 0x0d, 0x20,
0x60, 0x62, 0x91, 0x17, 0x99, 0xe2, 0xa9, 0x08, 0x0b, 0xf0, 0x3f, 0xf1, 0x3b, 0xc1, 0xe8, 0x6d,
0xce, 0x3e, 0xb2, 0x02, 0x3d, 0x81, 0xae, 0xd4, 0xe7, 0x38, 0xcb, 0x59, 0xfc, 0x93, 0x15, 0x81,
0x33, 0x74, 0x46, 0x3e, 0xf6, 0xa5, 0x1d, 0x15, 0x40, 0x63, 0xcd, 0x72, 0xc9, 0x53, 0x11, 0xb8,
0x43, 0x67, 0xd4, 0xc1, 0xe6, 0x88, 0x9e, 0x41, 0x7f, 0x53, 0xd5, 0x84, 0x78, 0x3a, 0xa4, 0x67,
0xf0, 0x2f, 0x15, 0x1c, 0xfe, 0x76, 0xe1, 0xec, 0xed, 0x4a, 0xd0, 0x84, 0xa1, 0x01, 0x34, 0x39,
0x65, 0x42, 0x71, 0x65, 0xea, 0x6d, 0xce, 0xe8, 0x06, 0x7a, 0xbb, 0x1d, 0xc9, 0xc0, 0x1d, 0x7a,
0xa3, 0xf6, 0xf8, 0x32, 0xda, 0xea, 0x88, 0x2a, 0xa2, 0xc8, 0xd6, 0x22, 0xdf, 0x0b, 0x95, 0x17,
0xb8, 0x63, 0x77, 0x2e, 0xd1, 0x05, 0xb4, 0x4a, 0x80, 0xa8, 0x55, 0xce, 0x82, 0x13, 0x5d, 0x6b,
0x0b, 0x94, 0xb7, 0x8a, 0x2f, 0x99, 0x54, 0x64, 0x99, 0x05, 0xa7, 0x43, 0x67, 0xe4, 0xe1, 0x2d,
0x30, 0xf8, 0x06, 0xe8, 0xb0, 0x00, 0xea, 0x83, 0x67, 0x7c, 0x6a, 0xe1, 0xf2, 0x13, 0x45, 0x70,
0xba, 0x26, 0xc9, 0x8a, 0x69, 0x73, 0xda, 0xe3, 0xc0, 0x6e, 0xd4, 0x26, 0xc0, 0x55, 0xd8, 0x1b,
0xf7, 0xb5, 0x13, 0xfe, 0x82, 0x5e, 0xa5, 0xe1, 0x5d, 0x2a, 0x14, 0xe1, 0x82, 0xe5, 0xe8, 0x39,
0x9c, 0xcd, 0x35, 0xa4, 0xb9, 0xdb, 0x63, 0x74, 0x28, 0x18, 0xd7, 0x11, 0x68, 0x52, 0x4e, 0x9b,
0xaf, 0x89, 0x62, 0xf1, 0xde, 0xfc, 0x5c, 0xad, 0xf1, 0x41, 0x7d, 0x6b, 0x97, 0xbf, 0x3e, 0x69,
0x7a, 0xfd, 0x93, 0xf0, 0x1a, 0x9a, 0x53, 0x3c, 0x63, 0x84, 0xb2, 0xdc, 0xd6, 0xe2, 0x57, 0x5a,
0x7c, 0x70, 0xcc, 0x90, 0x1d, 0x81, 0xba, 0xe0, 0x66, 0x66, 0xa0, 0x6e, 0xa6, 0xcf, 0x9c, 0xd6,
0x36, 0xba, 0x9c, 0x86, 0x17, 0xd0, 0x9c, 0xce, 0xee, 0xe3, 0x0a, 0x5f, 0x01, 0x7c, 0x9d, 0xdc,
0x7f, 0xbf, 0xcf, 0x56, 0xf7, 0xf7, 0xc7, 0x81, 0x47, 0x53, 0x9e, 0xb3, 0x85, 0xba, 0xa9, 0x9e,
0xf1, 0x6d, 0xfd, 0x90, 0xd0, 0x15, 0xb4, 0x4b, 0xbe, 0xf8, 0x87, 0x26, 0xac, 0x5d, 0x3a, 0xb7,
0x5d, 0xda, 0x96, 0xc3, 0x76, 0xe9, 0x97, 0xd0, 0x9a, 0x62, 0x93, 0x56, 0x0d, 0xe9, 0xa1, 0x9d,
0x66, 0xfc, 0xc0, 0x5b, 0x67, 0xca, 0x94, 0x4d, 0x25, 0x76, 0x24, 0x65, 0xb6, 0x49, 0x31, 0x55,
0x02, 0x68, 0x64, 0xa4, 0x48, 0x52, 0x42, 0xb5, 0x63, 0x3e, 0x36, 0xc7, 0xf0, 0xaf, 0x0b, 0x3d,
0xa3, 0xa2, 0x16, 0xf5, 0x5f, 0xd3, 0x7e, 0x0a, 0x3d, 0x2e, 0xa4, 0x22, 0x49, 0x42, 0xca, 0xeb,
0x98, 0x53, 0xad, 0xa2, 0x85, 0xbb, 0x36, 0xfc, 0x81, 0xa2, 0x17, 0xd0, 0xa8, 0x52, 0x64, 0xe0,
0xe9, 0xa5, 0x39, 0xc6, 0x6a, 0x42, 0xd0, 0x67, 0xe8, 0x52, 0x6d, 0xb4, 0xf9, 0x61, 0x04, 0x4c,
0x27, 0x45, 0x76, 0xd2, 0x5e, 0xdf, 0xd1, 0xce, 0x68, 0xea, 0x95, 0xa3, 0x36, 0x86, 0x2e, 0xa1,
0x9b, 0xad, 0xe6, 0x09, 0x5f, 0x6c, 0x68, 0xbf, 0x6b, 0x3b, 0x3a, 0x15, 0x5a, 0x87, 0x0d, 0x16,
0x80, 0x0e, 0xb9, 0x8e, 0x6c, 0xd7, 0xd5, 0xee, 0x76, 0x3d, 0xde, 0x99, 0xc2, 0xb1, 0x77, 0x62,
0xad, 0xd9, 0xfc, 0x4c, 0xff, 0x85, 0x26, 0xff, 0x02, 0x00, 0x00, 0xff, 0xff, 0xb6, 0xd6, 0x74,
0x9a, 0x1c, 0x05, 0x00, 0x00,
}

Some files were not shown because too many files have changed in this diff Show More