integrate status-protocol-go
This commit is contained in:
parent
664488faf9
commit
e93d994460
|
@ -42,4 +42,3 @@ exclude_patterns:
|
|||
- static/
|
||||
- t/
|
||||
- mailserver/migrations
|
||||
- messaging/chat/migrations
|
||||
|
|
|
@ -47,7 +47,6 @@ linters:
|
|||
- golint
|
||||
- govet
|
||||
- ineffassign
|
||||
- interfacer
|
||||
- megacheck
|
||||
- misspell
|
||||
- structcheck
|
||||
|
|
1
Makefile
1
Makefile
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
16
go.mod
|
@ -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
19
go.sum
|
@ -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=
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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`)
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
|
@ -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")
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
// -----
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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[:]
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
DROP TABLE sessions;
|
||||
DROP TABLE bundles;
|
||||
DROP TABLE keys;
|
||||
DROP TABLE ratchet_info;
|
|
@ -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)
|
||||
);
|
|
@ -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)
|
||||
);
|
|
@ -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)
|
||||
);
|
|
@ -1,3 +0,0 @@
|
|||
ALTER TABLE keys DROP COLUMN session_id;
|
||||
ALTER TABLE sessions DROP COLUMN keys_count;
|
||||
ALTER TABLE bundles DROP COLUMN version;
|
|
@ -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;
|
|
@ -1 +0,0 @@
|
|||
DROP TABLE installations;
|
|
@ -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
|
||||
);
|
|
@ -1,2 +0,0 @@
|
|||
DROP TABLE secret_installation_ids;
|
||||
DROP TABLE secrets;
|
|
@ -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)
|
||||
);
|
|
@ -1 +0,0 @@
|
|||
ALTER TABLE installations ADD version INTEGER DEFAULT 0;
|
|
@ -1 +0,0 @@
|
|||
DROP TABLE contact_code_config;
|
|
@ -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);
|
|
@ -1 +0,0 @@
|
|||
DROP TABLE whisper_keys;
|
|
@ -1,4 +0,0 @@
|
|||
CREATE TABLE whisper_keys (
|
||||
chat_id TEXT PRIMARY KEY ON CONFLICT IGNORE,
|
||||
key BLOB NOT NULL
|
||||
) WITHOUT ROWID;
|
|
@ -1 +0,0 @@
|
|||
DROP TABLE installations_metadata;
|
|
@ -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
|
||||
);
|
|
@ -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 .
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
*.test
|
||||
|
||||
*.out
|
||||
*.txt
|
|
@ -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)
|
|
@ -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)
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
}
|
|
@ -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 ./...
|
|
@ -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
|
||||
}
|
|
@ -1,4 +1,8 @@
|
|||
language: go
|
||||
go:
|
||||
- 1.3
|
||||
- 1.4
|
||||
- 1.9
|
||||
- "1.10"
|
||||
- tip
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: tip
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
module github.com/rs/cors
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
language: go
|
||||
go:
|
||||
- 1.5
|
||||
- tip
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: tip
|
|
@ -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.
|
|
@ -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).
|
|
@ -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))
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
run:
|
||||
modules-download-mode: vendor
|
||||
deadline: 5m
|
|
@ -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
|
|
@ -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.
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
# status-protocol-go
|
||||
|
||||
This is the Status Protocol implementation in Go.
|
||||
|
||||
TBD
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package statusproto
|
||||
|
||||
import "crypto/ecdsa"
|
||||
|
||||
type Chat interface {
|
||||
ID() string
|
||||
PublicName() string
|
||||
PublicKey() *ecdsa.PublicKey
|
||||
}
|
190
vendor/github.com/status-im/status-protocol-go/crypto/ethereum_crypto.go
generated
vendored
Normal file
190
vendor/github.com/status-im/status-protocol-go/crypto/ethereum_crypto.go
generated
vendored
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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, "/")...)...)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
|
@ -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
|
|
@ -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 = ?`)
|
533
vendor/github.com/status-im/status-protocol-go/encryption/protocol.go
generated
vendored
Normal file
533
vendor/github.com/status-im/status-protocol-go/encryption/protocol.go
generated
vendored
Normal 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
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue