Add enabling/disabling of installations (#1264)
This commit adds a list new table, installations, which is used to keep track of which installation are active for a given identity key. In general, we limit the number of installation that we keep synchronized to 5, to avoid excessive usage of resources. Any installation coming from our own identity, will have to be manually enabled, otherwise we trust the other peer has correctly paired their devices. We use a timestamp to decide which installations to keep synchronized as a logical clock would have make the creation of the bundle more complicated, but this can always be converted to a logical clock at later stages without breaking compatibility.
This commit is contained in:
parent
3fe5b25ff2
commit
1f6cccd0fc
|
@ -517,12 +517,6 @@ func (b *StatusBackend) ExtractIdentityFromContactCode(contactCode string) (stri
|
|||
return chat.ExtractIdentity(bundle)
|
||||
}
|
||||
|
||||
// DEPRECATED
|
||||
// VerifyGroupMembershipSignatures verifies that the signatures are valid
|
||||
func (b *StatusBackend) VerifyGroupMembershipSignatures(signaturePairs [][3]string) error {
|
||||
return crypto.VerifySignatures(signaturePairs)
|
||||
}
|
||||
|
||||
// ExtractGroupMembershipSignatures extract signatures from tuples of content/signature
|
||||
func (b *StatusBackend) ExtractGroupMembershipSignatures(signaturePairs [][2]string) ([]string, error) {
|
||||
return crypto.ExtractSignatures(signaturePairs)
|
||||
|
@ -537,3 +531,43 @@ func (b *StatusBackend) SignGroupMembership(content string) (string, error) {
|
|||
|
||||
return crypto.Sign(content, selectedAccount.AccountKey.PrivateKey)
|
||||
}
|
||||
|
||||
// EnableInstallation enables an installation for multi-device sync.
|
||||
func (b *StatusBackend) EnableInstallation(installationID string) error {
|
||||
selectedAccount, err := b.AccountManager().SelectedAccount()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
st, err := b.statusNode.ShhExtService()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := st.EnableInstallation(&selectedAccount.AccountKey.PrivateKey.PublicKey, installationID); err != nil {
|
||||
b.log.Error("error enabling installation", "err", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisableInstallation disables an installation for multi-device sync.
|
||||
func (b *StatusBackend) DisableInstallation(installationID string) error {
|
||||
selectedAccount, err := b.AccountManager().SelectedAccount()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
st, err := b.statusNode.ShhExtService()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := st.DisableInstallation(&selectedAccount.AccountKey.PrivateKey.PublicKey, installationID); err != nil {
|
||||
b.log.Error("error disabling installation", "err", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -94,29 +94,6 @@ func ExtractIdentityFromContactCode(bundleString *C.char) *C.char {
|
|||
return C.CString(string(data))
|
||||
}
|
||||
|
||||
// VerifyGroupMembershipSignatures ensure the signature pairs are valid
|
||||
//export VerifyGroupMembershipSignatures
|
||||
func VerifyGroupMembershipSignatures(signaturePairsStr *C.char) *C.char {
|
||||
var signaturePairs [][3]string
|
||||
|
||||
if err := json.Unmarshal([]byte(C.GoString(signaturePairsStr)), &signaturePairs); err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
if err := statusBackend.VerifyGroupMembershipSignatures(signaturePairs); err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
data, err := json.Marshal(struct {
|
||||
Success bool `json:"success"`
|
||||
}{Success: true})
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
return C.CString(string(data))
|
||||
}
|
||||
|
||||
// ExtractGroupMembershipSignatures extract public keys from tuples of content/signature
|
||||
//export ExtractGroupMembershipSignatures
|
||||
func ExtractGroupMembershipSignatures(signaturePairsStr *C.char) *C.char {
|
||||
|
@ -159,6 +136,42 @@ func SignGroupMembership(content *C.char) *C.char {
|
|||
return C.CString(string(data))
|
||||
}
|
||||
|
||||
// EnableInstallation enables an installation for multi-device sync.
|
||||
//export EnableInstallation
|
||||
func EnableInstallation(installationID *C.char) *C.char {
|
||||
err := statusBackend.EnableInstallation(C.GoString(installationID))
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
data, err := json.Marshal(struct {
|
||||
Response string `json:"response"`
|
||||
}{Response: "ok"})
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
return C.CString(string(data))
|
||||
}
|
||||
|
||||
// DisableInstallation disables an installation for multi-device sync.
|
||||
//export DisableInstallation
|
||||
func DisableInstallation(installationID *C.char) *C.char {
|
||||
err := statusBackend.DisableInstallation(C.GoString(installationID))
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
data, err := json.Marshal(struct {
|
||||
Response string `json:"response"`
|
||||
}{Response: "ok"})
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
return C.CString(string(data))
|
||||
}
|
||||
|
||||
//ValidateNodeConfig validates config for status node
|
||||
//export ValidateNodeConfig
|
||||
func ValidateNodeConfig(configJSON *C.char) *C.char {
|
||||
|
|
|
@ -3,9 +3,11 @@
|
|||
|
||||
package chat
|
||||
|
||||
import proto "github.com/golang/protobuf/proto"
|
||||
import fmt "fmt"
|
||||
import math "math"
|
||||
import (
|
||||
fmt "fmt"
|
||||
proto "github.com/golang/protobuf/proto"
|
||||
math "math"
|
||||
)
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ = proto.Marshal
|
||||
|
@ -39,6 +41,7 @@ func (*ChatMessagePayload) ProtoMessage() {}
|
|||
func (*ChatMessagePayload) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_8c585a45e2093e54, []int{0}
|
||||
}
|
||||
|
||||
func (m *ChatMessagePayload) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ChatMessagePayload.Unmarshal(m, b)
|
||||
}
|
||||
|
@ -106,6 +109,7 @@ func (*ContactUpdatePayload) ProtoMessage() {}
|
|||
func (*ContactUpdatePayload) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_8c585a45e2093e54, []int{1}
|
||||
}
|
||||
|
||||
func (m *ContactUpdatePayload) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ContactUpdatePayload.Unmarshal(m, b)
|
||||
}
|
||||
|
@ -168,6 +172,7 @@ func (*OneToOneRPC) ProtoMessage() {}
|
|||
func (*OneToOneRPC) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_8c585a45e2093e54, []int{2}
|
||||
}
|
||||
|
||||
func (m *OneToOneRPC) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_OneToOneRPC.Unmarshal(m, b)
|
||||
}
|
||||
|
@ -222,6 +227,7 @@ func (*ContactUpdateRPC) ProtoMessage() {}
|
|||
func (*ContactUpdateRPC) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_8c585a45e2093e54, []int{3}
|
||||
}
|
||||
|
||||
func (m *ContactUpdateRPC) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ContactUpdateRPC.Unmarshal(m, b)
|
||||
}
|
||||
|
@ -275,6 +281,7 @@ func (*ChatProtocolMessage) ProtoMessage() {}
|
|||
func (*ChatProtocolMessage) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_8c585a45e2093e54, []int{4}
|
||||
}
|
||||
|
||||
func (m *ChatProtocolMessage) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ChatProtocolMessage.Unmarshal(m, b)
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
ecrypto "github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/crypto/ecies"
|
||||
|
@ -18,19 +19,22 @@ import (
|
|||
|
||||
var ErrSessionNotFound = errors.New("session not found")
|
||||
|
||||
// If we have no bundles, we use a constant so that the message can reach any device
|
||||
// Max number of installations we keep synchronized.
|
||||
const maxInstallations = 5
|
||||
|
||||
// If we have no bundles, we use a constant so that the message can reach any device.
|
||||
const noInstallationID = "none"
|
||||
|
||||
// How many consecutive messages can be skipped in the receiving chain
|
||||
// How many consecutive messages can be skipped in the receiving chain.
|
||||
const maxSkip = 1000
|
||||
|
||||
// Any message with seqNo <= currentSeq - maxKeep will be deleted
|
||||
// Any message with seqNo <= currentSeq - maxKeep will be deleted.
|
||||
const maxKeep = 3000
|
||||
|
||||
// How many keys do we store in total per session
|
||||
// How many keys do we store in total per session.
|
||||
const maxMessageKeysPerSession = 2000
|
||||
|
||||
// EncryptionService defines a service that is responsible for the encryption aspect of the protocol
|
||||
// EncryptionService defines a service that is responsible for the encryption aspect of the protocol.
|
||||
type EncryptionService struct {
|
||||
log log.Logger
|
||||
persistence PersistenceService
|
||||
|
@ -40,7 +44,7 @@ type EncryptionService struct {
|
|||
|
||||
type IdentityAndIDPair [2]string
|
||||
|
||||
// NewEncryptionService creates a new EncryptionService instance
|
||||
// NewEncryptionService creates a new EncryptionService instance.
|
||||
func NewEncryptionService(p PersistenceService, installationID string) *EncryptionService {
|
||||
logger := log.New("package", "status-go/services/sshext.chat")
|
||||
logger.Info("Initialized encryption service", "installationID", installationID)
|
||||
|
@ -64,13 +68,21 @@ func (s *EncryptionService) keyFromActiveX3DH(theirIdentityKey []byte, theirSign
|
|||
// CreateBundle retrieves or creates an X3DH bundle given a private key
|
||||
func (s *EncryptionService) CreateBundle(privateKey *ecdsa.PrivateKey) (*Bundle, error) {
|
||||
ourIdentityKeyC := ecrypto.CompressPubkey(&privateKey.PublicKey)
|
||||
bundleContainer, err := s.persistence.GetAnyPrivateBundle(ourIdentityKeyC)
|
||||
|
||||
installationIDs, err := s.persistence.GetActiveInstallations(maxInstallations-1, ourIdentityKeyC)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
installationIDs = append(installationIDs, s.installationID)
|
||||
|
||||
bundleContainer, err := s.persistence.GetAnyPrivateBundle(ourIdentityKeyC, installationIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the bundle has expired we create a new one
|
||||
if bundleContainer != nil && bundleContainer.Timestamp < time.Now().AddDate(0, 0, -14).UnixNano() {
|
||||
if bundleContainer != nil && bundleContainer.GetBundle().Timestamp < time.Now().AddDate(0, 0, -14).UnixNano() {
|
||||
// Mark sessions has expired
|
||||
if err := s.persistence.MarkBundleExpired(bundleContainer.GetBundle().GetIdentity()); err != nil {
|
||||
return nil, err
|
||||
|
@ -143,6 +155,16 @@ func (s *EncryptionService) keyFromPassiveX3DH(myIdentityKey *ecdsa.PrivateKey,
|
|||
return key, nil
|
||||
}
|
||||
|
||||
func (s *EncryptionService) EnableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error {
|
||||
myIdentityKeyC := ecrypto.CompressPubkey(myIdentityKey)
|
||||
return s.persistence.EnableInstallation(myIdentityKeyC, installationID)
|
||||
}
|
||||
|
||||
func (s *EncryptionService) DisableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error {
|
||||
myIdentityKeyC := ecrypto.CompressPubkey(myIdentityKey)
|
||||
return s.persistence.DisableInstallation(myIdentityKeyC, installationID)
|
||||
}
|
||||
|
||||
// ProcessPublicBundle persists a bundle and returns a list of tuples identity/installationID
|
||||
func (s *EncryptionService) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey, b *Bundle) ([]IdentityAndIDPair, error) {
|
||||
// Make sure the bundle belongs to who signed it
|
||||
|
@ -151,16 +173,27 @@ func (s *EncryptionService) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey,
|
|||
return nil, err
|
||||
}
|
||||
signedPreKeys := b.GetSignedPreKeys()
|
||||
response := make([]IdentityAndIDPair, len(signedPreKeys))
|
||||
var response []IdentityAndIDPair
|
||||
var installationIDs []string
|
||||
myIdentityStr := fmt.Sprintf("0x%x", ecrypto.FromECDSAPub(&myIdentityKey.PublicKey))
|
||||
|
||||
if err = s.persistence.AddPublicBundle(b); err != nil {
|
||||
// Any device from other peers will be considered enabled, ours needs to
|
||||
// be explicitly enabled
|
||||
fromOurIdentity := identity != myIdentityStr
|
||||
|
||||
for installationID := range signedPreKeys {
|
||||
if installationID != s.installationID {
|
||||
installationIDs = append(installationIDs, installationID)
|
||||
response = append(response, IdentityAndIDPair{identity, installationID})
|
||||
}
|
||||
}
|
||||
|
||||
if err = s.persistence.AddInstallations(b.GetIdentity(), b.GetTimestamp(), installationIDs, fromOurIdentity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
index := 0
|
||||
for installationID := range signedPreKeys {
|
||||
response[index] = IdentityAndIDPair{identity, installationID}
|
||||
index++
|
||||
if err = s.persistence.AddPublicBundle(b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
|
@ -414,8 +447,13 @@ func (s *EncryptionService) EncryptPayload(theirIdentityKey *ecdsa.PublicKey, my
|
|||
|
||||
theirIdentityKeyC := ecrypto.CompressPubkey(theirIdentityKey)
|
||||
|
||||
installationIDs, err := s.persistence.GetActiveInstallations(maxInstallations, theirIdentityKeyC)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get their latest bundle
|
||||
theirBundle, err := s.persistence.GetPublicBundle(theirIdentityKey)
|
||||
theirBundle, err := s.persistence.GetPublicBundle(theirIdentityKey, installationIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -454,7 +492,6 @@ func (s *EncryptionService) EncryptPayload(theirIdentityKey *ecdsa.PublicKey, my
|
|||
dmp.X3DHHeader = &X3DHHeader{
|
||||
Key: drInfo.EphemeralKey,
|
||||
Id: drInfo.BundleID,
|
||||
InstallationId: s.installationID,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -462,13 +499,6 @@ func (s *EncryptionService) EncryptPayload(theirIdentityKey *ecdsa.PublicKey, my
|
|||
continue
|
||||
}
|
||||
|
||||
// check if a bundle is there
|
||||
theirBundle, err := s.persistence.GetPublicBundle(theirIdentityKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if theirBundle != nil {
|
||||
sharedKey, ourEphemeralKey, err := s.keyFromActiveX3DH(theirIdentityKeyC, theirSignedPreKey, myIdentityKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -484,10 +514,9 @@ func (s *EncryptionService) EncryptPayload(theirIdentityKey *ecdsa.PublicKey, my
|
|||
x3dhHeader := &X3DHHeader{
|
||||
Key: ourEphemeralKeyC,
|
||||
Id: theirSignedPreKey,
|
||||
InstallationId: s.installationID,
|
||||
}
|
||||
|
||||
drInfo, err := s.persistence.GetAnyRatchetInfo(theirIdentityKeyC, installationID)
|
||||
drInfo, err = s.persistence.GetAnyRatchetInfo(theirIdentityKeyC, installationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -507,7 +536,6 @@ func (s *EncryptionService) EncryptPayload(theirIdentityKey *ecdsa.PublicKey, my
|
|||
response[drInfo.InstallationID] = dmp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
|
|
@ -3,9 +3,11 @@
|
|||
|
||||
package chat
|
||||
|
||||
import proto "github.com/golang/protobuf/proto"
|
||||
import fmt "fmt"
|
||||
import math "math"
|
||||
import (
|
||||
fmt "fmt"
|
||||
proto "github.com/golang/protobuf/proto"
|
||||
math "math"
|
||||
)
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ = proto.Marshal
|
||||
|
@ -32,6 +34,7 @@ func (*SignedPreKey) ProtoMessage() {}
|
|||
func (*SignedPreKey) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_8293a649ce9418c6, []int{0}
|
||||
}
|
||||
|
||||
func (m *SignedPreKey) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_SignedPreKey.Unmarshal(m, b)
|
||||
}
|
||||
|
@ -72,6 +75,8 @@ type Bundle struct {
|
|||
SignedPreKeys map[string]*SignedPreKey `protobuf:"bytes,2,rep,name=signed_pre_keys,json=signedPreKeys,proto3" json:"signed_pre_keys,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
// Prekey signature
|
||||
Signature []byte `protobuf:"bytes,4,opt,name=signature,proto3" json:"signature,omitempty"`
|
||||
// When the bundle was created locally
|
||||
Timestamp int64 `protobuf:"varint,5,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
|
@ -83,6 +88,7 @@ func (*Bundle) ProtoMessage() {}
|
|||
func (*Bundle) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_8293a649ce9418c6, []int{1}
|
||||
}
|
||||
|
||||
func (m *Bundle) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_Bundle.Unmarshal(m, b)
|
||||
}
|
||||
|
@ -122,13 +128,18 @@ func (m *Bundle) GetSignature() []byte {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *Bundle) GetTimestamp() int64 {
|
||||
if m != nil {
|
||||
return m.Timestamp
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type BundleContainer struct {
|
||||
// X3DH prekey bundle
|
||||
Bundle *Bundle `protobuf:"bytes,1,opt,name=bundle,proto3" json:"bundle,omitempty"`
|
||||
// Private signed prekey
|
||||
PrivateSignedPreKey []byte `protobuf:"bytes,2,opt,name=private_signed_pre_key,json=privateSignedPreKey,proto3" json:"private_signed_pre_key,omitempty"`
|
||||
// Local time creation
|
||||
Timestamp int64 `protobuf:"varint,3,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
|
@ -140,6 +151,7 @@ func (*BundleContainer) ProtoMessage() {}
|
|||
func (*BundleContainer) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_8293a649ce9418c6, []int{2}
|
||||
}
|
||||
|
||||
func (m *BundleContainer) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_BundleContainer.Unmarshal(m, b)
|
||||
}
|
||||
|
@ -172,13 +184,6 @@ func (m *BundleContainer) GetPrivateSignedPreKey() []byte {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *BundleContainer) GetTimestamp() int64 {
|
||||
if m != nil {
|
||||
return m.Timestamp
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type DRHeader struct {
|
||||
// Current ratchet public key
|
||||
Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
|
||||
|
@ -199,6 +204,7 @@ func (*DRHeader) ProtoMessage() {}
|
|||
func (*DRHeader) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_8293a649ce9418c6, []int{3}
|
||||
}
|
||||
|
||||
func (m *DRHeader) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_DRHeader.Unmarshal(m, b)
|
||||
}
|
||||
|
@ -259,6 +265,7 @@ func (*DHHeader) ProtoMessage() {}
|
|||
func (*DHHeader) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_8293a649ce9418c6, []int{4}
|
||||
}
|
||||
|
||||
func (m *DHHeader) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_DHHeader.Unmarshal(m, b)
|
||||
}
|
||||
|
@ -289,8 +296,6 @@ type X3DHHeader struct {
|
|||
Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
|
||||
// Used bundle's signed prekey
|
||||
Id []byte `protobuf:"bytes,4,opt,name=id,proto3" json:"id,omitempty"`
|
||||
// DEPRECATED: The device id
|
||||
InstallationId string `protobuf:"bytes,3,opt,name=installation_id,json=installationId,proto3" json:"installation_id,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
|
@ -302,6 +307,7 @@ func (*X3DHHeader) ProtoMessage() {}
|
|||
func (*X3DHHeader) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_8293a649ce9418c6, []int{5}
|
||||
}
|
||||
|
||||
func (m *X3DHHeader) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_X3DHHeader.Unmarshal(m, b)
|
||||
}
|
||||
|
@ -334,13 +340,6 @@ func (m *X3DHHeader) GetId() []byte {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *X3DHHeader) GetInstallationId() string {
|
||||
if m != nil {
|
||||
return m.InstallationId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Direct message value
|
||||
type DirectMessageProtocol struct {
|
||||
X3DHHeader *X3DHHeader `protobuf:"bytes,1,opt,name=X3DH_header,json=X3DHHeader,proto3" json:"X3DH_header,omitempty"`
|
||||
|
@ -359,6 +358,7 @@ func (*DirectMessageProtocol) ProtoMessage() {}
|
|||
func (*DirectMessageProtocol) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_8293a649ce9418c6, []int{6}
|
||||
}
|
||||
|
||||
func (m *DirectMessageProtocol) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_DirectMessageProtocol.Unmarshal(m, b)
|
||||
}
|
||||
|
@ -426,6 +426,7 @@ func (*ProtocolMessage) ProtoMessage() {}
|
|||
func (*ProtocolMessage) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_8293a649ce9418c6, []int{7}
|
||||
}
|
||||
|
||||
func (m *ProtocolMessage) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ProtocolMessage.Unmarshal(m, b)
|
||||
}
|
||||
|
@ -488,39 +489,39 @@ func init() {
|
|||
func init() { proto.RegisterFile("encryption.proto", fileDescriptor_8293a649ce9418c6) }
|
||||
|
||||
var fileDescriptor_8293a649ce9418c6 = []byte{
|
||||
// 539 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x54, 0x51, 0x8b, 0xd3, 0x40,
|
||||
0x10, 0x26, 0xe9, 0xd9, 0x6b, 0xa7, 0x69, 0x5a, 0x56, 0x94, 0x50, 0x0b, 0x96, 0x70, 0x62, 0x40,
|
||||
0x28, 0x5c, 0xfb, 0x22, 0x3e, 0x6a, 0xc5, 0xaa, 0xa8, 0xc7, 0x2a, 0xe8, 0x8b, 0x84, 0x6d, 0x33,
|
||||
0xde, 0x2d, 0xa6, 0x9b, 0xb0, 0xd9, 0x16, 0xfa, 0x17, 0xfc, 0x43, 0xbe, 0xf9, 0x5f, 0xfc, 0x27,
|
||||
0x92, 0xdd, 0xa4, 0xdd, 0xf6, 0x7a, 0xe0, 0x5b, 0x66, 0x76, 0xf6, 0x9b, 0xef, 0xfb, 0x66, 0x27,
|
||||
0xd0, 0x47, 0xb1, 0x94, 0xdb, 0x5c, 0xf1, 0x4c, 0x8c, 0x73, 0x99, 0xa9, 0x8c, 0x9c, 0x2d, 0x6f,
|
||||
0x98, 0x0a, 0x3f, 0x82, 0xf7, 0x99, 0x5f, 0x0b, 0x4c, 0xae, 0x24, 0xbe, 0xc7, 0x2d, 0xb9, 0x00,
|
||||
0xbf, 0xd0, 0x71, 0x9c, 0x4b, 0x8c, 0x7f, 0xe2, 0x36, 0x70, 0x46, 0x4e, 0xe4, 0x51, 0xaf, 0xb0,
|
||||
0xab, 0x02, 0x38, 0xdf, 0xa0, 0x2c, 0x78, 0x26, 0x02, 0x77, 0xe4, 0x44, 0x5d, 0x5a, 0x87, 0xe1,
|
||||
0x5f, 0x07, 0x9a, 0x2f, 0xd7, 0x22, 0x49, 0x91, 0x0c, 0xa0, 0xc5, 0x13, 0x14, 0x8a, 0xab, 0x1a,
|
||||
0x64, 0x17, 0x93, 0x37, 0xd0, 0x3b, 0x6c, 0x53, 0x04, 0xee, 0xa8, 0x11, 0x75, 0x26, 0x8f, 0xc7,
|
||||
0x25, 0xad, 0xb1, 0x81, 0x18, 0xdb, 0xd4, 0x8a, 0xd7, 0x42, 0xc9, 0x2d, 0xed, 0xda, 0x44, 0x0a,
|
||||
0x32, 0x84, 0x76, 0x99, 0x60, 0x6a, 0x2d, 0x31, 0x38, 0xd3, 0x5d, 0xf6, 0x89, 0xc1, 0x17, 0x20,
|
||||
0xb7, 0x21, 0x48, 0x1f, 0x1a, 0xb5, 0xb0, 0x36, 0x2d, 0x3f, 0x49, 0x04, 0xf7, 0x36, 0x2c, 0x5d,
|
||||
0xa3, 0x56, 0xd3, 0x99, 0x10, 0x43, 0xc2, 0xbe, 0x4a, 0x4d, 0xc1, 0x0b, 0xf7, 0xb9, 0x13, 0xfe,
|
||||
0x72, 0xa0, 0x67, 0x08, 0xbe, 0xca, 0x84, 0x62, 0x5c, 0xa0, 0x24, 0x17, 0xd0, 0x5c, 0xe8, 0x94,
|
||||
0x86, 0xed, 0x4c, 0x3c, 0x5b, 0x07, 0xad, 0xce, 0xc8, 0x14, 0x1e, 0xe6, 0x92, 0x6f, 0x98, 0xc2,
|
||||
0xf8, 0xc8, 0x65, 0x57, 0x53, 0xbf, 0x5f, 0x9d, 0x1e, 0x8c, 0x64, 0x08, 0x6d, 0xc5, 0x57, 0x58,
|
||||
0x28, 0xb6, 0xca, 0x83, 0xc6, 0xc8, 0x89, 0x1a, 0x74, 0x9f, 0x08, 0xdf, 0x41, 0x6b, 0x46, 0xe7,
|
||||
0xc8, 0x12, 0x94, 0xb6, 0x30, 0xcf, 0x08, 0xf3, 0xc0, 0xa9, 0x47, 0xe4, 0x08, 0xe2, 0x83, 0x9b,
|
||||
0x0b, 0x0d, 0xd1, 0xa5, 0x6e, 0xae, 0x63, 0x9e, 0x54, 0xae, 0xb9, 0x3c, 0x09, 0x87, 0xd0, 0x9a,
|
||||
0xcd, 0xef, 0xc2, 0x0a, 0xbf, 0x02, 0x7c, 0x9b, 0xde, 0x7d, 0x7e, 0x8c, 0x46, 0x9e, 0x42, 0x8f,
|
||||
0x8b, 0x42, 0xb1, 0x34, 0x65, 0xe5, 0xb3, 0x8b, 0x79, 0xa2, 0x5b, 0xb7, 0xa9, 0x6f, 0xa7, 0xdf,
|
||||
0x26, 0xe1, 0x1f, 0x07, 0x1e, 0xcc, 0xb8, 0xc4, 0xa5, 0xfa, 0x80, 0x45, 0xc1, 0xae, 0xf1, 0xaa,
|
||||
0x7c, 0xa0, 0xcb, 0x2c, 0x25, 0x97, 0xd0, 0x29, 0x5b, 0xc6, 0x37, 0xba, 0x67, 0x65, 0x6d, 0xdf,
|
||||
0x58, 0xbb, 0xe7, 0x42, 0x6d, 0x5e, 0xcf, 0xa0, 0x3d, 0xa3, 0xf5, 0x05, 0x33, 0x4e, 0xdf, 0x5c,
|
||||
0xa8, 0x6d, 0xa2, 0x7b, 0xc3, 0xca, 0xe2, 0x1d, 0x3a, 0x1e, 0x14, 0xcf, 0x77, 0xc5, 0x35, 0x72,
|
||||
0x00, 0xe7, 0x39, 0xdb, 0xa6, 0x19, 0x33, 0x3a, 0x3c, 0x5a, 0x87, 0xe1, 0x6f, 0x17, 0x7a, 0x35,
|
||||
0xe7, 0x4a, 0xc2, 0x7f, 0x3e, 0x88, 0x13, 0x1e, 0xb9, 0xa7, 0x3c, 0x22, 0x9f, 0xc0, 0x4f, 0xb4,
|
||||
0x45, 0xf1, 0xca, 0x34, 0x08, 0x50, 0xef, 0x4b, 0x64, 0x60, 0x8f, 0xba, 0x8f, 0x0f, 0xec, 0xac,
|
||||
0x16, 0x27, 0xb1, 0x73, 0xe4, 0x09, 0xf8, 0xf9, 0x7a, 0x91, 0xf2, 0xe5, 0x0e, 0xf0, 0x87, 0x16,
|
||||
0xd5, 0x35, 0xd9, 0xaa, 0x6c, 0xf0, 0x1d, 0xc8, 0x6d, 0xac, 0x13, 0x1b, 0x74, 0x79, 0xb8, 0x41,
|
||||
0x8f, 0x2a, 0x17, 0x4f, 0x4d, 0xd5, 0x5a, 0xa5, 0x45, 0x53, 0xff, 0x8b, 0xa6, 0xff, 0x02, 0x00,
|
||||
0x00, 0xff, 0xff, 0x42, 0x9c, 0x34, 0x10, 0x9f, 0x04, 0x00, 0x00,
|
||||
// 535 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x54, 0x5d, 0x8b, 0xd3, 0x40,
|
||||
0x14, 0x25, 0x69, 0xb7, 0xdb, 0xde, 0xa6, 0x69, 0x19, 0x51, 0x42, 0x5d, 0xb0, 0x84, 0x15, 0x03,
|
||||
0x42, 0x61, 0x5b, 0x1f, 0xc4, 0x47, 0xad, 0x58, 0x57, 0xd4, 0x65, 0xf4, 0xc1, 0x17, 0x09, 0xd3,
|
||||
0xe6, 0xba, 0x3b, 0x98, 0x4e, 0xc2, 0x64, 0x5a, 0xe8, 0x2f, 0xf3, 0xcd, 0xbf, 0xa6, 0x64, 0x26,
|
||||
0x69, 0xa7, 0xdd, 0x5d, 0xd8, 0xb7, 0xde, 0x8f, 0x39, 0xe7, 0xdc, 0x73, 0x7b, 0x03, 0x03, 0x14,
|
||||
0x4b, 0xb9, 0xcd, 0x15, 0xcf, 0xc4, 0x38, 0x97, 0x99, 0xca, 0x48, 0x73, 0x79, 0xc3, 0x54, 0xf8,
|
||||
0x05, 0xbc, 0x6f, 0xfc, 0x5a, 0x60, 0x72, 0x25, 0xf1, 0x13, 0x6e, 0xc9, 0x39, 0xf8, 0x85, 0x8e,
|
||||
0xe3, 0x5c, 0x62, 0xfc, 0x1b, 0xb7, 0x81, 0x33, 0x72, 0x22, 0x8f, 0x7a, 0x85, 0xdd, 0x15, 0xc0,
|
||||
0xe9, 0x06, 0x65, 0xc1, 0x33, 0x11, 0xb8, 0x23, 0x27, 0xea, 0xd1, 0x3a, 0x0c, 0xff, 0x39, 0xd0,
|
||||
0x7a, 0xbb, 0x16, 0x49, 0x8a, 0x64, 0x08, 0x6d, 0x9e, 0xa0, 0x50, 0x5c, 0xd5, 0x20, 0xbb, 0x98,
|
||||
0x7c, 0x80, 0xfe, 0x21, 0x4d, 0x11, 0xb8, 0xa3, 0x46, 0xd4, 0x9d, 0x3c, 0x1b, 0x97, 0xb2, 0xc6,
|
||||
0x06, 0x62, 0x6c, 0x4b, 0x2b, 0xde, 0x0b, 0x25, 0xb7, 0xb4, 0x67, 0x0b, 0x29, 0xc8, 0x19, 0x74,
|
||||
0xca, 0x04, 0x53, 0x6b, 0x89, 0x41, 0x53, 0xb3, 0xec, 0x13, 0x65, 0x55, 0xf1, 0x15, 0x16, 0x8a,
|
||||
0xad, 0xf2, 0xe0, 0x64, 0xe4, 0x44, 0x0d, 0xba, 0x4f, 0x0c, 0xbf, 0x03, 0xb9, 0x4d, 0x40, 0x06,
|
||||
0xd0, 0xa8, 0xc7, 0xee, 0xd0, 0xf2, 0x27, 0x89, 0xe0, 0x64, 0xc3, 0xd2, 0x35, 0xea, 0x59, 0xbb,
|
||||
0x13, 0x62, 0x24, 0xda, 0x4f, 0xa9, 0x69, 0x78, 0xe3, 0xbe, 0x76, 0x42, 0x09, 0x7d, 0xa3, 0xfe,
|
||||
0x5d, 0x26, 0x14, 0xe3, 0x02, 0x25, 0x39, 0x87, 0xd6, 0x42, 0xa7, 0x34, 0x6a, 0x77, 0xe2, 0xd9,
|
||||
0x43, 0xd2, 0xaa, 0x46, 0xa6, 0xf0, 0x24, 0x97, 0x7c, 0xc3, 0x14, 0xc6, 0x47, 0x2b, 0x70, 0xf5,
|
||||
0x5c, 0x8f, 0xaa, 0xaa, 0x4d, 0x7c, 0xd9, 0x6c, 0x37, 0x06, 0xcd, 0xf0, 0x12, 0xda, 0x33, 0x3a,
|
||||
0x47, 0x96, 0xa0, 0xb4, 0xf5, 0x7b, 0x46, 0xbf, 0x07, 0x4e, 0xbd, 0x27, 0x47, 0x10, 0x1f, 0xdc,
|
||||
0x5c, 0x04, 0x0d, 0x1d, 0xba, 0xb9, 0x8e, 0x79, 0x52, 0x59, 0xe7, 0xf2, 0x24, 0x3c, 0x83, 0xf6,
|
||||
0x6c, 0x7e, 0x1f, 0x56, 0xf8, 0x0a, 0xe0, 0xc7, 0xf4, 0xfe, 0xfa, 0x31, 0x5a, 0xa5, 0xef, 0xaf,
|
||||
0x03, 0x8f, 0x67, 0x5c, 0xe2, 0x52, 0x7d, 0xc6, 0xa2, 0x60, 0xd7, 0x78, 0x55, 0xfe, 0x05, 0x97,
|
||||
0x59, 0x4a, 0x2e, 0xa0, 0x5b, 0xe2, 0xc5, 0x37, 0x1a, 0xb0, 0xf2, 0x67, 0x60, 0xfc, 0xd9, 0x13,
|
||||
0x51, 0x9b, 0xf4, 0x25, 0x74, 0x66, 0xb4, 0x7e, 0x60, 0x56, 0xe2, 0x9b, 0x07, 0xb5, 0x07, 0x74,
|
||||
0xef, 0x46, 0xd9, 0xbc, 0x43, 0xc7, 0x83, 0xe6, 0xf9, 0xae, 0xb9, 0x46, 0x0e, 0xe0, 0x34, 0x67,
|
||||
0xdb, 0x34, 0x63, 0x89, 0xf6, 0xc7, 0xa3, 0x75, 0x18, 0xfe, 0x71, 0xa1, 0x5f, 0x6b, 0xae, 0x46,
|
||||
0x78, 0xe0, 0x56, 0x5f, 0x40, 0x9f, 0x8b, 0x42, 0xb1, 0x34, 0x65, 0xe5, 0xf1, 0xc5, 0x3c, 0xd1,
|
||||
0x9a, 0x3b, 0xd4, 0xb7, 0xd3, 0x1f, 0x13, 0xf2, 0x15, 0xfc, 0x44, 0x5b, 0x14, 0xaf, 0x0c, 0x41,
|
||||
0x80, 0xfa, 0x22, 0x22, 0x03, 0x7b, 0xc4, 0x3e, 0x3e, 0xb0, 0xb3, 0x3a, 0x8d, 0xc4, 0xce, 0x91,
|
||||
0xe7, 0xe0, 0xe7, 0xeb, 0x45, 0xca, 0x97, 0x3b, 0xc0, 0x5f, 0x7a, 0xa8, 0x9e, 0xc9, 0x56, 0x6d,
|
||||
0xc3, 0x9f, 0x40, 0x6e, 0x63, 0xdd, 0x71, 0x05, 0x17, 0x87, 0x57, 0xf0, 0xb4, 0x72, 0xf1, 0xae,
|
||||
0xad, 0x5a, 0xe7, 0xb0, 0x68, 0xe9, 0xaf, 0xcd, 0xf4, 0x7f, 0x00, 0x00, 0x00, 0xff, 0xff, 0xed,
|
||||
0x53, 0xaf, 0x00, 0x81, 0x04, 0x00, 0x00,
|
||||
}
|
||||
|
|
|
@ -15,16 +15,17 @@ message Bundle {
|
|||
map<string,SignedPreKey> signed_pre_keys = 2;
|
||||
// Prekey signature
|
||||
bytes signature = 4;
|
||||
|
||||
// When the bundle was created locally
|
||||
int64 timestamp = 5;
|
||||
}
|
||||
|
||||
message BundleContainer {
|
||||
reserved 3;
|
||||
// X3DH prekey bundle
|
||||
Bundle bundle = 1;
|
||||
// Private signed prekey
|
||||
bytes private_signed_pre_key = 2;
|
||||
|
||||
// Local time creation
|
||||
int64 timestamp = 3;
|
||||
}
|
||||
|
||||
message DRHeader {
|
||||
|
@ -44,12 +45,11 @@ message DHHeader {
|
|||
}
|
||||
|
||||
message X3DHHeader {
|
||||
reserved 3;
|
||||
// Ephemeral key used
|
||||
bytes key = 1;
|
||||
// Used bundle's signed prekey
|
||||
bytes id = 4;
|
||||
// DEPRECATED: The device id
|
||||
string installation_id = 3;
|
||||
}
|
||||
|
||||
// Direct message value
|
||||
|
|
|
@ -18,6 +18,7 @@ type EncryptionServiceMultiDeviceSuite struct {
|
|||
bob1 *EncryptionService
|
||||
alice2 *EncryptionService
|
||||
bob2 *EncryptionService
|
||||
alice3 *EncryptionService
|
||||
}
|
||||
|
||||
func (s *EncryptionServiceMultiDeviceSuite) SetupTest() {
|
||||
|
@ -26,6 +27,8 @@ func (s *EncryptionServiceMultiDeviceSuite) SetupTest() {
|
|||
aliceDBKey1 = "alice1"
|
||||
aliceDBPath2 = "/tmp/alice2.db"
|
||||
aliceDBKey2 = "alice2"
|
||||
aliceDBPath3 = "/tmp/alice3.db"
|
||||
aliceDBKey3 = "alice3"
|
||||
bobDBPath1 = "/tmp/bob1.db"
|
||||
bobDBKey1 = "bob1"
|
||||
bobDBPath2 = "/tmp/bob2.db"
|
||||
|
@ -36,6 +39,7 @@ func (s *EncryptionServiceMultiDeviceSuite) SetupTest() {
|
|||
os.Remove(bobDBPath1)
|
||||
os.Remove(aliceDBPath2)
|
||||
os.Remove(bobDBPath2)
|
||||
os.Remove(aliceDBPath3)
|
||||
|
||||
alicePersistence1, err := NewSQLLitePersistence(aliceDBPath1, aliceDBKey1)
|
||||
if err != nil {
|
||||
|
@ -47,6 +51,11 @@ func (s *EncryptionServiceMultiDeviceSuite) SetupTest() {
|
|||
panic(err)
|
||||
}
|
||||
|
||||
alicePersistence3, err := NewSQLLitePersistence(aliceDBPath3, aliceDBKey3)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
bobPersistence1, err := NewSQLLitePersistence(bobDBPath1, bobDBKey1)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
@ -63,41 +72,79 @@ func (s *EncryptionServiceMultiDeviceSuite) SetupTest() {
|
|||
s.alice2 = NewEncryptionService(alicePersistence2, "alice2")
|
||||
s.bob2 = NewEncryptionService(bobPersistence2, "bob2")
|
||||
|
||||
s.alice3 = NewEncryptionService(alicePersistence3, "alice3")
|
||||
}
|
||||
|
||||
func (s *EncryptionServiceMultiDeviceSuite) TestProcessPublicBundle() {
|
||||
aliceKey, err := crypto.GenerateKey()
|
||||
s.Require().NoError(err)
|
||||
|
||||
alice1Bundle, err := s.alice1.CreateBundle(aliceKey)
|
||||
s.Require().NoError(err)
|
||||
|
||||
alice1Identity, err := ExtractIdentity(alice1Bundle)
|
||||
s.Require().NoError(err)
|
||||
|
||||
alice2Bundle, err := s.alice2.CreateBundle(aliceKey)
|
||||
s.Require().NoError(err)
|
||||
|
||||
alice2Identity, err := ExtractIdentity(alice2Bundle)
|
||||
s.Require().NoError(err)
|
||||
|
||||
alice3Bundle, err := s.alice3.CreateBundle(aliceKey)
|
||||
s.Require().NoError(err)
|
||||
|
||||
alice3Identity, err := ExtractIdentity(alice2Bundle)
|
||||
s.Require().NoError(err)
|
||||
|
||||
// Add alice2 bundle
|
||||
response, err := s.alice1.ProcessPublicBundle(aliceKey, alice2Bundle)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal(IdentityAndIDPair{alice2Identity, "alice2"}, response[0])
|
||||
|
||||
// Add alice3 bundle
|
||||
response, err = s.alice1.ProcessPublicBundle(aliceKey, alice3Bundle)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal(IdentityAndIDPair{alice3Identity, "alice3"}, response[0])
|
||||
|
||||
// No installation is enabled
|
||||
alice1MergedBundle1, err := s.alice1.CreateBundle(aliceKey)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Require().Equal(1, len(alice1MergedBundle1.GetSignedPreKeys()))
|
||||
s.Require().NotNil(alice1MergedBundle1.GetSignedPreKeys()["alice1"])
|
||||
s.Require().NotNil(alice1MergedBundle1.GetSignedPreKeys()["alice2"])
|
||||
|
||||
response, err = s.alice1.ProcessPublicBundle(aliceKey, alice1MergedBundle1)
|
||||
// We enable the installations
|
||||
err = s.alice1.EnableInstallation(&aliceKey.PublicKey, "alice2")
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = s.alice1.EnableInstallation(&aliceKey.PublicKey, "alice3")
|
||||
s.Require().NoError(err)
|
||||
|
||||
alice1MergedBundle2, err := s.alice1.CreateBundle(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"])
|
||||
|
||||
response, err = s.alice1.ProcessPublicBundle(aliceKey, alice1MergedBundle2)
|
||||
s.Require().NoError(err)
|
||||
sort.Slice(response, func(i, j int) bool {
|
||||
return response[i][1] < response[j][1]
|
||||
})
|
||||
s.Require().Equal(IdentityAndIDPair{alice1Identity, "alice1"}, response[0])
|
||||
s.Require().Equal(IdentityAndIDPair{alice2Identity, "alice2"}, response[1])
|
||||
// We only get back installationIDs not equal to us
|
||||
s.Require().Equal(2, len(response))
|
||||
s.Require().Equal(IdentityAndIDPair{alice2Identity, "alice2"}, response[0])
|
||||
s.Require().Equal(IdentityAndIDPair{alice2Identity, "alice3"}, response[1])
|
||||
|
||||
// We disable the installations
|
||||
err = s.alice1.DisableInstallation(&aliceKey.PublicKey, "alice2")
|
||||
s.Require().NoError(err)
|
||||
|
||||
alice1MergedBundle3, err := s.alice1.CreateBundle(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() {
|
||||
|
@ -116,10 +163,14 @@ func (s *EncryptionServiceMultiDeviceSuite) TestProcessPublicBundleOutOfOrder()
|
|||
_, err = s.alice2.CreateBundle(aliceKey)
|
||||
s.Require().NoError(err)
|
||||
|
||||
// It should contain both bundles
|
||||
alice1MergedBundle1, err := s.alice2.CreateBundle(aliceKey)
|
||||
// We enable the installation
|
||||
err = s.alice2.EnableInstallation(&aliceKey.PublicKey, "alice1")
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Require().NotNil(alice1MergedBundle1.GetSignedPreKeys()["alice1"])
|
||||
s.Require().NotNil(alice1MergedBundle1.GetSignedPreKeys()["alice2"])
|
||||
// It should contain both bundles
|
||||
alice2MergedBundle1, err := s.alice2.CreateBundle(aliceKey)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Require().NotNil(alice2MergedBundle1.GetSignedPreKeys()["alice1"])
|
||||
s.Require().NotNil(alice2MergedBundle1.GetSignedPreKeys()["alice2"])
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
// 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
|
||||
// static.go
|
||||
// DO NOT EDIT!
|
||||
|
||||
|
@ -169,7 +171,7 @@ 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(1540989119, 0)}
|
||||
info := bindataFileInfo{name: "1540715431_add_version.down.sql", size: 127, mode: os.FileMode(420), modTime: time.Unix(1541170185, 0)}
|
||||
a := &asset{bytes: bytes, info: info}
|
||||
return a, nil
|
||||
}
|
||||
|
@ -189,7 +191,47 @@ 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(1540989075, 0)}
|
||||
info := bindataFileInfo{name: "1540715431_add_version.up.sql", size: 265, mode: os.FileMode(420), modTime: time.Unix(1541170185, 0)}
|
||||
a := &asset{bytes: bytes, info: info}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
var __1541164797_add_installationsDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\x09\xf2\x0f\x50\x08\x71\x74\xf2\x71\x55\xc8\xcc\x2b\x2e\x49\xcc\xc9\x49\x2c\xc9\xcc\xcf\x2b\xb6\xe6\x02\x04\x00\x00\xff\xff\xd8\xbf\x14\x75\x1a\x00\x00\x00")
|
||||
|
||||
func _1541164797_add_installationsDownSqlBytes() ([]byte, error) {
|
||||
return bindataRead(
|
||||
__1541164797_add_installationsDownSql,
|
||||
"1541164797_add_installations.down.sql",
|
||||
)
|
||||
}
|
||||
|
||||
func _1541164797_add_installationsDownSql() (*asset, error) {
|
||||
bytes, err := _1541164797_add_installationsDownSqlBytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := bindataFileInfo{name: "1541164797_add_installations.down.sql", size: 26, mode: os.FileMode(420), modTime: time.Unix(1541165366, 0)}
|
||||
a := &asset{bytes: bytes, info: info}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
var __1541164797_add_installationsUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x5c\xce\xb1\x6a\xc3\x30\x14\x85\xe1\xdd\x4f\x71\x46\x1b\xbc\x74\xee\x24\xc9\xd7\x46\x70\xb9\x6a\x5d\x09\xba\x05\x05\x6b\x10\xd8\x4a\xc0\x5a\xf2\xf6\xc1\x43\x20\xce\xfc\x7f\x70\x8e\x99\x49\x79\x82\x57\x9a\x09\xb9\xec\x35\xae\x6b\xac\xf9\x56\x76\xa0\x6d\x80\xbc\xa4\x52\x73\x7d\x40\xb3\xd3\x10\xe7\x21\x81\xb9\x3f\xca\x1b\xbe\xe4\x05\x9e\xfe\xfd\x09\xd4\xbc\xa5\xbd\xc6\xed\x8e\x20\x7f\x76\x12\x1a\xa0\xed\x04\x2b\x67\x96\x4a\xbc\xae\x69\x81\x76\x8e\x49\x09\x06\x1a\x55\x60\x8f\xaf\x23\x06\xb1\xbf\x81\xda\xd7\x8b\xfe\x73\xb5\x83\x13\x18\x27\x23\x5b\xe3\x31\xd3\x0f\x2b\x43\x4d\xf7\xdd\x3c\x03\x00\x00\xff\xff\x28\x14\xac\x9d\xd8\x00\x00\x00")
|
||||
|
||||
func _1541164797_add_installationsUpSqlBytes() ([]byte, error) {
|
||||
return bindataRead(
|
||||
__1541164797_add_installationsUpSql,
|
||||
"1541164797_add_installations.up.sql",
|
||||
)
|
||||
}
|
||||
|
||||
func _1541164797_add_installationsUpSql() (*asset, error) {
|
||||
bytes, err := _1541164797_add_installationsUpSqlBytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := bindataFileInfo{name: "1541164797_add_installations.up.sql", size: 216, mode: os.FileMode(420), modTime: time.Unix(1541165467, 0)}
|
||||
a := &asset{bytes: bytes, info: info}
|
||||
return a, nil
|
||||
}
|
||||
|
@ -272,6 +314,8 @@ var _bindata = map[string]func() (*asset, error){
|
|||
"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,
|
||||
"static.go": staticGo,
|
||||
}
|
||||
|
||||
|
@ -321,6 +365,8 @@ var _bintree = &bintree{nil, 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{}},
|
||||
"static.go": &bintree{staticGo, map[string]*bintree{}},
|
||||
}}
|
||||
|
||||
|
|
|
@ -20,32 +20,41 @@ type RatchetInfo struct {
|
|||
|
||||
// PersistenceService defines the interface for a storage service
|
||||
type PersistenceService interface {
|
||||
// GetKeysStorage returns the associated double ratchet KeysStorage object
|
||||
// GetKeysStorage returns the associated double ratchet KeysStorage object.
|
||||
GetKeysStorage() dr.KeysStorage
|
||||
// GetSessionStorage returns the associated double ratchet SessionStorage object
|
||||
// GetSessionStorage returns the associated double ratchet SessionStorage object.
|
||||
GetSessionStorage() dr.SessionStorage
|
||||
|
||||
// GetPublicBundle retrieves an existing Bundle for the specified public key
|
||||
GetPublicBundle(*ecdsa.PublicKey) (*Bundle, error)
|
||||
// GetPublicBundle retrieves an existing Bundle for the specified public key & installationIDs.
|
||||
GetPublicBundle(*ecdsa.PublicKey, []string) (*Bundle, error)
|
||||
// AddPublicBundle persists a specified Bundle
|
||||
AddPublicBundle(*Bundle) error
|
||||
|
||||
// GetAnyPrivateBundle retrieves any bundle containing a private key
|
||||
GetAnyPrivateBundle([]byte) (*BundleContainer, error)
|
||||
// GetPrivateKeyBundle retrieves a BundleContainer with the specified signed prekey
|
||||
// GetAnyPrivateBundle retrieves any bundle for our identity & installationIDs
|
||||
GetAnyPrivateBundle([]byte, []string) (*BundleContainer, error)
|
||||
// GetPrivateKeyBundle retrieves a BundleContainer with the specified signed prekey.
|
||||
GetPrivateKeyBundle([]byte) ([]byte, error)
|
||||
// AddPrivateBundle persists a BundleContainer
|
||||
// AddPrivateBundle persists a BundleContainer.
|
||||
AddPrivateBundle(*BundleContainer) error
|
||||
// MarkBundleExpired marks a bundle as expired, not to be used for encryption anymore
|
||||
// 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 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 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
|
||||
// associated with the specified bundle ID and interlocutor identity public key.
|
||||
RatchetInfoConfirmed([]byte, []byte, string) error
|
||||
|
||||
// GetActiveInstallations returns the active installations for a given identity.
|
||||
GetActiveInstallations(maxInstallations uint, identity []byte) ([]string, error)
|
||||
// AddInstallations adds the installations for a given identity.
|
||||
AddInstallations(identity []byte, timestamp int64, installationIDs []string, enabled bool) error
|
||||
// EnableInstallation enables the installation.
|
||||
EnableInstallation(identity []byte, installationID string) error
|
||||
// DisableInstallation disable the installation.
|
||||
DisableInstallation(identity []byte, installationID string) error
|
||||
}
|
||||
|
|
|
@ -87,7 +87,7 @@ func (p *ProtocolService) BuildDirectMessage(myIdentityKey *ecdsa.PrivateKey, pa
|
|||
return response, nil
|
||||
}
|
||||
|
||||
// BuildPairingMessage sends a message to our own devices using DH so that it can be decrypted by any other device
|
||||
// BuildPairingMessage sends a message to our own devices using DH so that it can be decrypted by any other device.
|
||||
func (p *ProtocolService) BuildPairingMessage(myIdentityKey *ecdsa.PrivateKey, payload []byte) ([]byte, error) {
|
||||
// Encrypt payload
|
||||
encryptionResponse, err := p.encryption.EncryptPayloadWithDH(&myIdentityKey.PublicKey, payload)
|
||||
|
@ -105,17 +105,27 @@ func (p *ProtocolService) BuildPairingMessage(myIdentityKey *ecdsa.PrivateKey, p
|
|||
return p.addBundleAndMarshal(myIdentityKey, protocolMessage)
|
||||
}
|
||||
|
||||
// ProcessPublicBundle processes a received X3DH bundle
|
||||
// ProcessPublicBundle processes a received X3DH bundle.
|
||||
func (p *ProtocolService) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey, bundle *Bundle) ([]IdentityAndIDPair, error) {
|
||||
return p.encryption.ProcessPublicBundle(myIdentityKey, bundle)
|
||||
}
|
||||
|
||||
// GetBundle retrieves or creates a X3DH bundle, given a private identity key
|
||||
// GetBundle retrieves or creates a X3DH bundle, given a private identity key.
|
||||
func (p *ProtocolService) GetBundle(myIdentityKey *ecdsa.PrivateKey) (*Bundle, error) {
|
||||
return p.encryption.CreateBundle(myIdentityKey)
|
||||
}
|
||||
|
||||
// HandleMessage unmarshals a message and processes it, decrypting it if it is a 1:1 message
|
||||
// EnableInstallation enables an installation for multi-device sync.
|
||||
func (p *ProtocolService) EnableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error {
|
||||
return p.encryption.EnableInstallation(myIdentityKey, installationID)
|
||||
}
|
||||
|
||||
// DisableInstallation disables an installation for multi-device sync.
|
||||
func (p *ProtocolService) DisableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error {
|
||||
return p.encryption.DisableInstallation(myIdentityKey, installationID)
|
||||
}
|
||||
|
||||
// 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, payload []byte) (*HandleMessageResponse, error) {
|
||||
if p.encryption == nil {
|
||||
return nil, errors.New("encryption service not initialized")
|
||||
|
|
|
@ -3,7 +3,7 @@ package chat
|
|||
import (
|
||||
"crypto/ecdsa"
|
||||
"database/sql"
|
||||
"time"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
|
||||
|
@ -103,13 +103,13 @@ func (s *SQLLitePersistence) Open(path string, key string) error {
|
|||
}
|
||||
|
||||
// AddPrivateBundle adds the specified BundleContainer to the database
|
||||
func (s *SQLLitePersistence) AddPrivateBundle(b *BundleContainer) error {
|
||||
func (s *SQLLitePersistence) AddPrivateBundle(bc *BundleContainer) error {
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for installationID, signedPreKey := range b.GetBundle().GetSignedPreKeys() {
|
||||
for installationID, signedPreKey := range bc.GetBundle().GetSignedPreKeys() {
|
||||
var version uint32
|
||||
stmt, err := tx.Prepare("SELECT version FROM bundles WHERE installation_id = ? AND identity = ? ORDER BY version DESC LIMIT 1")
|
||||
if err != nil {
|
||||
|
@ -118,7 +118,7 @@ func (s *SQLLitePersistence) AddPrivateBundle(b *BundleContainer) error {
|
|||
|
||||
defer stmt.Close()
|
||||
|
||||
err = stmt.QueryRow(installationID, b.GetBundle().GetIdentity()).Scan(&version)
|
||||
err = stmt.QueryRow(installationID, bc.GetBundle().GetIdentity()).Scan(&version)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
|
@ -130,12 +130,12 @@ func (s *SQLLitePersistence) AddPrivateBundle(b *BundleContainer) error {
|
|||
defer stmt.Close()
|
||||
|
||||
_, err = stmt.Exec(
|
||||
b.GetBundle().GetIdentity(),
|
||||
b.GetPrivateSignedPreKey(),
|
||||
bc.GetBundle().GetIdentity(),
|
||||
bc.GetPrivateSignedPreKey(),
|
||||
signedPreKey.GetSignedPreKey(),
|
||||
installationID,
|
||||
version+1,
|
||||
time.Now().UnixNano(),
|
||||
bc.GetBundle().GetTimestamp(),
|
||||
)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
|
@ -173,7 +173,7 @@ func (s *SQLLitePersistence) AddPublicBundle(b *Bundle) error {
|
|||
signedPreKey,
|
||||
installationID,
|
||||
version,
|
||||
time.Now().UnixNano(),
|
||||
b.GetTimestamp(),
|
||||
)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
|
@ -202,8 +202,11 @@ func (s *SQLLitePersistence) AddPublicBundle(b *Bundle) error {
|
|||
}
|
||||
|
||||
// GetAnyPrivateBundle retrieves any bundle from the database containing a private key
|
||||
func (s *SQLLitePersistence) GetAnyPrivateBundle(myIdentityKey []byte) (*BundleContainer, error) {
|
||||
stmt, err := s.db.Prepare("SELECT identity, private_key, signed_pre_key, installation_id, timestamp FROM bundles WHERE identity = ? AND expired = 0")
|
||||
func (s *SQLLitePersistence) GetAnyPrivateBundle(myIdentityKey []byte, installationIDs []string) (*BundleContainer, error) {
|
||||
|
||||
/* #nosec */
|
||||
statement := "SELECT identity, private_key, signed_pre_key, installation_id, timestamp FROM bundles WHERE expired = 0 AND identity = ? AND installation_id IN (?" + strings.Repeat(",?", len(installationIDs)-1) + ")"
|
||||
stmt, err := s.db.Prepare(statement)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -213,7 +216,13 @@ func (s *SQLLitePersistence) GetAnyPrivateBundle(myIdentityKey []byte) (*BundleC
|
|||
var identity []byte
|
||||
var privateKey []byte
|
||||
|
||||
rows, err := stmt.Query(myIdentityKey)
|
||||
args := make([]interface{}, len(installationIDs)+1)
|
||||
args[0] = myIdentityKey
|
||||
for i, installationID := range installationIDs {
|
||||
args[i+1] = installationID
|
||||
}
|
||||
|
||||
rows, err := stmt.Query(args...)
|
||||
rowCount := 0
|
||||
|
||||
if err != nil {
|
||||
|
@ -246,7 +255,7 @@ func (s *SQLLitePersistence) GetAnyPrivateBundle(myIdentityKey []byte) (*BundleC
|
|||
}
|
||||
// If there is a private key, we set the timestamp of the bundle container
|
||||
if privateKey != nil {
|
||||
bundleContainer.Timestamp = timestamp
|
||||
bundle.Timestamp = timestamp
|
||||
}
|
||||
|
||||
bundle.SignedPreKeys[installationID] = &SignedPreKey{SignedPreKey: signedPreKey}
|
||||
|
@ -254,7 +263,7 @@ func (s *SQLLitePersistence) GetAnyPrivateBundle(myIdentityKey []byte) (*BundleC
|
|||
}
|
||||
|
||||
// If no records are found or no record with private key, return nil
|
||||
if rowCount == 0 || bundleContainer.Timestamp == 0 {
|
||||
if rowCount == 0 || bundleContainer.GetBundle().Timestamp == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
@ -283,10 +292,9 @@ func (s *SQLLitePersistence) GetPrivateKeyBundle(bundleID []byte) ([]byte, error
|
|||
}
|
||||
}
|
||||
|
||||
// RatchetInfoConfirmed clears the ephemeral key in the RatchetInfo
|
||||
// associated with the specified bundle ID and interlocutor identity public key
|
||||
// MarkBundleExpired expires any private bundle for a given identity
|
||||
func (s *SQLLitePersistence) MarkBundleExpired(identity []byte) error {
|
||||
stmt, err := s.db.Prepare("UPDATE bundles SET expired = 1 WHERE identity = ?")
|
||||
stmt, err := s.db.Prepare("UPDATE bundles SET expired = 1 WHERE identity = ? AND private_key IS NOT NULL")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -298,16 +306,29 @@ 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) (*Bundle, error) {
|
||||
func (s *SQLLitePersistence) GetPublicBundle(publicKey *ecdsa.PublicKey, installationIDs []string) (*Bundle, error) {
|
||||
|
||||
if len(installationIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
identity := crypto.CompressPubkey(publicKey)
|
||||
stmt, err := s.db.Prepare("SELECT signed_pre_key,installation_id, version FROM bundles WHERE expired = 0 AND identity = ? ORDER BY version DESC")
|
||||
|
||||
/* #nosec */
|
||||
statement := "SELECT signed_pre_key,installation_id, version FROM bundles WHERE expired = 0 AND identity = ? AND installation_id IN (?" + strings.Repeat(",?", len(installationIDs)-1) + ") ORDER BY version DESC"
|
||||
stmt, err := s.db.Prepare(statement)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
rows, err := stmt.Query(identity)
|
||||
args := make([]interface{}, len(installationIDs)+1)
|
||||
args[0] = identity
|
||||
for i, installationID := range installationIDs {
|
||||
args[i+1] = installationID
|
||||
}
|
||||
|
||||
rows, err := stmt.Query(args...)
|
||||
rowCount := 0
|
||||
|
||||
if err != nil {
|
||||
|
@ -687,6 +708,126 @@ func (s *SQLLiteSessionStorage) Load(id []byte) (*dr.State, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// GetActiveInstallations returns the active installations for a given identity
|
||||
func (s *SQLLitePersistence) GetActiveInstallations(maxInstallations uint, identity []byte) ([]string, error) {
|
||||
stmt, err := s.db.Prepare("SELECT installation_id FROM installations WHERE enabled = 1 AND identity = ? ORDER BY timestamp DESC LIMIT ?")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var installations []string
|
||||
rows, err := stmt.Query(identity, maxInstallations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var installationID string
|
||||
err = rows.Scan(
|
||||
&installationID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
installations = append(installations, installationID)
|
||||
}
|
||||
|
||||
return installations, nil
|
||||
|
||||
}
|
||||
|
||||
// AddInstallations adds the installations for a given identity, maintaining the enabled flag
|
||||
func (s *SQLLitePersistence) AddInstallations(identity []byte, timestamp int64, installationIDs []string, defaultEnabled bool) error {
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, installationID := range installationIDs {
|
||||
stmt, err := tx.Prepare("SELECT enabled FROM installations WHERE identity = ? AND installation_id = ? LIMIT 1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
var oldEnabled bool
|
||||
|
||||
err = stmt.QueryRow(identity, installationID).Scan(&oldEnabled)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
|
||||
// We update timestamp if present without changing enabled
|
||||
if err != sql.ErrNoRows {
|
||||
stmt, err = tx.Prepare("UPDATE installations SET timestamp = ?, enabled = ? WHERE identity = ? AND installation_id = ?")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(
|
||||
timestamp,
|
||||
oldEnabled,
|
||||
identity,
|
||||
installationID,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
} else {
|
||||
stmt, err = tx.Prepare("INSERT INTO installations(identity, installation_id, timestamp, enabled) VALUES (?, ?, ?, ?)")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(
|
||||
identity,
|
||||
installationID,
|
||||
timestamp,
|
||||
defaultEnabled,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// EnableInstallation enables the installation
|
||||
func (s *SQLLitePersistence) EnableInstallation(identity []byte, installationID string) error {
|
||||
stmt, err := s.db.Prepare("UPDATE installations SET enabled = 1 WHERE identity = ? AND installation_id = ?")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(identity, installationID)
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
// DisableInstallation disable the installation
|
||||
func (s *SQLLitePersistence) DisableInstallation(identity []byte, installationID string) error {
|
||||
|
||||
stmt, err := s.db.Prepare("UPDATE installations SET enabled = 0 WHERE identity = ? AND installation_id = ?")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(identity, installationID)
|
||||
return err
|
||||
}
|
||||
|
||||
func toKey(a []byte) dr.Key {
|
||||
var k [32]byte
|
||||
copy(k[:], a)
|
||||
|
|
|
@ -54,7 +54,7 @@ func (s *SQLLitePersistenceTestSuite) TestPrivateBundle() {
|
|||
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"))
|
||||
anyPrivateBundle, err := s.service.GetAnyPrivateBundle([]byte("non-existing-id"), []string{installationID})
|
||||
s.Require().NoError(err)
|
||||
s.Nil(anyPrivateBundle)
|
||||
|
||||
|
@ -71,7 +71,7 @@ func (s *SQLLitePersistenceTestSuite) TestPrivateBundle() {
|
|||
s.Equal(bundle.GetPrivateSignedPreKey(), actualKey, "It returns the same key")
|
||||
|
||||
identity := crypto.CompressPubkey(&key.PublicKey)
|
||||
anyPrivateBundle, err = s.service.GetAnyPrivateBundle(identity)
|
||||
anyPrivateBundle, err = s.service.GetAnyPrivateBundle(identity, []string{installationID})
|
||||
s.Require().NoError(err)
|
||||
s.NotNil(anyPrivateBundle)
|
||||
s.True(proto.Equal(bundle.GetBundle(), anyPrivateBundle.GetBundle()), "It returns the same bundle")
|
||||
|
@ -81,7 +81,7 @@ func (s *SQLLitePersistenceTestSuite) TestPublicBundle() {
|
|||
key, err := crypto.GenerateKey()
|
||||
s.Require().NoError(err)
|
||||
|
||||
actualBundle, err := s.service.GetPublicBundle(&key.PublicKey)
|
||||
actualBundle, err := s.service.GetPublicBundle(&key.PublicKey, []string{"1"})
|
||||
s.Require().NoError(err, "Error was not returned even though bundle is not there")
|
||||
s.Nil(actualBundle)
|
||||
|
||||
|
@ -92,7 +92,7 @@ func (s *SQLLitePersistenceTestSuite) TestPublicBundle() {
|
|||
err = s.service.AddPublicBundle(bundle)
|
||||
s.Require().NoError(err)
|
||||
|
||||
actualBundle, err = s.service.GetPublicBundle(&key.PublicKey)
|
||||
actualBundle, err = s.service.GetPublicBundle(&key.PublicKey, []string{"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")
|
||||
|
@ -102,7 +102,7 @@ func (s *SQLLitePersistenceTestSuite) TestUpdatedBundle() {
|
|||
key, err := crypto.GenerateKey()
|
||||
s.Require().NoError(err)
|
||||
|
||||
actualBundle, err := s.service.GetPublicBundle(&key.PublicKey)
|
||||
actualBundle, err := s.service.GetPublicBundle(&key.PublicKey, []string{"1"})
|
||||
s.Require().NoError(err, "Error was not returned even though bundle is not there")
|
||||
s.Nil(actualBundle)
|
||||
|
||||
|
@ -124,7 +124,7 @@ func (s *SQLLitePersistenceTestSuite) TestUpdatedBundle() {
|
|||
err = s.service.AddPublicBundle(bundle)
|
||||
s.Require().NoError(err)
|
||||
|
||||
actualBundle, err = s.service.GetPublicBundle(&key.PublicKey)
|
||||
actualBundle, err = s.service.GetPublicBundle(&key.PublicKey, []string{"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")
|
||||
|
@ -134,7 +134,7 @@ func (s *SQLLitePersistenceTestSuite) TestOutOfOrderBundles() {
|
|||
key, err := crypto.GenerateKey()
|
||||
s.Require().NoError(err)
|
||||
|
||||
actualBundle, err := s.service.GetPublicBundle(&key.PublicKey)
|
||||
actualBundle, err := s.service.GetPublicBundle(&key.PublicKey, []string{"1"})
|
||||
s.Require().NoError(err, "Error was not returned even though bundle is not there")
|
||||
s.Nil(actualBundle)
|
||||
|
||||
|
@ -161,7 +161,7 @@ func (s *SQLLitePersistenceTestSuite) TestOutOfOrderBundles() {
|
|||
err = s.service.AddPublicBundle(bundle1)
|
||||
s.Require().NoError(err)
|
||||
|
||||
actualBundle, err = s.service.GetPublicBundle(&key.PublicKey)
|
||||
actualBundle, err = s.service.GetPublicBundle(&key.PublicKey, []string{"1"})
|
||||
s.Require().NoError(err)
|
||||
s.Equal(bundle2.GetIdentity(), actualBundle.GetIdentity(), "It sets the right identity")
|
||||
s.Equal(bundle2.GetSignedPreKeys()["1"].GetVersion(), uint32(1))
|
||||
|
@ -172,7 +172,7 @@ func (s *SQLLitePersistenceTestSuite) TestMultiplePublicBundle() {
|
|||
key, err := crypto.GenerateKey()
|
||||
s.Require().NoError(err)
|
||||
|
||||
actualBundle, err := s.service.GetPublicBundle(&key.PublicKey)
|
||||
actualBundle, err := s.service.GetPublicBundle(&key.PublicKey, []string{"1"})
|
||||
s.Require().NoError(err, "Error was not returned even though bundle is not there")
|
||||
s.Nil(actualBundle)
|
||||
|
||||
|
@ -198,7 +198,7 @@ func (s *SQLLitePersistenceTestSuite) TestMultiplePublicBundle() {
|
|||
s.Require().NoError(err)
|
||||
|
||||
// Returns the most recent bundle
|
||||
actualBundle, err = s.service.GetPublicBundle(&key.PublicKey)
|
||||
actualBundle, err = s.service.GetPublicBundle(&key.PublicKey, []string{"1"})
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Equal(bundle.GetIdentity(), actualBundle.GetIdentity(), "It sets the identity")
|
||||
|
@ -210,7 +210,7 @@ func (s *SQLLitePersistenceTestSuite) TestMultiDevicePublicBundle() {
|
|||
key, err := crypto.GenerateKey()
|
||||
s.Require().NoError(err)
|
||||
|
||||
actualBundle, err := s.service.GetPublicBundle(&key.PublicKey)
|
||||
actualBundle, err := s.service.GetPublicBundle(&key.PublicKey, []string{"1"})
|
||||
s.Require().NoError(err, "Error was not returned even though bundle is not there")
|
||||
s.Nil(actualBundle)
|
||||
|
||||
|
@ -234,7 +234,7 @@ func (s *SQLLitePersistenceTestSuite) TestMultiDevicePublicBundle() {
|
|||
s.Require().NoError(err)
|
||||
|
||||
// Returns the most recent bundle
|
||||
actualBundle, err = s.service.GetPublicBundle(&key.PublicKey)
|
||||
actualBundle, err = s.service.GetPublicBundle(&key.PublicKey, []string{"1", "2"})
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Equal(bundle.GetIdentity(), actualBundle.GetIdentity(), "It sets the identity")
|
||||
|
@ -344,4 +344,137 @@ func (s *SQLLitePersistenceTestSuite) TestRatchetInfoNoBundle() {
|
|||
s.Nil(ratchetInfo, "It returns nil when no bundle is there")
|
||||
}
|
||||
|
||||
func (s *SQLLitePersistenceTestSuite) TestAddInstallations() {
|
||||
identity := []byte("alice")
|
||||
installations := []string{"alice-1", "alice-2"}
|
||||
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)
|
||||
}
|
||||
|
||||
func (s *SQLLitePersistenceTestSuite) TestAddInstallationsLimit() {
|
||||
identity := []byte("alice")
|
||||
|
||||
installations := []string{"alice-1", "alice-2"}
|
||||
err := s.service.AddInstallations(
|
||||
identity,
|
||||
1,
|
||||
installations,
|
||||
true,
|
||||
)
|
||||
s.Require().NoError(err)
|
||||
|
||||
installations = []string{"alice-2", "alice-3"}
|
||||
err = s.service.AddInstallations(
|
||||
identity,
|
||||
2,
|
||||
installations,
|
||||
true,
|
||||
)
|
||||
s.Require().NoError(err)
|
||||
|
||||
installations = []string{"alice-2", "alice-3", "alice-4"}
|
||||
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([]string{"alice-2", "alice-3", "alice-4"}, enabledInstallations)
|
||||
}
|
||||
|
||||
func (s *SQLLitePersistenceTestSuite) TestAddInstallationsDisabled() {
|
||||
identity := []byte("alice")
|
||||
|
||||
installations := []string{"alice-1", "alice-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 := []string{"alice-1", "alice-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 = []string{"alice-1", "alice-2"}
|
||||
err = s.service.AddInstallations(
|
||||
identity,
|
||||
1,
|
||||
installations,
|
||||
true,
|
||||
)
|
||||
s.Require().NoError(err)
|
||||
|
||||
actualInstallations, err := s.service.GetActiveInstallations(3, identity)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Require().Equal([]string{"alice-2"}, actualInstallations)
|
||||
}
|
||||
|
||||
func (s *SQLLitePersistenceTestSuite) TestEnableInstallation() {
|
||||
identity := []byte("alice")
|
||||
|
||||
installations := []string{"alice-1", "alice-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)
|
||||
|
||||
s.Require().Equal([]string{"alice-2"}, actualInstallations)
|
||||
|
||||
err = s.service.EnableInstallation(identity, "alice-1")
|
||||
s.Require().NoError(err)
|
||||
|
||||
actualInstallations, err = s.service.GetActiveInstallations(3, identity)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Require().Equal([]string{"alice-1", "alice-2"}, actualInstallations)
|
||||
|
||||
}
|
||||
|
||||
// TODO: Add test for MarkBundleExpired
|
||||
|
|
|
@ -6,6 +6,8 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/crypto/ecies"
|
||||
|
@ -43,9 +45,12 @@ func FromBase64(str string) (*Bundle, error) {
|
|||
return bundle, nil
|
||||
}
|
||||
|
||||
func buildSignatureMaterial(signedPreKeys *map[string]*SignedPreKey) []byte {
|
||||
func buildSignatureMaterial(bundle *Bundle) []byte {
|
||||
signedPreKeys := bundle.GetSignedPreKeys()
|
||||
timestamp := bundle.GetTimestamp()
|
||||
var keys []string
|
||||
for k := range *signedPreKeys {
|
||||
|
||||
for k := range signedPreKeys {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
var signatureMaterial []byte
|
||||
|
@ -53,18 +58,23 @@ func buildSignatureMaterial(signedPreKeys *map[string]*SignedPreKey) []byte {
|
|||
sort.Strings(keys)
|
||||
|
||||
for _, installationID := range keys {
|
||||
signedPreKey := (*signedPreKeys)[installationID]
|
||||
signedPreKey := signedPreKeys[installationID]
|
||||
signatureMaterial = append(signatureMaterial, []byte(installationID)...)
|
||||
signatureMaterial = append(signatureMaterial, signedPreKey.SignedPreKey...)
|
||||
signatureMaterial = append(signatureMaterial, []byte(fmt.Sprint(signedPreKey.Version))...)
|
||||
signatureMaterial = append(signatureMaterial, []byte(strconv.FormatUint(uint64(signedPreKey.Version), 10))...)
|
||||
// We don't use timestamp in the signature if it's 0, for backward compatibility
|
||||
}
|
||||
|
||||
if timestamp != 0 {
|
||||
signatureMaterial = append(signatureMaterial, []byte(strconv.FormatInt(timestamp, 10))...)
|
||||
}
|
||||
|
||||
return signatureMaterial
|
||||
|
||||
}
|
||||
|
||||
func SignBundle(identity *ecdsa.PrivateKey, bundleContainer *BundleContainer) error {
|
||||
signedPreKeys := bundleContainer.GetBundle().GetSignedPreKeys()
|
||||
signatureMaterial := buildSignatureMaterial(&signedPreKeys)
|
||||
signatureMaterial := buildSignatureMaterial(bundleContainer.GetBundle())
|
||||
|
||||
signature, err := crypto.Sign(crypto.Keccak256(signatureMaterial), identity)
|
||||
if err != nil {
|
||||
|
@ -89,6 +99,7 @@ func NewBundleContainer(identity *ecdsa.PrivateKey, installationID string) (*Bun
|
|||
signedPreKeys[installationID] = &SignedPreKey{SignedPreKey: compressedPreKey}
|
||||
|
||||
bundle := Bundle{
|
||||
Timestamp: time.Now().UnixNano(),
|
||||
Identity: compressedIdentityKey,
|
||||
SignedPreKeys: signedPreKeys,
|
||||
}
|
||||
|
@ -112,8 +123,7 @@ func ExtractIdentity(bundle *Bundle) (string, error) {
|
|||
return "", err
|
||||
}
|
||||
|
||||
signedPreKeys := bundle.GetSignedPreKeys()
|
||||
signatureMaterial := buildSignatureMaterial(&signedPreKeys)
|
||||
signatureMaterial := buildSignatureMaterial(bundle)
|
||||
|
||||
recoveredKey, err := crypto.SigToPub(
|
||||
crypto.Keccak256(signatureMaterial),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package chat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
|
@ -65,6 +66,7 @@ func TestNewBundleContainer(t *testing.T) {
|
|||
|
||||
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,
|
||||
|
@ -102,6 +104,7 @@ func TestSignBundle(t *testing.T) {
|
|||
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),
|
||||
|
|
|
@ -122,6 +122,24 @@ func (s *Service) GetBundle(myIdentityKey *ecdsa.PrivateKey) (*chat.Bundle, erro
|
|||
return s.protocol.GetBundle(myIdentityKey)
|
||||
}
|
||||
|
||||
// EnableInstallation enables an installation for multi-device sync.
|
||||
func (s *Service) EnableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error {
|
||||
if s.protocol == nil {
|
||||
return errProtocolNotInitialized
|
||||
}
|
||||
|
||||
return s.protocol.EnableInstallation(myIdentityKey, installationID)
|
||||
}
|
||||
|
||||
// DisableInstallation disables an installation for multi-device sync.
|
||||
func (s *Service) DisableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error {
|
||||
if s.protocol == nil {
|
||||
return errProtocolNotInitialized
|
||||
}
|
||||
|
||||
return s.protocol.DisableInstallation(myIdentityKey, installationID)
|
||||
}
|
||||
|
||||
// APIs returns a list of new APIs.
|
||||
func (s *Service) APIs() []rpc.API {
|
||||
apis := []rpc.API{
|
||||
|
|
|
@ -88,7 +88,7 @@ func ConfigCliFleetEthBetaJson() (*asset, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
info := bindataFileInfo{name: "../config/cli/fleet-eth.beta.json", size: 3237, mode: os.FileMode(420), modTime: time.Unix(1539606161, 0)}
|
||||
info := bindataFileInfo{name: "../config/cli/fleet-eth.beta.json", size: 3237, mode: os.FileMode(420), modTime: time.Unix(1541148771, 0)}
|
||||
a := &asset{bytes: bytes, info: info}
|
||||
return a, nil
|
||||
}
|
||||
|
@ -108,7 +108,7 @@ func ConfigCliFleetEthStagingJson() (*asset, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
info := bindataFileInfo{name: "../config/cli/fleet-eth.staging.json", size: 1838, mode: os.FileMode(420), modTime: time.Unix(1539606161, 0)}
|
||||
info := bindataFileInfo{name: "../config/cli/fleet-eth.staging.json", size: 1838, mode: os.FileMode(420), modTime: time.Unix(1541148771, 0)}
|
||||
a := &asset{bytes: bytes, info: info}
|
||||
return a, nil
|
||||
}
|
||||
|
@ -128,7 +128,7 @@ func ConfigCliFleetEthTestJson() (*asset, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
info := bindataFileInfo{name: "../config/cli/fleet-eth.test.json", size: 1519, mode: os.FileMode(420), modTime: time.Unix(1539606161, 0)}
|
||||
info := bindataFileInfo{name: "../config/cli/fleet-eth.test.json", size: 1519, mode: os.FileMode(420), modTime: time.Unix(1541148771, 0)}
|
||||
a := &asset{bytes: bytes, info: info}
|
||||
return a, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
DROP TABLE installations;
|
|
@ -0,0 +1,7 @@
|
|||
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
|
||||
);
|
Loading…
Reference in New Issue