Compare commits
32 Commits
52ae52ca9f
...
alpha0
Author | SHA1 | Date | |
---|---|---|---|
6a42d261f1 | |||
1dda1b27a8 | |||
535181d669 | |||
c3ad8d5d2d | |||
4cd928fdc6 | |||
420b7d0af0 | |||
432f449558 | |||
65f9ee2e07 | |||
eeb39338e2 | |||
c203431ede | |||
04a390d558 | |||
08ff9d58b8 | |||
cd41a59518 | |||
4750915b49 | |||
e0faaf8cef | |||
539e2c528e | |||
d082724432 | |||
5eb06ebcc5 | |||
753cd30f38 | |||
4a009b69eb | |||
9d5ba42dfc | |||
71df3a792b | |||
d748735ded | |||
698740e20a | |||
940a8d395c | |||
6f2a65dac9 | |||
d9155bac51 | |||
0f8bdf5f66 | |||
ce758c5bb1 | |||
c4bb8c5693 | |||
61958593a1 | |||
5ac92ce3a8 |
11
.drone.yml
Normal file
11
.drone.yml
Normal file
@ -0,0 +1,11 @@
|
||||
type: docker
|
||||
kind: pipeline
|
||||
name: unit
|
||||
|
||||
|
||||
steps:
|
||||
- name: test
|
||||
image: golang
|
||||
commands:
|
||||
- go test
|
||||
- go build
|
13
.gitignore
vendored
13
.gitignore
vendored
@ -1,9 +1,10 @@
|
||||
doc/protocol.aux
|
||||
doc/protocol.fdb_latexmk
|
||||
doc/protocol.fls
|
||||
doc/protocol.log
|
||||
doc/protocol.pdf
|
||||
doc/protocol.synctex.gz
|
||||
doc/*.aux
|
||||
doc/*.fdb_latexmk
|
||||
doc/*.fls
|
||||
doc/*.log
|
||||
doc/*.pdf
|
||||
doc/*.synctex.gz
|
||||
doc/generated
|
||||
out/doc/general_deployment/general_deployment.png
|
||||
out/doc/server_deployment/server_deployment.png
|
||||
*.json
|
||||
|
24
README.md
24
README.md
@ -1,9 +1,21 @@
|
||||
# MEOW lib
|
||||
|
||||
## Go Mobile setup
|
||||
go install golang.org/x/mobile/cmd/gobind@latest
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
go get golang.org/x/mobile/bind
|
||||
Replace "Get" by "get" in messages.pb.go
|
||||
# Documentation generation
|
||||
## required tools
|
||||
* protoc
|
||||
* protoc-gen-doc
|
||||
* protoc-gen-uml
|
||||
* go-plantuml
|
||||
* plantuml (plantuml command is as shell script : `java -jar /<path-to-plantuml>/plantuml-mit-<version>.jar "$@"`)
|
||||
## generation
|
||||
run the shell scripts
|
||||
|
||||
cd pb
|
||||
./protogen.sh
|
||||
|
||||
cd doc
|
||||
./docgen.sh
|
||||
|
||||
# Tests
|
||||
|
||||
|
||||
gomobile bind -target android -androidapi=19
|
@ -4,7 +4,6 @@ import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -122,6 +121,7 @@ func TestFlutterCompat(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
func TestFlutterDecode(t *testing.T) {
|
||||
pub, err := os.ReadFile("/home/yves/Documents/code/flutter/meowlib/pub.key")
|
||||
if err != nil {
|
||||
@ -179,3 +179,4 @@ func TestFlutterEncode(t *testing.T) {
|
||||
println(base64.StdEncoding.EncodeToString(pub))
|
||||
|
||||
}
|
||||
*/
|
||||
|
@ -11,27 +11,40 @@ type Config struct {
|
||||
SavePassword bool `json:"save_password,omitempty"`
|
||||
SavedPassword string `json:"saved_password,omitempty"`
|
||||
// Technical
|
||||
IdentityFile string `json:"identity_file,omitempty"`
|
||||
StoragePath string `json:"storage_path,omitempty"`
|
||||
MaxIdsPerUser int `json:"max_ids_per_user,omitempty"`
|
||||
MsgDbRollingPeriod int `json:"msg_db_rolling_period,omitempty"`
|
||||
Chunksize int64 `json:"chunksize,omitempty"`
|
||||
ServerPollInterval int `json:"server_poll_interval,omitempty"`
|
||||
DbSize int `json:"db_size,omitempty"`
|
||||
UserAgent string `json:"user_agent,omitempty"`
|
||||
// GUI
|
||||
LastOpenChat string `json:"last_open_chat,omitempty"`
|
||||
SoundNotification bool `json:"sound_notification,omitempty"`
|
||||
SoundNotificationEnable bool `json:"sound_notification_enable,omitempty"`
|
||||
DefaultNotificationSound string `json:"default_notification_sound,omitempty"`
|
||||
NotificationVibe string `json:"notification_vibe,omitempty"`
|
||||
NotificationVibeEnable bool `json:"notification_vibe_enable,omitempty"`
|
||||
DefaultNotificationVibe string `json:"default_notification_vibe,omitempty"`
|
||||
NotificationLED string `json:"notification_led,omitempty"`
|
||||
NotificationLEDEnable bool `json:"notification_led_enable,omitempty"`
|
||||
DefaultNotificationLEDColor string `json:"default_notification_led_color,omitempty"`
|
||||
VisualNotification bool `json:"visual_notification,omitempty"`
|
||||
VisualNotifiactionModes string `json:"visual_notifiaction_modes,omitempty"`
|
||||
PrivateChatNotifiactions bool `json:"private_chat_notifiactions,omitempty"`
|
||||
GroupChatNotifiactions bool `json:"group_chat_notifiactions,omitempty"`
|
||||
ChannelNotifications bool `json:"channel_notifications,omitempty"`
|
||||
VisualNotificationEnable bool `json:"visual_notification_enable,omitempty"`
|
||||
VisualNotificationModes string `json:"visual_notifiaction_modes,omitempty"`
|
||||
PrivateChatNotificationsEnable bool `json:"private_chat_notifiactions_enable,omitempty"`
|
||||
GroupChatNotificationsEnable bool `json:"group_chat_notifiactions_enable,omitempty"`
|
||||
ChannelNotificationsEnable bool `json:"channel_notifications_enable,omitempty"`
|
||||
Language string `json:"language,omitempty"`
|
||||
Theme string `json:"theme,omitempty"`
|
||||
FingerprintEnable bool `json:"fingerprint_enable,omitempty"`
|
||||
ShowFavoriteContacts bool `json:"show_favorite_contacts,omitempty"`
|
||||
NightModeEnable bool `json:"night_mode_enable,omitempty"`
|
||||
|
||||
// Debug
|
||||
DbSuffix string `json:"db_suffix,omitempty"`
|
||||
|
||||
// Inner
|
||||
memoryPassword string
|
||||
identityFile string
|
||||
memoryPassword string `json:"memory_password,omitempty"`
|
||||
additionalPasswords []string `json:"additional_passwords,omitempty"`
|
||||
me *Identity `json:"me,omitempty"`
|
||||
}
|
||||
|
||||
var instance *Config
|
||||
@ -75,3 +88,19 @@ func (c *Config) SetMemPass(pass string) {
|
||||
func (c *Config) GetMemPass() string {
|
||||
return c.memoryPassword
|
||||
}
|
||||
|
||||
func (c *Config) GetIdentity() *Identity {
|
||||
return c.me
|
||||
}
|
||||
|
||||
func (c *Config) SetIdentity(id *Identity) {
|
||||
c.me = id
|
||||
}
|
||||
|
||||
func (c *Config) SaveIdentity() error {
|
||||
return c.me.Save()
|
||||
}
|
||||
|
||||
func (c *Config) Clean() {
|
||||
c.additionalPasswords = []string{}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
func TestConfigSave(t *testing.T) {
|
||||
c := GetConfig()
|
||||
c.memoryPassword = "hideme"
|
||||
c.IdentityFile = "test.id"
|
||||
c.Chunksize = 10000000
|
||||
c.SavePassword = true
|
||||
c.SavedPassword = "stupid"
|
||||
|
@ -3,39 +3,47 @@ package client
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math/rand"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"github.com/ProtonMail/gopenpgp/v2/helper"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const maxHiddenCount = 30
|
||||
|
||||
type Identity struct {
|
||||
Nickname string `json:"nickname,omitempty"`
|
||||
DefaultAvatarUuid string `json:"default_avatar_uuid,omitempty"`
|
||||
RootKp meowlib.KeyPair `json:"id_kp,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Peers PeerList `json:"peers,omitempty"`
|
||||
HiddenPeers [][]byte `json:"hiddend_peers,omitempty"`
|
||||
Device meowlib.KeyPair `json:"device,omitempty"`
|
||||
KnownServers InternalServerList `json:"known_servers,omitempty"`
|
||||
MessageServers InternalServerList `json:"message_servers,omitempty"`
|
||||
ArchiveServers InternalServerList `json:"archive_servers,omitempty"`
|
||||
OwnedServers InternalServerList `json:"owned_servers,omitempty"`
|
||||
KnownServers ServerList `json:"known_servers,omitempty"`
|
||||
MessageServers ServerList `json:"message_servers,omitempty"`
|
||||
DefaultDbPassword string `json:"default_db_password,omitempty"`
|
||||
DbPasswordStore bool `json:"db_password_store,omitempty"`
|
||||
OwnedDevices PeerList `json:"owned_devices,omitempty"`
|
||||
StaticMtkServerPaths []InternalServerList `json:"static_mtk_server_paths,omitempty"`
|
||||
StaticMtkServerPaths []ServerList `json:"static_mtk_server_paths,omitempty"`
|
||||
DynamicMtkServeRules []string `json:"dynamic_mtk_serve_rules,omitempty"`
|
||||
InvitationTimeout int `json:"invitation_timeout,omitempty"`
|
||||
unlockedHiddenPeers PeerList
|
||||
}
|
||||
|
||||
func CreateIdentity(nickname string) *Identity {
|
||||
var id Identity
|
||||
id.Nickname = nickname
|
||||
id.RootKp = meowlib.NewKeyPair()
|
||||
GetConfig().me = &id
|
||||
id.generateRandomHiddenStuff()
|
||||
return &id
|
||||
}
|
||||
|
||||
func (id *Identity) InvitePeer(MyName string, ContactName string, MessageServerIdxs []int) (*meowlib.ContactCard, error) {
|
||||
// Creates an invitation for a peer, returns the peer containing
|
||||
func (id *Identity) InvitePeer(MyName string, ContactName string, MessageServerIdxs []int) (*Peer, *meowlib.ContactCard, error) {
|
||||
var peer Peer
|
||||
var myContactCard meowlib.ContactCard
|
||||
peer.MyIdentity = meowlib.NewKeyPair()
|
||||
@ -44,11 +52,11 @@ func (id *Identity) InvitePeer(MyName string, ContactName string, MessageServerI
|
||||
peer.Name = ContactName
|
||||
peer.InvitationId = uuid.New().String()
|
||||
if id.MessageServers.Servers == nil {
|
||||
return nil, errors.New("no message servers defined in your identity")
|
||||
return nil, nil, errors.New("no message servers defined in your identity")
|
||||
}
|
||||
for _, i := range MessageServerIdxs {
|
||||
if i > len(id.MessageServers.Servers)-1 {
|
||||
return nil, errors.New("requested server out of range of defined message servers")
|
||||
return nil, nil, errors.New("requested server out of range of defined message servers")
|
||||
}
|
||||
}
|
||||
for _, i := range MessageServerIdxs {
|
||||
@ -62,7 +70,7 @@ func (id *Identity) InvitePeer(MyName string, ContactName string, MessageServerI
|
||||
myContactCard.InvitationId = peer.InvitationId
|
||||
id.Peers = append(id.Peers, peer)
|
||||
|
||||
return &myContactCard, nil
|
||||
return &peer, &myContactCard, nil
|
||||
}
|
||||
|
||||
func (id *Identity) CheckInvitation(ReceivedContact *meowlib.ContactCard) (isAnswer bool, proposedNick string, receivedNick string) {
|
||||
@ -114,7 +122,7 @@ func (id *Identity) FinalizeInvitation(ReceivedContact *meowlib.ContactCard) err
|
||||
func LoadIdentity(filename string, password string) (*Identity, error) {
|
||||
var id Identity
|
||||
GetConfig().memoryPassword = password
|
||||
GetConfig().identityFile = filename
|
||||
GetConfig().IdentityFile = filename
|
||||
indata, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -124,15 +132,91 @@ func LoadIdentity(filename string, password string) (*Identity, error) {
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal([]byte(pass), &id)
|
||||
GetConfig().me = &id
|
||||
return &id, err
|
||||
}
|
||||
|
||||
func (id *Identity) Save() error {
|
||||
if GetConfig().IdentityFile == "" {
|
||||
return errors.New("identity filename empty")
|
||||
}
|
||||
b, _ := json.Marshal(id)
|
||||
armor, err := helper.EncryptMessageWithPassword([]byte(GetConfig().memoryPassword), string(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(GetConfig().identityFile, []byte(armor), 0600)
|
||||
err = os.WriteFile(GetConfig().IdentityFile, []byte(armor), 0600)
|
||||
return err
|
||||
}
|
||||
|
||||
func (id *Identity) TryUnlockHidden(password string) error {
|
||||
found := false
|
||||
for _, encPeer := range id.HiddenPeers {
|
||||
p := Peer{}
|
||||
jsonPeer, err := meowlib.SymDecrypt(password, encPeer)
|
||||
if err == nil {
|
||||
err = json.Unmarshal(jsonPeer, &p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.dbPassword = password
|
||||
id.unlockedHiddenPeers = append(id.unlockedHiddenPeers, p)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if found {
|
||||
return nil
|
||||
}
|
||||
return errors.New("no peer found")
|
||||
}
|
||||
|
||||
func (id *Identity) HidePeer(peerIdx int, password string) error {
|
||||
serializedPeer, err := json.Marshal(id.Peers[peerIdx])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
encrypted, err := meowlib.SymEncrypt(password, serializedPeer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// add encrypted peer data
|
||||
id.HiddenPeers = append(id.HiddenPeers, encrypted)
|
||||
// remove clear text peer
|
||||
id.Peers = append(id.Peers[:peerIdx], id.Peers[peerIdx+1:]...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (id *Identity) generateRandomHiddenStuff() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
count := rand.Intn(maxHiddenCount) + 1
|
||||
for i := 1; i < count; i++ {
|
||||
var p Peer
|
||||
p.Name = randomLenString(4, 20)
|
||||
p.MyEncryptionKp = meowlib.NewKeyPair()
|
||||
p.MyIdentity = meowlib.NewKeyPair()
|
||||
p.MyLookupKp = meowlib.NewKeyPair()
|
||||
p.Contact.Name = randomLenString(4, 20)
|
||||
p.Contact.ContactPublicKey = p.MyLookupKp.Public
|
||||
p.Contact.EncryptionPublicKey = p.MyIdentity.Public
|
||||
p.Contact.LookupPublicKey = p.MyEncryptionKp.Public
|
||||
p.Contact.AddUrls([]string{randomLenString(14, 60), randomLenString(14, 60)})
|
||||
id.Peers = append(id.Peers, p)
|
||||
id.HidePeer(0, randomLenString(8, 14))
|
||||
// TODO Add conversations
|
||||
}
|
||||
}
|
||||
|
||||
func randomLenString(min int, max int) string {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
n := rand.Intn(max-min) + min
|
||||
return randomString(n)
|
||||
}
|
||||
|
||||
func randomString(n int) string {
|
||||
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||
s := make([]rune, n)
|
||||
for i := range s {
|
||||
s[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
return string(s)
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@ -16,6 +17,31 @@ func exists(filename string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func createId() *Identity {
|
||||
config := GetConfig()
|
||||
config.IdentityFile = "test.id"
|
||||
config.memoryPassword = "generalPassword"
|
||||
// ! Extension to quickly open db : Debug only !
|
||||
config.DbSuffix = ".sqlite"
|
||||
id := CreateIdentity("myname")
|
||||
err := id.Save()
|
||||
if err != nil {
|
||||
log.Fatal("Save failed")
|
||||
}
|
||||
var p Peer
|
||||
p.Name = "testName"
|
||||
p.MyEncryptionKp = meowlib.NewKeyPair()
|
||||
p.MyIdentity = meowlib.NewKeyPair()
|
||||
p.MyLookupKp = meowlib.NewKeyPair()
|
||||
p.Contact.Name = "foo"
|
||||
p.Contact.ContactPublicKey = p.MyLookupKp.Public
|
||||
p.Contact.EncryptionPublicKey = p.MyIdentity.Public
|
||||
p.Contact.LookupPublicKey = p.MyEncryptionKp.Public
|
||||
p.Contact.AddUrls([]string{"http:/127.0.0.1/meow", "tcp://localhost:1234"})
|
||||
id.Peers = append(id.Peers, p)
|
||||
return id
|
||||
}
|
||||
|
||||
func TestLoad(t *testing.T) {
|
||||
if exists("test.id") {
|
||||
os.Remove("test.id")
|
||||
@ -24,6 +50,8 @@ func TestLoad(t *testing.T) {
|
||||
if err != nil {
|
||||
id := CreateIdentity("myname")
|
||||
id.Save()
|
||||
} else {
|
||||
log.Println(id.Nickname)
|
||||
}
|
||||
id, err = LoadIdentity("test.id", "toto")
|
||||
if err != nil {
|
||||
@ -36,6 +64,18 @@ func TestLoad(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreate(t *testing.T) {
|
||||
|
||||
func TestHidePeer(t *testing.T) {
|
||||
id := createId()
|
||||
name := id.Peers[0].Name
|
||||
assert.Equal(t, len(id.Peers), 1)
|
||||
h := len(id.HiddenPeers)
|
||||
id.HidePeer(0, "mypassword")
|
||||
assert.Equal(t, len(id.Peers), 0)
|
||||
assert.Equal(t, len(id.HiddenPeers), h+1)
|
||||
id.TryUnlockHidden("mypassword")
|
||||
assert.Equal(t, len(id.unlockedHiddenPeers), 1)
|
||||
assert.Equal(t, id.unlockedHiddenPeers[0].Name, name)
|
||||
if exists("test.id") {
|
||||
os.Remove("test.id")
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func ProcessForOutput(usermessage *meowlib.UserMessage, peer *Peer, servers *InternalServerList, trackingLookupKey string) ([]byte, error) {
|
||||
func ProcessForOutput(usermessage *meowlib.UserMessage, peer *Peer, servers *ServerList, trackingLookupKey string) ([]byte, error) {
|
||||
lastIdx := len(servers.Servers) - 1
|
||||
// LAST SERVER : Message delivery as usual
|
||||
srv := &servers.Servers[lastIdx]
|
||||
|
@ -1,6 +1,13 @@
|
||||
package client
|
||||
|
||||
func ProcessOutboundTextMessage(peer *Peer, text string, srv *InternalServer) ([]byte, error) {
|
||||
import "forge.redroom.link/yves/meowlib"
|
||||
|
||||
type InternalUserMessage struct {
|
||||
message *meowlib.UserMessage
|
||||
dbid int64
|
||||
}
|
||||
|
||||
func ProcessOutboundTextMessage(peer *Peer, text string, srv *Server) ([]byte, error) {
|
||||
// Creating User message
|
||||
usermessage, err := peer.BuildSimpleUserMessage([]byte(text))
|
||||
if err != nil {
|
||||
|
110
client/peer.go
110
client/peer.go
@ -11,14 +11,19 @@ import (
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// Peer manages the peer messaging functions
|
||||
// - Building simple user messages
|
||||
// - Utility functions for packing/unpacking, encrypting/decrypting messages for peer communication
|
||||
type Peer struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
MyName string `json:"my_name,omitempty"`
|
||||
MyAvatar string `json:"my_avatar,omitempty"`
|
||||
// Conversation []InternalMessage `json:"conversation,omitempty"`
|
||||
// My own keys for that peer
|
||||
MyIdentity meowlib.KeyPair `json:"my_identity,omitempty"`
|
||||
MyEncryptionKp meowlib.KeyPair `json:"my_encryption_kp,omitempty"`
|
||||
MyLookupKp meowlib.KeyPair `json:"my_lookup_kp,omitempty"`
|
||||
MyPullServers []meowlib.Server `json:"my_pull_servers,omitempty"`
|
||||
MyPullServers []meowlib.ServerCard `json:"my_pull_servers,omitempty"`
|
||||
// Peer keys and infos
|
||||
Contact meowlib.ContactCard `json:"contact,omitempty"`
|
||||
InvitationId string `json:"invitation_id,omitempty"`
|
||||
@ -31,53 +36,13 @@ type Peer struct {
|
||||
OnionMode bool `json:"onion_mode,omitempty"`
|
||||
LastMessage time.Time `json:"last_message,omitempty"`
|
||||
DbIds []string `json:"db_ids,omitempty"`
|
||||
DbPassword string `json:"db_password,omitempty"`
|
||||
AvatarUuid string `json:"avatar_uid,omitempty"`
|
||||
dbPassword string
|
||||
}
|
||||
|
||||
type PeerList []Peer
|
||||
|
||||
type Group struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Members []Peer `json:"members,omitempty"`
|
||||
}
|
||||
|
||||
func (pl *PeerList) GetFromPublicKey(publickey string) *Peer {
|
||||
for _, peer := range *pl {
|
||||
if peer.Contact.ContactPublicKey == publickey {
|
||||
return &peer
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pl *PeerList) GetFromMyLookupKey(publickey string) *Peer {
|
||||
for _, peer := range *pl {
|
||||
if peer.MyLookupKp.Public == publickey {
|
||||
return &peer
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pl *PeerList) GetFromName(name string) *Peer {
|
||||
for _, peer := range *pl {
|
||||
if peer.Contact.Name == name {
|
||||
return &peer
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pl *PeerList) GetConversationRequests() []*meowlib.ToServerMessage_ConversationRequest {
|
||||
var list []*meowlib.ToServerMessage_ConversationRequest
|
||||
for _, peer := range *pl {
|
||||
var cr meowlib.ToServerMessage_ConversationRequest
|
||||
cr.LookupKey = peer.MyLookupKp.Public
|
||||
// TODO Add key signature
|
||||
list = append(list, &cr)
|
||||
}
|
||||
return list
|
||||
}
|
||||
//
|
||||
// Messages building
|
||||
//
|
||||
|
||||
func (p *Peer) BuildSimpleUserMessage(message []byte) (*meowlib.UserMessage, error) {
|
||||
var msg meowlib.UserMessage
|
||||
@ -85,7 +50,7 @@ func (p *Peer) BuildSimpleUserMessage(message []byte) (*meowlib.UserMessage, err
|
||||
msg.From = p.MyIdentity.Public
|
||||
msg.Data = message
|
||||
msg.Type = "1"
|
||||
msg.Status = &meowlib.UserMessage_ConversationStatus{}
|
||||
msg.Status = &meowlib.ConversationStatus{}
|
||||
msg.Status.LocalUuid = uuid.New().String()
|
||||
return &msg, nil
|
||||
}
|
||||
@ -96,16 +61,13 @@ func (p *Peer) BuildSingleFileMessage(filename string, message []byte) ([]meowli
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// declare chunk size
|
||||
maxSz := GetConfig().Chunksize
|
||||
|
||||
// create buffer
|
||||
b := make([]byte, maxSz)
|
||||
chunk := 0
|
||||
@ -129,7 +91,7 @@ func (p *Peer) BuildSingleFileMessage(filename string, message []byte) ([]meowli
|
||||
msg.Files = append(msg.Files, &file)
|
||||
msg.Type = "2"
|
||||
if chunk == 0 {
|
||||
msg.Status = &meowlib.UserMessage_ConversationStatus{}
|
||||
msg.Status = &meowlib.ConversationStatus{}
|
||||
msg.Status.LocalUuid = uuid.New().String()
|
||||
}
|
||||
msgs = append(msgs, msg)
|
||||
@ -138,6 +100,27 @@ func (p *Peer) BuildSingleFileMessage(filename string, message []byte) ([]meowli
|
||||
return msgs, nil
|
||||
}
|
||||
|
||||
// Builds an invitation answer user message.
|
||||
// it takes as input a contactcard generated by Identity.AnswerInvitation
|
||||
func (p *Peer) BuildInvitationAnswserMessage(myContactCard *meowlib.ContactCard) (*meowlib.UserMessage, error) {
|
||||
var msg meowlib.UserMessage
|
||||
var invitation meowlib.Invitation
|
||||
invitation.Step = 3
|
||||
out, err := proto.Marshal(myContactCard)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
invitation.Payload = out
|
||||
msg.Destination = p.Contact.LookupPublicKey
|
||||
msg.From = p.MyIdentity.Public
|
||||
msg.Type = "1"
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
//
|
||||
// Messages encryption and packaging
|
||||
//
|
||||
|
||||
func (p *Peer) SerializeUserMessage(msg *meowlib.UserMessage) ([]byte, error) {
|
||||
out, err := proto.Marshal(msg)
|
||||
if err != nil {
|
||||
@ -176,7 +159,7 @@ func (p *Peer) AsymDecryptMessage(Message []byte, Signature []byte) (DecryptedMe
|
||||
return DecryptedMessage, err
|
||||
}
|
||||
|
||||
// PackUserMessage will package the previously encrypted message for sending it to the peer in protobuff format
|
||||
// PackUserMessage will package the previously encrypted message
|
||||
func (p *Peer) PackUserMessage(message []byte, signature []byte) *meowlib.PackedUserMessage {
|
||||
var msg meowlib.PackedUserMessage
|
||||
msg.Destination = p.Contact.LookupPublicKey
|
||||
@ -185,6 +168,7 @@ func (p *Peer) PackUserMessage(message []byte, signature []byte) *meowlib.Packed
|
||||
return &msg
|
||||
}
|
||||
|
||||
// UnPackUserMessage unpacks a user message
|
||||
func (p *Peer) UnPackUserMessage(protoPackedMessage []byte) (payload []byte, signature []byte, err error) {
|
||||
msg := &meowlib.PackedServerMessage{}
|
||||
if err := proto.Unmarshal(protoPackedMessage, msg); err != nil {
|
||||
@ -193,11 +177,12 @@ func (p *Peer) UnPackUserMessage(protoPackedMessage []byte) (payload []byte, sig
|
||||
return msg.Payload, msg.Signature, nil
|
||||
}
|
||||
|
||||
func (p *Peer) GetConversationRequest() meowlib.ToServerMessage_ConversationRequest {
|
||||
var cr meowlib.ToServerMessage_ConversationRequest
|
||||
return cr
|
||||
func (p *Peer) GetConversationRequest() *meowlib.ConversationRequest {
|
||||
var cr meowlib.ConversationRequest
|
||||
return &cr
|
||||
}
|
||||
|
||||
// ProcessOutboundUserMessage is a helper function that serializes, encrypts and packs a user message
|
||||
func (p *Peer) ProcessOutboundUserMessage(usermessage *meowlib.UserMessage) (*meowlib.PackedUserMessage, error) {
|
||||
serializedMessage, err := p.SerializeUserMessage(usermessage)
|
||||
if err != nil {
|
||||
@ -210,10 +195,10 @@ func (p *Peer) ProcessOutboundUserMessage(usermessage *meowlib.UserMessage) (*me
|
||||
}
|
||||
// Packing it
|
||||
packedMsg := p.PackUserMessage(enc.Data, enc.Signature)
|
||||
|
||||
return packedMsg, nil
|
||||
}
|
||||
|
||||
// ProcessInboundUserMessage is a helper function that decrypts and deserializes a user message
|
||||
func (p *Peer) ProcessInboundUserMessage(message []byte, signature []byte) (*meowlib.UserMessage, error) {
|
||||
dec, err := p.AsymDecryptMessage(message, signature)
|
||||
if err != nil {
|
||||
@ -226,6 +211,19 @@ func (p *Peer) ProcessInboundUserMessage(message []byte, signature []byte) (*meo
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
//
|
||||
// Messages database
|
||||
//
|
||||
|
||||
// SetDbPassword sets a specific password for a hidden user, it won't be saved, but kept in memory only for the next operations
|
||||
func (p *Peer) SetDbPassword(password string) {
|
||||
p.dbPassword = password
|
||||
}
|
||||
|
||||
func (p *Peer) GetDbPassword() string {
|
||||
return p.dbPassword
|
||||
}
|
||||
|
||||
func (p *Peer) StoreMessage(msg []byte) {
|
||||
|
||||
}
|
||||
|
@ -1,10 +1,21 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetFromPublicKey(t *testing.T) {
|
||||
id := CreateIdentity("test")
|
||||
id.Save()
|
||||
for i := 1; i < 10; i++ {
|
||||
var p Peer
|
||||
p.Name = "test" + strconv.Itoa(i)
|
||||
p.Contact.ContactPublicKey = "stringToFind" + strconv.Itoa(i)
|
||||
id.Peers = append(id.Peers, p)
|
||||
}
|
||||
p5 := id.Peers.GetFromPublicKey("stringToFind5")
|
||||
assert.Equal(t, p5.Name, "test5")
|
||||
}
|
||||
|
45
client/peerlist.go
Normal file
45
client/peerlist.go
Normal file
@ -0,0 +1,45 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
)
|
||||
|
||||
type PeerList []Peer
|
||||
|
||||
func (pl *PeerList) GetFromPublicKey(publickey string) *Peer {
|
||||
for _, peer := range *pl {
|
||||
if peer.Contact.ContactPublicKey == publickey {
|
||||
return &peer
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pl *PeerList) GetFromMyLookupKey(publickey string) *Peer {
|
||||
for _, peer := range *pl {
|
||||
if peer.MyLookupKp.Public == publickey {
|
||||
return &peer
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pl *PeerList) GetFromName(name string) *Peer {
|
||||
for _, peer := range *pl {
|
||||
if peer.Contact.Name == name {
|
||||
return &peer
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pl *PeerList) GetConversationRequests() []*meowlib.ConversationRequest {
|
||||
var list []*meowlib.ConversationRequest
|
||||
for _, peer := range *pl {
|
||||
var cr meowlib.ConversationRequest
|
||||
cr.LookupKey = peer.MyLookupKp.Public
|
||||
// TODO Add key signature
|
||||
list = append(list, &cr)
|
||||
}
|
||||
return list
|
||||
}
|
107
client/server.go
107
client/server.go
@ -9,46 +9,46 @@ import (
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type InternalServer struct {
|
||||
ServerData meowlib.Server `json:"server_data,omitempty"`
|
||||
// Server manages server related operations
|
||||
// - Sending messages for server usage
|
||||
// - Two first steps of an invitation
|
||||
// - User message sending with UserKp identification
|
||||
// - Messages lookup requests
|
||||
// - Utility functions for packing/unpacking, encrypting/decrypting messages for server communication
|
||||
// - Server remote management if ManagerKp is available for that server
|
||||
type Server struct {
|
||||
ServerData meowlib.ServerCard `json:"server_data,omitempty"`
|
||||
Presence bool `json:"presence,omitempty"`
|
||||
LastCheck time.Time `json:"last_check,omitempty"`
|
||||
Uptime time.Duration `json:"uptime,omitempty"`
|
||||
Login string `json:"login,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Me meowlib.KeyPair `json:"me,omitempty"`
|
||||
UserKp meowlib.KeyPair `json:"user_kp,omitempty"`
|
||||
ManagerKp meowlib.KeyPair `json:"manager_kp,omitempty"`
|
||||
Country string `json:"country,omitempty"`
|
||||
AllowedDelay int `json:"allowed_delay,omitempty"`
|
||||
Backup bool `json:"backup,omitempty"`
|
||||
}
|
||||
|
||||
type InternalServerList struct {
|
||||
Name string
|
||||
Servers []InternalServer
|
||||
}
|
||||
|
||||
func InternalServerFromUrl(url string) *InternalServer {
|
||||
var is InternalServer
|
||||
// CreateServerFromUrl creates a server from a basic url, ex : https://my.meowserver.example:8443/meow/
|
||||
func CreateServerFromUrl(url string) *Server {
|
||||
var is Server
|
||||
is.ServerData.Url = url
|
||||
return &is
|
||||
}
|
||||
|
||||
func InternalServerFromServer(server *meowlib.Server) *InternalServer {
|
||||
var is InternalServer
|
||||
// Create a server from a server card
|
||||
func CreateServerFromServerCard(server *meowlib.ServerCard) *Server {
|
||||
var is Server
|
||||
is.ServerData = *server
|
||||
is.Me = meowlib.NewKeyPair()
|
||||
is.UserKp = meowlib.NewKeyPair()
|
||||
return &is
|
||||
}
|
||||
|
||||
func (sl *InternalServerList) AddUrls(urls []string) {
|
||||
for _, url := range urls {
|
||||
sl.Servers = append(sl.Servers, *InternalServerFromUrl(url))
|
||||
}
|
||||
}
|
||||
|
||||
// AsymEncryptMessage prepares a message to send to a specific internal server
|
||||
func (ints *InternalServer) AsymEncryptMessage(Message []byte) (*meowlib.EncryptedMessage, error) {
|
||||
func (ints *Server) AsymEncryptMessage(Message []byte) (*meowlib.EncryptedMessage, error) {
|
||||
var enc *meowlib.EncryptedMessage
|
||||
enc, err := meowlib.AsymEncryptAndSign(ints.ServerData.PublicKey, ints.Me.Private, Message)
|
||||
enc, err := meowlib.AsymEncryptAndSign(ints.ServerData.PublicKey, ints.UserKp.Private, Message)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return nil, err
|
||||
@ -57,8 +57,8 @@ func (ints *InternalServer) AsymEncryptMessage(Message []byte) (*meowlib.Encrypt
|
||||
}
|
||||
|
||||
// AsymDecryptMessage reads a message from a specific internal server
|
||||
func (ints *InternalServer) AsymDecryptMessage(Message []byte, Signature []byte) (DecryptedMessage []byte, err error) {
|
||||
DecryptedMessage, err = meowlib.AsymDecryptAndCheck(ints.Me.Private, ints.ServerData.PublicKey, Message, Signature)
|
||||
func (ints *Server) AsymDecryptMessage(Message []byte, Signature []byte) (DecryptedMessage []byte, err error) {
|
||||
DecryptedMessage, err = meowlib.AsymDecryptAndCheck(ints.UserKp.Private, ints.ServerData.PublicKey, Message, Signature)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return nil, err
|
||||
@ -66,18 +66,18 @@ func (ints *InternalServer) AsymDecryptMessage(Message []byte, Signature []byte)
|
||||
return DecryptedMessage, err
|
||||
}
|
||||
|
||||
// Creates a basic message to server from a single packed user message and returns it as protobuf serialized byte array
|
||||
func (ints *InternalServer) BuildToServerMessageFromUserMessage(usermsg *meowlib.PackedUserMessage) *meowlib.ToServerMessage {
|
||||
// BuildToServerMessageFromUserMessage creates a basic message to server from a single packed user message and returns a meowlib.ToServerMessage
|
||||
func (ints *Server) BuildToServerMessageFromUserMessage(usermsg *meowlib.PackedUserMessage) *meowlib.ToServerMessage {
|
||||
var msg meowlib.ToServerMessage
|
||||
msg.Uuid = uuid.New().String()
|
||||
msg.Type = "1"
|
||||
msg.From = ints.Me.Public
|
||||
msg.From = ints.UserKp.Public
|
||||
msg.Messages = append(msg.Messages, usermsg)
|
||||
return &msg
|
||||
}
|
||||
|
||||
// Creates a basic message to server from a single packed user message and returns it as protobuf serialized byte array
|
||||
func (ints *InternalServer) BuildMessageSendingMessage(usermsg *meowlib.PackedUserMessage) ([]byte, error) {
|
||||
// BuildMessageSendingMessage creates a basic message to server from a single packed user message and returns it as protobuf serialized byte array
|
||||
func (ints *Server) BuildMessageSendingMessage(usermsg *meowlib.PackedUserMessage) ([]byte, error) {
|
||||
msg := ints.BuildToServerMessageFromUserMessage(usermsg)
|
||||
out, err := proto.Marshal(msg)
|
||||
if err != nil {
|
||||
@ -86,13 +86,12 @@ func (ints *InternalServer) BuildMessageSendingMessage(usermsg *meowlib.PackedUs
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Creates a basic message to server from a single packed user message and returns it as protobuf serialized byte array
|
||||
func (ints *InternalServer) BuildMessageRequestMessage(lookupKeys []string) ([]byte, error) {
|
||||
// BuildMessageRequestMessage creates a message lookup message to server and returns it as protobuf serialized byte array
|
||||
func (ints *Server) BuildMessageRequestMessage(lookupKeys []string) ([]byte, error) {
|
||||
var msg meowlib.ToServerMessage
|
||||
msg.Uuid = uuid.New().String()
|
||||
msg.Type = "1"
|
||||
msg.From = ints.Me.Public
|
||||
|
||||
msg.From = ints.UserKp.Public
|
||||
out, err := proto.Marshal(&msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -100,9 +99,43 @@ func (ints *InternalServer) BuildMessageRequestMessage(lookupKeys []string) ([]b
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (ints *InternalServer) PackServerMessage(payload []byte, signature []byte) (protoPackedMessage []byte, err error) {
|
||||
// BuildToServerMessageInvitation creates an invitation message to server and returns it as a meowlib.ToServerMessage
|
||||
// it takes as input a contactcard generated by Identity.InvitePeer
|
||||
func (ints *Server) BuildToServerMessageInvitationCreation(invitation *meowlib.ContactCard, password string, timeout int, invitationIdLen int) (*meowlib.ToServerMessage, error) {
|
||||
var msg meowlib.ToServerMessage
|
||||
var inv meowlib.Invitation
|
||||
payload, err := invitation.Compress()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msg.Type = "1"
|
||||
msg.From = ints.UserKp.Public
|
||||
inv.Step = 1
|
||||
inv.Password = password
|
||||
inv.Timeout = int32(timeout)
|
||||
inv.ShortcodeLen = int32(invitationIdLen)
|
||||
inv.Payload = payload
|
||||
msg.Invitation = &inv
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
// BuildToServerMessageInvitationRequest requests invitation with provided id from server and returns it as a meowlib.ToServerMessage
|
||||
func (ints *Server) BuildToServerMessageInvitationRequest(shortcode string, password string) (*meowlib.ToServerMessage, error) {
|
||||
var msg meowlib.ToServerMessage
|
||||
var inv meowlib.Invitation
|
||||
msg.Type = "1"
|
||||
msg.From = ints.UserKp.Public
|
||||
inv.Step = 2
|
||||
inv.Password = password
|
||||
inv.Shortcode = shortcode
|
||||
msg.Invitation = &inv
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
// PackServerMessage
|
||||
func (ints *Server) PackServerMessage(payload []byte, signature []byte) (protoPackedMessage []byte, err error) {
|
||||
var msg meowlib.PackedServerMessage
|
||||
msg.From = ints.Me.Public
|
||||
msg.From = ints.UserKp.Public
|
||||
msg.Payload = payload
|
||||
msg.Signature = signature
|
||||
out, err := proto.Marshal(&msg)
|
||||
@ -112,7 +145,7 @@ func (ints *InternalServer) PackServerMessage(payload []byte, signature []byte)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (ints *InternalServer) UnPackServerMessage(protoPackedMessage []byte) (payload []byte, signature []byte, err error) {
|
||||
func (ints *Server) UnPackServerMessage(protoPackedMessage []byte) (payload []byte, signature []byte, err error) {
|
||||
msg := &meowlib.PackedServerMessage{}
|
||||
if err := proto.Unmarshal(protoPackedMessage, msg); err != nil {
|
||||
return nil, nil, err
|
||||
@ -120,7 +153,7 @@ func (ints *InternalServer) UnPackServerMessage(protoPackedMessage []byte) (payl
|
||||
return msg.Payload, msg.Signature, nil
|
||||
}
|
||||
|
||||
func (srv *InternalServer) ProcessOutboundMessage(toServerMessage *meowlib.ToServerMessage) ([]byte, error) {
|
||||
func (srv *Server) ProcessOutboundMessage(toServerMessage *meowlib.ToServerMessage) ([]byte, error) {
|
||||
byteToServerMessage, err := proto.Marshal(toServerMessage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -138,7 +171,7 @@ func (srv *InternalServer) ProcessOutboundMessage(toServerMessage *meowlib.ToSer
|
||||
return protoPackedServerMsg, nil
|
||||
}
|
||||
|
||||
func (srv *InternalServer) ProcessInboundServerResponse(msg []byte) (*meowlib.FromServerMessage, error) {
|
||||
func (srv *Server) ProcessInboundServerResponse(msg []byte) (*meowlib.FromServerMessage, error) {
|
||||
fsmsg := &meowlib.FromServerMessage{}
|
||||
payload, signature, err := srv.UnPackServerMessage(msg)
|
||||
if err != nil {
|
||||
|
52
client/serverlist.go
Normal file
52
client/serverlist.go
Normal file
@ -0,0 +1,52 @@
|
||||
package client
|
||||
|
||||
import "errors"
|
||||
|
||||
// ServerList manages lists that are used in Identity
|
||||
// - Message servers list
|
||||
// - ArchiveServers lists
|
||||
// - Owned servers lists
|
||||
// - Matriochka paths
|
||||
type ServerList struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Servers []Server `json:"servers,omitempty"`
|
||||
}
|
||||
|
||||
// FilterByIdxs returns a filtered server list filtered according to an index list
|
||||
func (sl *ServerList) FilterByIdxs(MessageServerIdxs []int) (filtered *ServerList, err error) {
|
||||
filtered.Servers = []Server{}
|
||||
for _, i := range MessageServerIdxs {
|
||||
if i > len(sl.Servers)-1 {
|
||||
return nil, errors.New("requested server out of range of defined message servers")
|
||||
}
|
||||
}
|
||||
for _, i := range MessageServerIdxs {
|
||||
filtered.Servers = append(filtered.Servers, sl.Servers[i])
|
||||
}
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
// GetServerByIdx returns a server from it's index
|
||||
func (sl *ServerList) GetServerByIdx(idx int) (server *Server, err error) {
|
||||
if idx > len(sl.Servers)-1 {
|
||||
return nil, errors.New("requested server out of range of defined message servers")
|
||||
}
|
||||
return &sl.Servers[idx], nil
|
||||
}
|
||||
|
||||
// GetServerByPubkey returns a server from it's public key
|
||||
func (sl *ServerList) GetServerByPubkey(pubkey string) (filtered *Server) {
|
||||
for _, srv := range sl.Servers {
|
||||
if srv.ServerData.PublicKey == pubkey {
|
||||
return &srv
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddUrls is a simple utility functon used mainly as a shortcut for testing purposes
|
||||
func (sl *ServerList) AddUrls(urls []string) {
|
||||
for _, url := range urls {
|
||||
sl.Servers = append(sl.Servers, *CreateServerFromUrl(url))
|
||||
}
|
||||
}
|
184
client/storage.go
Normal file
184
client/storage.go
Normal file
@ -0,0 +1,184 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"github.com/google/uuid"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func StoreMessage(peer *Peer, usermessage *meowlib.UserMessage, password string) error {
|
||||
var dbid string
|
||||
// If no db/no ID create DB + Tablz
|
||||
// TODO : if file size > X new db
|
||||
if len(peer.DbIds) == 0 {
|
||||
dbid = uuid.NewString()
|
||||
file, err := os.Create(filepath.Join(GetConfig().StoragePath, dbid+GetConfig().DbSuffix))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file.Close()
|
||||
peer.DbIds = append(peer.DbIds, dbid)
|
||||
sqliteDatabase, _ := sql.Open("sqlite3", filepath.Join(GetConfig().StoragePath, dbid+GetConfig().DbSuffix)) // Open the created SQLite File
|
||||
err = createMessageTable(sqliteDatabase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sqliteDatabase.Close()
|
||||
GetConfig().me.Save()
|
||||
} else {
|
||||
dbid = peer.DbIds[len(peer.DbIds)-1]
|
||||
}
|
||||
// Open Db
|
||||
db, _ := sql.Open("sqlite3", filepath.Join(GetConfig().StoragePath, dbid+GetConfig().DbSuffix)) // Open the created SQLite File
|
||||
defer db.Close()
|
||||
// Detach Files
|
||||
if len(usermessage.Files) > 0 {
|
||||
for _, f := range usermessage.Files {
|
||||
hiddenFilename := uuid.NewString()
|
||||
// Cypher file
|
||||
encData, err := meowlib.SymEncrypt(password, f.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.WriteFile(hiddenFilename, encData, 0600)
|
||||
// replace f.Data by uuid filename
|
||||
f.Data = []byte(hiddenFilename)
|
||||
}
|
||||
}
|
||||
// Encrypt message
|
||||
out, err := proto.Marshal(usermessage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
encData, err := meowlib.SymEncrypt(password, out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Insert message
|
||||
insertMessageSQL := `INSERT INTO message(m) VALUES (?) RETURNING ID`
|
||||
statement, err := db.Prepare(insertMessageSQL) // Prepare statement.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = statement.Exec(encData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get last messages from a peer
|
||||
func GetLastMessages(peer *Peer, inAppMsgCount int, lastDbId int, wantMore int, password string) ([]InternalUserMessage, error) {
|
||||
var messages []InternalUserMessage
|
||||
fileidx := len(peer.DbIds) - 1
|
||||
// initialize count with last db message count
|
||||
countStack, err := getMessageCount(peer.DbIds[fileidx])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// while the db message count < what we already have in app, step to next db file
|
||||
for inAppMsgCount > countStack {
|
||||
fileidx--
|
||||
if fileidx < 0 {
|
||||
return nil, nil
|
||||
}
|
||||
newCount, err := getMessageCount(peer.DbIds[fileidx])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
countStack += newCount
|
||||
}
|
||||
// There fileidx should provide the db that we need (unless wantMore overlaps the next DB)
|
||||
db, _ := sql.Open("sqlite3", filepath.Join(GetConfig().StoragePath, peer.DbIds[fileidx]+GetConfig().DbSuffix)) // Open the created SQLite File
|
||||
defer db.Close()
|
||||
// if it's first app query, it won't hold a lastIndex, so let's start from end
|
||||
if lastDbId == 0 {
|
||||
lastDbId = math.MaxInt64
|
||||
}
|
||||
stm, err := db.Prepare("SELECT id, m FROM message WHERE id < ? ORDER BY id DESC LIMIT ?")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stm.Close()
|
||||
rows, err := stm.Query(lastDbId, wantMore)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var ium InternalUserMessage
|
||||
var um meowlib.UserMessage
|
||||
var id int64
|
||||
var m []byte
|
||||
err = rows.Scan(&id, &m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decdata, err := meowlib.SymDecrypt(password, m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = proto.Unmarshal(decdata, &um)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ium.dbid = id
|
||||
ium.message = &um
|
||||
messages = append(messages, ium)
|
||||
}
|
||||
// TODO DB overlap
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
func getMessageCount(dbid string) (int, error) {
|
||||
db, _ := sql.Open("sqlite3", filepath.Join(GetConfig().StoragePath, dbid+GetConfig().DbSuffix)) // Open the created SQLite File
|
||||
defer db.Close()
|
||||
var count int
|
||||
query := "SELECT COUNT(*) FROM message"
|
||||
err := db.QueryRow(query).Scan(&count)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func createMessageTable(db *sql.DB) error {
|
||||
createMessageTableSQL := `CREATE TABLE message (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"m" BLOB);` // SQL Statement for Create Table
|
||||
statement, err := db.Prepare(createMessageTableSQL) // Prepare SQL Statement
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
statement.Exec() // Execute SQL Statements
|
||||
return nil
|
||||
}
|
||||
|
||||
func createServerTable(db *sql.DB) error {
|
||||
createServerTableSQL := `CREATE TABLE servers (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"country" varchar(2),
|
||||
"public" bool,
|
||||
"uptime" int,
|
||||
"bandwith" float,
|
||||
"load" float,
|
||||
"url" varchar(2000)
|
||||
"name" varchar(255);
|
||||
"description" varchar(5000)
|
||||
"publickey" varchar(10000)
|
||||
)` // SQL Statement for Create Table
|
||||
statement, err := db.Prepare(createServerTableSQL) // Prepare SQL Statement
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
statement.Exec() // Execute SQL Statements
|
||||
return nil
|
||||
}
|
72
client/storage_test.go
Normal file
72
client/storage_test.go
Normal file
@ -0,0 +1,72 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStoreMessage(t *testing.T) {
|
||||
id := createId()
|
||||
var um meowlib.UserMessage
|
||||
um.Data = []byte("blabla")
|
||||
err := StoreMessage(&id.Peers[0], &um, GetConfig().memoryPassword)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
messages, err := GetLastMessages(&id.Peers[0], 0, 0, 10, GetConfig().memoryPassword)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Checks
|
||||
assert.Equal(t, len(messages), 1, "not 1 message")
|
||||
assert.Equal(t, messages[0].message.Data, um.Data, "not 1 message")
|
||||
// Cleanup
|
||||
if exists("test.id") {
|
||||
os.Remove("test.id")
|
||||
}
|
||||
files, err := filepath.Glob("*.sqlite")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, f := range files {
|
||||
if err := os.Remove(f); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestManyStoreMessage(t *testing.T) {
|
||||
id := createId()
|
||||
for i := 1; i < 100; i++ {
|
||||
var um meowlib.UserMessage
|
||||
um.Data = []byte(randomLenString(20, 200))
|
||||
err := StoreMessage(&id.Peers[0], &um, GetConfig().memoryPassword)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
messages, err := GetLastMessages(&id.Peers[0], 0, 0, 10, GetConfig().memoryPassword)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Checks
|
||||
assert.Equal(t, len(messages), 10, "not 10 message")
|
||||
// Cleanup
|
||||
if exists("test.id") {
|
||||
os.Remove("test.id")
|
||||
}
|
||||
files, err := filepath.Glob("*.sqlite")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, f := range files {
|
||||
if err := os.Remove(f); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
@ -17,8 +17,8 @@ import (
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func ServerFromUrl(url string) *Server {
|
||||
var s Server
|
||||
func ServerFromUrl(url string) *ServerCard {
|
||||
var s ServerCard
|
||||
s.Url = url
|
||||
return &s
|
||||
}
|
||||
|
12
doc/docgen.sh
Executable file
12
doc/docgen.sh
Executable file
@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
go-plantuml generate -o generated/meowlib.puml -d ..
|
||||
sed -i 's/\.\./meowlib\ /g' generated/meowlib.puml
|
||||
go-plantuml generate -o generated/client.puml -d ../client
|
||||
sed -i 's/\.\.\/client/client/g' generated/client.puml
|
||||
go-plantuml generate -o generated/server.puml -d ../server
|
||||
sed -i 's/\.\.\/server/server/g' generated/server.puml
|
||||
cd generated
|
||||
plantuml .
|
||||
plantuml -latex .
|
||||
|
||||
|
218
doc/meow.tex
Normal file
218
doc/meow.tex
Normal file
@ -0,0 +1,218 @@
|
||||
\documentclass{article}
|
||||
\usepackage{fetamont}
|
||||
\begin{document}
|
||||
\title{
|
||||
\textffm{Meow} messaging protocol}
|
||||
\author{Author
|
||||
\texttt{meow@redroom.link}}
|
||||
\date{\today}
|
||||
|
||||
\maketitle
|
||||
|
||||
\begin{abstract}
|
||||
|
||||
|
||||
The \textffm{Meow} protocol is a privacy driven instant messaging protocol.
|
||||
That protocol might be used for creating secure and distributed chat services or allowing machine to machine communication.
|
||||
This document describes the services provided by the protocol.
|
||||
\begin{quote}
|
||||
|
||||
\centering
|
||||
\emph{"Nous ne vivrons pas d'utopie collective, nous arrivons trop tard, le grand marché est déjà là.
|
||||
Nous devons élaborer des stratégies de survie et de contamination, par la prolifération d'utopies privées, cryptées, qui se substitueront à l'ancien ordre social.
|
||||
Tout ce que je sais, c'est que nous vivons dans un monde dont on ne s'évade pas"}\\
|
||||
\footnotesize{Maurice G. Dantec for NOII (1997)}
|
||||
\end{quote}
|
||||
\end{abstract}
|
||||
|
||||
|
||||
\section{Services}
|
||||
\subsection{Unregulated identities}
|
||||
The only requirement to get a valid \textffm{Meow} identity is to generate a user key pair.
|
||||
No phone number or email check will be performed, unlike main instant messaging protocols, there is no central administration.
|
||||
|
||||
\subsection{Fine grained privacy control}
|
||||
|
||||
\subsubsection{Trustable server based communication}
|
||||
Like most widely available messaging softwares, (Whatsapp, Signal, Viber, Telegram...), \textffm{Meow} provides a simple server based messaging.
|
||||
The main difference is that \textffm{Meow} allows you to explicitly choose which server you want to use.
|
||||
The server code being open source, we strongly encourage you to run your own server at home or in your company.
|
||||
The server requires very few ressources and will run on any low cost single board computer.
|
||||
|
||||
\subsubsection{Anonymized message transfer}
|
||||
\textffm{Meow} also provides an anonymizing transfer service very similar to the Tor Onion protocol, we call it the Matriochka protocol.
|
||||
Any server can be used for building the transfer chain.
|
||||
Some of them might be marked as trusted.
|
||||
Random delays and random payload padding might be set for each forwarding step, making the overall message tracking much more difficult, even for organizations having capabilities of global network surveillance.
|
||||
It is strongly advised to use trusted servers as your first node and message server (the one that holds your incoming messages).
|
||||
|
||||
\subsubsection{Presence protocol for direct messaging}
|
||||
A presence service associating your conversation keys to your IP address for direct peer to peer connection is also provided.
|
||||
The presence protocol is simply activated by setting a flag in the message poll requests.
|
||||
If that flag is set, your encrypted IP will be published on the server, allowing your only your peer(s) to decrypt it and directly communicate with your terminal.
|
||||
|
||||
\subsubsection{Peer based privacy settings}
|
||||
You might define specific communication privacy preferences for each of your contacts:
|
||||
\begin{itemize}
|
||||
\item simple server based communication allowed for Sarah,
|
||||
\item preferred direct communication with Julian, fallback to my own server,
|
||||
\item required matriochka protocol for Edward, first node is one of my trusted servers, my message node is my own server, randomly switch from trusted server lists for others.
|
||||
\item ...
|
||||
\end{itemize}
|
||||
|
||||
\subsubsection{Resistance to device requisition / forensic}
|
||||
All your contact information and discussion are encrypted on the device and password protected.
|
||||
Password shall be asked on application startup and allows your identity file and contact decrytion.
|
||||
That password is not recoverable, so you can't forget it, or you'll loose your whole configuration and identity.
|
||||
Real security implies some constraints.
|
||||
You might configure the app to save your password, but that is a security flaw.
|
||||
In many authoritarian countries, you are required by law to provide your device passwords to authorities.
|
||||
In a \textffm{Meow} device, you might set a specific password for some contacts.
|
||||
Those contacts won't be visible when entering your main identity password.
|
||||
You'll have to type their specific password in order to make them visible.
|
||||
The \textffm{Meow} application will by default create a random set of fake hidden contacts and conversations.
|
||||
Even in case of device storage analysis, authorities won't be able to differentiate a real hidden contact from an normal fake generated one.
|
||||
It could be argued that this feature puts every user at risk, because authorities might think you're hiding something, even if you're not.
|
||||
As every \textffm{Meow} user has the same constraint, users are not responsible for that. Moreover solidarity is also a requirement for real security.
|
||||
|
||||
\subsection{Multiple devices support}
|
||||
\textffm{Meow} allows you to be connected from multiple devices and offers chat synchronization capability.
|
||||
A device might be revoqued anytime from any other one. Proof of your identity (password or other) shall be provided in order to grant device revocation.
|
||||
|
||||
\subsection{Adding contacts}
|
||||
If you want to add a new contact, keys will be generated, then a contact card will be created.
|
||||
That contact card might be sent by any trustable communication means, or preferably from hand to hand, as a file on a flash disk or a QR code.\\
|
||||
In return your contact will provide a similar contact card as an answer to your invitation.
|
||||
|
||||
\subsection{Contacts forwarding}
|
||||
By using the \textffm{Meow} protocol a user won't be able to forward your contact information without your consent.
|
||||
Each user knows you as a different identity, thus forwarding a known identity to another user is meaningless. Any message to that identity signed by another user than you would be discarded.
|
||||
|
||||
|
||||
\subsection{Group conversation}
|
||||
A very basic group messaging service is available. It allows to exchange group information between users. After that, a message to a group will send a copy of the message to each member.
|
||||
|
||||
|
||||
\subsection{Emergency broadcast}
|
||||
A local (server based) emergency broadcast service will be provided. It will provide the ability to send/receive broadcast messages to all users connected to the current server.
|
||||
|
||||
|
||||
\subsection{Public networks shortage resilience}
|
||||
\textffm{Meow} may run without Internet connection, either on an isolated wifi access point, or on a meshed network of wifi routers or even via serial IOT transport layers (LoRa,...)
|
||||
|
||||
|
||||
\subsection{User directory service}
|
||||
This service allows restoring a lost functionality of Internet historic chat services (like ICQ). You could simply set a "Free for chat" status that would allow other people to contact you, either randomly or based on a short description that you might provide.
|
||||
Why providing that service while the internet is suffocating due to the abundance of social networks ?\\
|
||||
Well, that option offers a few advantages :
|
||||
\begin{itemize}
|
||||
\item you are still an anonymous user chatting with other anonymous users;
|
||||
\item no social network algorithm will select people that think/behave/vote/eat... just like you. Diversity makes a better world;
|
||||
\item a smaller community of users, skilled enough to operate a \textffm{Meow} chat app... that might provide a first filter;
|
||||
It's a bit like in the early ages, when people had to be able to start a win98 computer, connect it to internet, then download and install ICQ...
|
||||
If you lost some time in social networks today, and experienced ICQ in the 2000's, you'll understand what we'd like to revive.
|
||||
\end{itemize}
|
||||
|
||||
\section{Identities and keys}
|
||||
|
||||
\subsection{User identity}
|
||||
Each \textffm{Meow} user has a unique identity. That identity is strictly private, only used to manage your own data (local encryption, devices, ...)
|
||||
Let's call that one the User Key Pair (Ukp)
|
||||
|
||||
\subsection{Contact identity}
|
||||
Each of your contacts will know you under a different identity, we'll call that one the Contact Key Pair (Ckp)
|
||||
That contact Key Pair will not change once it's agreed between both peers: an initial key will be exchanged as part of the peer invitation process.
|
||||
As other people might have seen your key, this means that :
|
||||
\begin{itemize}
|
||||
\item none of your contacts will be able to forward your id to another person without your consent;
|
||||
\item any message to that Ckp, not signed by its associated user, will be discarded.
|
||||
\end{itemize}
|
||||
|
||||
\subsection{Conversation encryption}
|
||||
Each conversation with one of your contacts will be encrypted using an encryption keypair (Ekp) allowing cyphering your conversation.
|
||||
The Ekp might be changed anytime by its owner and the new public key will be sent along the last message.
|
||||
|
||||
\subsection{Conversation lookup}
|
||||
A contact conversation Lookup Key Pair(Lkp) is also associated with your conversation. The Lkp public key is used to identify your conversation on a server.
|
||||
The private key allows you to sign your request and prove the server that you are the legitimate recipient for a message.
|
||||
This Lkp can be changed anytime by its owner and the new public key will be sent along the last message.
|
||||
The Lkp and the Ekp are only changed once the change has been acknowledged by your contact.
|
||||
|
||||
\subsection{Server identity}
|
||||
Each server has a Server key (Skp). That key allows you to cypher the messages that you're sending to the server.
|
||||
|
||||
\subsection{Device identity}
|
||||
Each device is identified by a device key (Dkp) that allows you to perform secured exchanges between your devices for synchronization/revocation purposes.
|
||||
Communication between devices is achieved using the same principle as the user to user communication. A device might be considered as any another user. The messages content is based on a synchronization protocol.
|
||||
|
||||
\section{Contact management}
|
||||
\subsection{Adding a contact}
|
||||
Rendez-vous card, containing :
|
||||
\begin{itemize}
|
||||
\item Your public key for that contact;
|
||||
\item An initial conversation public key for getting encrypted messages from that contact;
|
||||
\item An initial conversation uuid that you'll use to lookup for incoming messages on the servers;
|
||||
\item A list of your preferred message servers;
|
||||
\item A signature to prevent transmission of tampered data.
|
||||
\end{itemize}
|
||||
\subsection{Sharing a contact}
|
||||
If a user wants to forward one of his contacts to you, it will be handled as a double request:
|
||||
\begin{enumerate}
|
||||
\item I'm receiving a contact name, without any key
|
||||
\item
|
||||
\end{enumerate}
|
||||
|
||||
\section{Messaging}
|
||||
\subsection{User messages}
|
||||
TODO
|
||||
|
||||
\subsection{Server stored message}
|
||||
TODO
|
||||
|
||||
\subsection{Matriochka message packing}
|
||||
TODO
|
||||
|
||||
\subsection{Synchronization messages}
|
||||
TODO
|
||||
|
||||
|
||||
\section{Server Features}
|
||||
\subsection{Server catalog}
|
||||
Each server will cache a list of all the servers that it is aware of.
|
||||
This server list will be shared between servers in a lazy exchange mode.
|
||||
|
||||
\subsection{Antispam}
|
||||
|
||||
\subsection{Self defense}
|
||||
The servers do integrate self defense mechanisms. Any threat to the \textffm{Meow} network by any computer,
|
||||
computer group or organization, might result in a distributed response from volunteering \textffm{Meow} servers and clients.
|
||||
An information about threat, desired defense action and request for assitance, might be submitted by any server or group of servers.
|
||||
Server owners and client users might accept or refuse to participate to the response action.
|
||||
|
||||
TODO : Request and actions definition consensus mechanism
|
||||
\section{Backup}
|
||||
\section{Recovery}
|
||||
|
||||
\section{Very secure devices}
|
||||
You don't trust your phone ?
|
||||
We're planning to provide very secured minimal devices dedicated to very sensitive \textffm{Meow} communication.
|
||||
|
||||
\section{Roadmap}
|
||||
\subsection{Nations}
|
||||
Beyond the scope of user directories, we plan to implement the concept of virtual Nations.
|
||||
Nation will allow people to regroup around common political funding values.
|
||||
They're not exclusive, you might be a citizen of several virtual nations.
|
||||
|
||||
Today still, most people don't really choose the nation they live in.
|
||||
You just have to live with the goverment decisions.
|
||||
In the best scenario that government was elected, and might represent at most 25\% of the population.
|
||||
In most case, they will vote laws to satisfy the powerful people who supported their election, and the most powerful lobbies.
|
||||
|
||||
\textffm{Meow} Nations aim to be the next lobbying power to influence real life politics, "the poor man's lobby".
|
||||
|
||||
Virtual nation in that perspective will be probably quickly flagged as terrorist nation by the old world media, but well,
|
||||
one man's terrorist is another man's freedom fighter.
|
||||
If requiring more democracy, using the same technique that is preventing it from happening, has to qualified that way, so be it.
|
||||
|
||||
|
||||
\end{document}
|
155
doc/protocol.tex
155
doc/protocol.tex
@ -1,8 +1,11 @@
|
||||
\documentclass{article}
|
||||
\usepackage{fetamont}
|
||||
\usepackage{listings}
|
||||
\usepackage{protobuf/lang} % include language definition for protobuf
|
||||
\usepackage{protobuf/style} % include custom style for proto declarations.
|
||||
\begin{document}
|
||||
\title{
|
||||
\textffm{Meow} messaging protocol}
|
||||
\textffm{Meow} messaging protocol description}
|
||||
\author{Author
|
||||
\texttt{meow@redroom.link}}
|
||||
\date{\today}
|
||||
@ -25,155 +28,13 @@ Tout ce que je sais, c'est que nous vivons dans un monde dont on ne s'évade pas
|
||||
\end{quote}
|
||||
\end{abstract}
|
||||
|
||||
\section{Identity creation}
|
||||
|
||||
\section{Services}
|
||||
\subsection{Unregulated identities}
|
||||
The only requirement to get a valid \textffm{Meow} identity is to generate a user key pair.
|
||||
No phone number or email check will be performed, unlike main instant messaging protocols, there is no central administration.
|
||||
\section{Messages structure}
|
||||
\lstinputlisting[language=protobuf2,style=protobuf]{../pb/messages.proto}
|
||||
|
||||
\subsection{Fine grained privacy control}
|
||||
\section{Invitation process}
|
||||
|
||||
\subsubsection{Trustable server based communication}
|
||||
Like most widely available messaging softwares, (Whatsapp, Signal, Viber, Telegram...), \textffm{Meow} provides a simple server based messaging.
|
||||
The main difference is that \textffm{Meow} allows you to explicitly choose which server you want to use.
|
||||
The server code being open source, we strongly encourage you to run your own server at home or in your company.
|
||||
The server requires very few ressources and will run on any low cost single board computer.
|
||||
|
||||
\subsubsection{Anonymized message transfer}
|
||||
\textffm{Meow} also provides an anonymizing transfer service very similar to the Tor Onion protocol, we call it the Matriochka protocol.
|
||||
Any server can be used for building the transfer chain.
|
||||
Some of them might be marked as trusted.
|
||||
Random delays might be set for each forwarding step, making the overall message tracking much more difficult, even with a global network audit.
|
||||
It is strongly advised to use trusted servers as your first node and message server (the one that holds your incoming messages).
|
||||
|
||||
\subsubsection{Presence protocol for direct messaging}
|
||||
A presence service associating your conversation keys to your IP address for direct peer to peer connection is also provided.
|
||||
The presence protocol is simply activated by setting a flag in the message poll requests.
|
||||
If that flag is set, your encrypted IP will be published on the server, allowing your only your peer(s) to decrypt it and directly communicate with your terminal.
|
||||
|
||||
\subsubsection{Peer based privacy settings}
|
||||
You might define specific communication privacy preferences for each of your contacts:
|
||||
\begin{itemize}
|
||||
\item simple server based communication allowed for Joe,
|
||||
\item preferred direct communication with Julian, fallback to my own server,
|
||||
\item required matriochka protocol for Edward, first node is one of my trusted servers, my message node is my own server, randomly switch from trusted server lists for others.
|
||||
\item ...
|
||||
\end{itemize}
|
||||
|
||||
\subsubsection{Resistance to device requisition}
|
||||
All your contact information and discussion are encrypted on the device and password protected.
|
||||
Password shall be asked on application startup and allows your identity file and contact decrytion.
|
||||
That password is not recoverable, so you can't forget it, or you'll loose your whole configuration and identity.
|
||||
Real security implies some constraints.
|
||||
You might configure the app to save your password, but that is a security flaw.
|
||||
In many authoritarian countries, you are required by law to provide your device passwords to authorities.
|
||||
In a \textffm{Meow} device, you might set a special password for specific contacts.
|
||||
Those contacts won't be visible when entering your main identity password.
|
||||
You'll have to type their specific password in order to make them visible.
|
||||
The \textffm{Meow} application will by default create a random set of fake hidden contacts and conversations.
|
||||
Even in case of device storage analysis, authorities won't be able to differentiate a real hidden contact from an normal fake generated one.
|
||||
It could be argued that this feature puts every user at risk, because authorities might think you're hiding something, even if you're not.
|
||||
As every \textffm{Meow} user has the same constraint, users are not responsible for that. Moreover solidarity is also a requirement for real security.
|
||||
|
||||
\subsection{Multiple devices support}
|
||||
\textffm{Meow} allows you to be connected from multiple devices and offers chat synchronization capability.
|
||||
A device might be revoqued anytime from any other one. Proof of your identity (password or other) shall be provided in order to grant device revocation.
|
||||
|
||||
\subsection{Adding contacts}
|
||||
If you want to add a new contact, keys and uuids will be generated, then a rendez-vous card will be created.
|
||||
That rendez-vous card might be sent by any trustable communication means, or preferably from hand to hand, as a file on a flash disk or a QR code.\\
|
||||
In return your contact will provide the exact same data, encrypted with your public key and delivered to the address specified in the initial rendez-vous card.
|
||||
|
||||
\subsection{Contacts forwarding}
|
||||
By using the \textffm{Meow} protocol a user won't be able to forward your contact information without your consent.
|
||||
Each user knows you as a different identity, thus forwarding a known identity to another user is meaningless. Any message to that identity signed by another user than you would be discarded.
|
||||
|
||||
|
||||
\subsection{Group conversation}
|
||||
A very basic group messaging service is available. It allows to exchange group information between users. After that, a message to a group will send a copy of the message to each member.
|
||||
|
||||
|
||||
\subsection{Emergency broadcast}
|
||||
A local (server based) emergency broadcast service will be provided. It will provide the ability to send/receive broadcast messages to all users connected to the current server.
|
||||
|
||||
|
||||
\subsection{Public networks shortage resilience}
|
||||
\textffm{Meow} may run without Internet connection, either on an isolated wifi access point, or on a meshed network of wifi routers or even via serial IOT transport layers (LoRa,...)
|
||||
|
||||
|
||||
\subsection{User directory service}
|
||||
This service allows to restore a lost functionality of Internet historic chat services (like ICQ). You could simply set a "Free for chat" status that would allow other people to contact you, either randomly or based on a short description that you might provide.
|
||||
Why providing that service while the internet is suffocating due to the abundance of social networks ?\\
|
||||
Well, that option offers a few advantages :
|
||||
\begin{itemize}
|
||||
\item you are still an anonymous user chatting with other anonymous users;
|
||||
\item no social network algorithm will select people that think/behave/vote/eat... just like you. Diversity makes a better world;
|
||||
\item a smaller community of users, skilled enough to operate a \textffm{Meow} chat app... that might provide a first filter;
|
||||
It's a bit like in the old times, when people had to be able to start a win98 computer, connect it to internet, then download and install ICQ...
|
||||
If you lost some time in social networks, and experienced ICQ in the 2000's, you'll understand.
|
||||
\end{itemize}
|
||||
|
||||
\section{Identities and keys}
|
||||
|
||||
\subsection{User identity}
|
||||
Each \textffm{Meow} user has a unique identity. That identity is strictly private, only used to manage your own data (local encryption, devices, ...)
|
||||
Let's call that one the User Key Pair (Ukp)
|
||||
|
||||
\subsection{Contact identity}
|
||||
Each of your contacts will know you under a different identity, we'll call that one the Contact Key Pair (Ckp)
|
||||
That contact Key Pair will not change once it's agreed between both peers: an initial key will be exchanged as part of the peer invitation process.
|
||||
As other people might have seen your key, this means that :
|
||||
\begin{itemize}
|
||||
\item none of your contacts will be able to forward your id to another person without your consent;
|
||||
\item any message to that Ckp, not signed by its associated user, will be discarded.
|
||||
\end{itemize}
|
||||
|
||||
\subsection{Conversation encryption}
|
||||
Each conversation with one of your contacts will be encrypted using an encryption keypair (Ekp) allowing cyphering your conversation.
|
||||
The Ekp might be changed anytime by its owner and the new public key will be sent along the last message.
|
||||
|
||||
\subsection{Conversation lookup}
|
||||
A contact conversation Lookup Key Pair(Lkp) is also associated with your conversation. The Lkp public key is used to identify your conversation on a server.
|
||||
The private key allows you to sign your request and prove the server that you are the legitimate recipient for a message.
|
||||
This Lkp can be changed anytime by its owner and the new public key will be sent along the last message.
|
||||
The Lkp and the Ekp are only changed once the change has beeen acknowledged by your contact.
|
||||
|
||||
\subsection{Server identity}
|
||||
Each server has a Server key (Skp). That key allows you to cypher the messages that you're sending to the server.
|
||||
|
||||
\subsection{Device identity}
|
||||
Each device is identified by a device key (Dkp) that allows you to perform secured exchanges between your devices for synchronization/revocation purposes.
|
||||
Communication between devices is achieved using the same principle as the user to user communication. A device might be considered as any another user. The messages content is based on a synchronization protocol.
|
||||
|
||||
\section{Contact management}
|
||||
\subsection{Adding a contact}
|
||||
Rendez-vous card, containing :
|
||||
\begin{itemize}
|
||||
\item Your public key for that contact;
|
||||
\item An initial conversation public key for getting encrypted messages from that contact;
|
||||
\item An initial conversation uuid that you'll use to lookup for incoming messages on the servers;
|
||||
\item A list of your preferred message servers;
|
||||
\item A signature to prevent transmission of tampered data.
|
||||
\end{itemize}
|
||||
\subsection{Sharing a contact}
|
||||
If a user wants to forward one of his contacts to you, it will be handled as a double request:
|
||||
\begin{enumerate}
|
||||
\item I'm receiving a contact name, without any key
|
||||
\item
|
||||
\end{enumerate}
|
||||
|
||||
\section{Messaging}
|
||||
\subsection{User messages}
|
||||
TODO
|
||||
|
||||
\subsection{Server stored message}
|
||||
TODO
|
||||
|
||||
\subsection{Matriochka message packing}
|
||||
TODO
|
||||
|
||||
\subsection{Synchronization messages}
|
||||
TODO
|
||||
|
||||
\section{Transport protocols}
|
||||
\subsection{URLs}
|
||||
|
@ -30,20 +30,21 @@ func TestEndToEnd(t *testing.T) {
|
||||
// Create an invitation for a friend, I want him/her to know me as Bender //
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
fmt.Println("Creating an invitation for the first friend...")
|
||||
invitation, err := Me.InvitePeer("Bender", "myfirstfriend", []int{1, 2})
|
||||
peer, myContactCard, err := Me.InvitePeer("Bender", "myfirstfriend", []int{1, 2})
|
||||
if err != nil {
|
||||
println(err)
|
||||
}
|
||||
println(peer.Name)
|
||||
// print my invitation
|
||||
a, _ := json.Marshal(invitation)
|
||||
a, _ := json.Marshal(myContactCard)
|
||||
fmt.Println(string(a))
|
||||
// TODO : Convert invitation to QR Code
|
||||
invitation.WritePng("invitation.png")
|
||||
data, err := invitation.Compress()
|
||||
myContactCard.WritePng("invitation.png")
|
||||
data, err := myContactCard.Compress()
|
||||
if err != nil {
|
||||
println(err)
|
||||
}
|
||||
invitation.WriteQr("qrcode.png")
|
||||
myContactCard.WriteQr("qrcode.png")
|
||||
println("Compressed contact card :", len(data))
|
||||
///////////////////////////////////////
|
||||
// Simulate peer invitation response //
|
||||
@ -59,9 +60,9 @@ func TestEndToEnd(t *testing.T) {
|
||||
ReceivedContact.ContactPublicKey = FirstFriendContactKp.Public
|
||||
ReceivedContact.EncryptionPublicKey = FirstFriendEncryptionKp.Public
|
||||
ReceivedContact.LookupPublicKey = FirstFriendLookupKp.Public
|
||||
ReceivedContact.InvitationId = invitation.InvitationId
|
||||
ReceivedContact.InvitationId = myContactCard.InvitationId
|
||||
FriendServer1KP := meowlib.NewKeyPair()
|
||||
FriendServer1 := meowlib.Server{Name: "FriendServer1", Url: "http://myfriend.org/meow/", PublicKey: FriendServer1KP.Public, Description: "Fancy description", ConfidenceLevel: 1}
|
||||
FriendServer1 := meowlib.ServerCard{Name: "FriendServer1", Url: "http://myfriend.org/meow/", PublicKey: FriendServer1KP.Public, Description: "Fancy description"}
|
||||
ReceivedContact.PullServers = append(ReceivedContact.PullServers, &FriendServer1)
|
||||
|
||||
///////////////////////////////////////////////////////
|
||||
@ -100,7 +101,7 @@ func TestEndToEnd(t *testing.T) {
|
||||
packedMsg := MyFirstFriend.PackUserMessage(enc.Data, enc.Signature)
|
||||
|
||||
srv := MyFirstFriend.Contact.PullServers[0]
|
||||
intS1 := client.InternalServerFromServer(srv)
|
||||
intS1 := client.CreateServerFromServerCard(srv)
|
||||
|
||||
// Creating Server message for transporting the user message
|
||||
toServerMessage, err := intS1.BuildMessageSendingMessage(packedMsg)
|
||||
@ -178,11 +179,11 @@ func TestEndToEnd(t *testing.T) {
|
||||
// user unpack
|
||||
|
||||
// user decrypt
|
||||
decMess, err2 := MyFirstFriend.AsymDecryptMessage([]byte(enc.Data), enc.Signature)
|
||||
/*decMess, err2 := MyFirstFriend.AsymDecryptMessage([]byte(enc.Data), enc.Signature)
|
||||
if err2 != nil {
|
||||
fmt.Println(err2.Error())
|
||||
}
|
||||
fmt.Println(decMess)
|
||||
fmt.Println(decMess)*/
|
||||
// user decode protobuf
|
||||
|
||||
}
|
||||
|
16
go.mod
16
go.mod
@ -3,16 +3,18 @@ module forge.redroom.link/yves/meowlib
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.2.4
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.3
|
||||
github.com/go-redis/redis v6.15.9+incompatible
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/google/uuid v1.3.1
|
||||
github.com/makiuchi-d/gozxing v0.1.1
|
||||
github.com/mattn/go-sqlite3 v1.14.16
|
||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||
github.com/onsi/gomega v1.22.1 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/rs/zerolog v1.25.0
|
||||
github.com/stretchr/testify v1.5.1
|
||||
golang.org/x/mobile v0.0.0-20221110043201-43a038452099 // indirect
|
||||
golang.org/x/net v0.3.0 // indirect
|
||||
google.golang.org/protobuf v1.28.1
|
||||
github.com/rs/zerolog v1.31.0
|
||||
github.com/stretchr/testify v1.7.0
|
||||
golang.org/x/net v0.16.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/protobuf v1.31.0
|
||||
)
|
||||
|
120
go.sum
120
go.sum
@ -1,14 +1,17 @@
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20210920160938-87db9fbc61c7 h1:DSqTh6nEes/uO8BlNcGk8PzZsxY2sN9ZL//veWBdTRI=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20210920160938-87db9fbc61c7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1msqzFFb+G80MFmpjMw61IU+slm+wln4=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.2.4 h1:PEke+LAMLH9CplflEl8WqGyz2IiDoiiipKkB+3cEWFQ=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.2.4/go.mod h1:ygdaHbrbWFPhKjmXii0zOs3/xlSR/01GaVePKqv19Hc=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.3 h1:AJu1OI/1UWVYZl6QcCLKGu9OTngS2r52618uGlje84I=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.3/go.mod h1:IhkNEDaxec6NyzSI0PlxapinnwPVIESk8/76da3Ct3g=
|
||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
|
||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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=
|
||||
@ -35,14 +38,19 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/makiuchi-d/gozxing v0.1.1 h1:xxqijhoedi+/lZlhINteGbywIrewVdVv2wl9r5O9S1I=
|
||||
github.com/makiuchi-d/gozxing v0.1.1/go.mod h1:eRIHbOjX7QWxLIDJoQuMLhuXg9LAuw6znsUtRkNw9DU=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
@ -67,68 +75,54 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.25.0 h1:Rj7XygbUHKUlDPcVdoLyR91fJBsduXj5fRxyqIQj/II=
|
||||
github.com/rs/zerolog v1.25.0/go.mod h1:7KHcEGe0QZPOm2IE4Kpb5rTh6n1h2hIgS5OOnu1rUaI=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
||||
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20200801112145-973feb4309de/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
|
||||
golang.org/x/mobile v0.0.0-20221110043201-43a038452099 h1:aIu0lKmfdgtn2uTj7JI2oN4TUrQvgB+wzTPO23bCKt8=
|
||||
golang.org/x/mobile v0.0.0-20221110043201-43a038452099/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.3.0 h1:VWL6FNY2bEEmsGVKabSlHu5Irp34xmMRoqb/9lF9lxk=
|
||||
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos=
|
||||
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -137,9 +131,7 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@ -147,32 +139,44 @@ golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@ -182,8 +186,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
@ -192,7 +196,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
@ -13,7 +13,7 @@ func (msg *UserMessage) AddFile(filename string, maxMessageSize int64) error {
|
||||
return err
|
||||
}
|
||||
if fi.Size() > maxMessageSize {
|
||||
return errors.New("cannot add file, file bigger than messge size")
|
||||
return errors.New("cannot add file, file bigger than message size")
|
||||
}
|
||||
var totalsize int64
|
||||
totalsize = 0
|
||||
@ -35,7 +35,7 @@ func (msg *UserMessage) AddFile(filename string, maxMessageSize int64) error {
|
||||
file.Data = data
|
||||
msg.Files = append(msg.Files, &file)
|
||||
|
||||
msg.Status = &UserMessage_ConversationStatus{}
|
||||
msg.Status = &ConversationStatus{}
|
||||
msg.Status.LocalUuid = uuid.New().String()
|
||||
|
||||
return nil
|
||||
|
1323
messages.pb.go
1323
messages.pb.go
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,14 @@
|
||||
// You should use the field numbers 1 through 15
|
||||
// for the most-frequently-set fields.
|
||||
// Lower field number values take less space in the wire format.
|
||||
// For example, field numbers in the range 1 through 15 take one byte to encode.
|
||||
// Field numbers in the range 16 through 2047 take two bytes.
|
||||
/**
|
||||
* Meow messages
|
||||
*
|
||||
* This is the Meow protocol protobuf messages description.
|
||||
*
|
||||
*/
|
||||
syntax = "proto3";
|
||||
package meowlib;
|
||||
option go_package = "forge.redroom.link/yves/meowlib";
|
||||
@ -9,11 +20,18 @@ message PackedServerMessage {
|
||||
bytes signature = 3; // The message signature with the client public key |eo| the reference to teh symetrical key used
|
||||
}
|
||||
|
||||
// structure defining a message for a server, that will be encrypted, then sent in a "packedmessage" payload
|
||||
message ToServerMessage {
|
||||
string type = 1; // Type 1 : final destination / 2 : forward
|
||||
string from = 2 ; // My pub key for the server to send me an encrypter answer
|
||||
bytes payload = 3 ; // optional payload for server
|
||||
// structure to hold an invitation through a server
|
||||
message Invitation {
|
||||
bytes payload = 1; // invitation payload, encrypted after step 2
|
||||
int32 timeout = 2; // how long do I want the invitation to remain available on the server
|
||||
int32 shortcodeLen = 3; // len of the shortcode you wish for short url transmission
|
||||
string shortcode = 4; // shortcode that the friend shall request to get the invitation
|
||||
string password = 5; // password tou set for accessin invitation (optional)
|
||||
string uuid = 6; // id that the friend gave you, that you should include to your reply to get recognized
|
||||
int64 expiry = 7; // the server allowed expiry date, it may be samller than the requested timeout according to server policy
|
||||
int32 step = 8; // progress in the inviattion process : 1=invite friend, 2=friend requests invitation, 3=friend's answer
|
||||
}
|
||||
|
||||
|
||||
// structure for requesting incoming messages
|
||||
message ConversationRequest {
|
||||
@ -23,16 +41,29 @@ message ToServerMessage {
|
||||
string lookupSignature = 4; // prove that I own the private key by signing that block
|
||||
}
|
||||
|
||||
|
||||
// structure defining a message for a server, that will be encrypted, then sent in a "packedmessage" payload
|
||||
message ToServerMessage {
|
||||
string type = 1; // Type 1 : final destination / 2 : forward
|
||||
string from = 2 ; // My pub key for the server to send me an encrypter answer
|
||||
bytes payload = 3 ; // optional payload for server
|
||||
|
||||
repeated ConversationRequest pullRequest = 4;
|
||||
|
||||
repeated PackedUserMessage messages = 5;
|
||||
|
||||
repeated Server knownServers = 6;
|
||||
repeated ServerCard knownServers = 6;
|
||||
|
||||
Matriochka matriochkaMessage = 7;
|
||||
|
||||
string uuid = 8;
|
||||
|
||||
Invitation invitation = 9; // invitation for the 2 first steps of a "through server" invitation process
|
||||
|
||||
}
|
||||
|
||||
message ConversationResponse {
|
||||
repeated string messageUuids = 1;
|
||||
}
|
||||
|
||||
// structure defining a from server receiver message decrypted from a "packedmessage" payload
|
||||
@ -43,13 +74,11 @@ message FromServerMessage {
|
||||
string uuidAck = 4 ; // Ack for the last received ToServerMessage Uuid
|
||||
string serverUuid = 5 ; // Provides the server uuid that replaced the client uuid
|
||||
|
||||
message ConversationResponse {
|
||||
repeated string messageUuids = 1;
|
||||
}
|
||||
|
||||
repeated PackedUserMessage chat = 6;
|
||||
|
||||
repeated Server knownServers = 7;
|
||||
repeated ServerCard knownServers = 7;
|
||||
|
||||
Invitation invitation = 8; // invitation answer, for the third steps of any invitation
|
||||
|
||||
}
|
||||
|
||||
@ -68,23 +97,23 @@ message Matriochka {
|
||||
}
|
||||
|
||||
// structure describing required server attributes
|
||||
message Server {
|
||||
string name = 1;
|
||||
string description=2;
|
||||
string publicKey = 3;
|
||||
string url = 4;
|
||||
bool publish = 5; // publish this server when asked for a list by server
|
||||
bytes signature = 6; // signature of all previous fields by the server itself
|
||||
int32 confidenceLevel = 7; // additional info from the user
|
||||
message ServerCard {
|
||||
string name = 1; // friendly server name
|
||||
string description=2; // description : owner type (company/private/university...),
|
||||
string publicKey = 3; // public key you must use to send encrypted messages to that server
|
||||
string url = 4; // meow server url
|
||||
string login = 5; // required login to access the server
|
||||
string password = 6; // password associated to the login
|
||||
string signature = 7; // signature of all previous fields by the server itself
|
||||
}
|
||||
|
||||
// structure describing a user contact card ie the minimum set of attributes for exchanging identities
|
||||
message ContactCard {
|
||||
string name=1;
|
||||
string contactPublicKey =2;
|
||||
string encryptionPublicKey= 3;
|
||||
string lookupPublicKey =4;
|
||||
repeated Server pullServers =5;
|
||||
string name=1; // contact nickname
|
||||
string contactPublicKey =2; // contact public key, will be used to authenticate her/his messages
|
||||
string encryptionPublicKey= 3; // public key you must use to to write encrypted messages to that contact
|
||||
string lookupPublicKey =4; // public key you will use as "destination identifier" for her/him to lookup for your messages on the servers
|
||||
repeated ServerCard pullServers =5; // list the servers where the contact will look for messages from you
|
||||
uint32 version = 6;
|
||||
string invitationId=7;
|
||||
}
|
||||
@ -97,13 +126,6 @@ message PackedUserMessage {
|
||||
repeated int64 serverTimestamp=4; // server time stamp, might be several in matriochka mode
|
||||
}
|
||||
|
||||
|
||||
// structure defining information that might be exchanged between two peers.
|
||||
message UserMessage {
|
||||
string destination = 1; // Lookupkey
|
||||
string from = 2; // My public key for that contact
|
||||
string type = 3;
|
||||
bytes data = 4;
|
||||
message ConversationStatus {
|
||||
string localUuid = 1;
|
||||
uint64 localSequence = 2 ;
|
||||
@ -113,24 +135,46 @@ message UserMessage {
|
||||
ContactCard myNextIdentity = 6;
|
||||
int32 peerNextIdentityAck = 7; // version of the new peer accepted id
|
||||
}
|
||||
ConversationStatus Status = 5;
|
||||
|
||||
ContactCard contact = 6;
|
||||
|
||||
Server knownServers = 7;
|
||||
|
||||
message Group{
|
||||
string name=1;
|
||||
repeated ContactCard members = 2;
|
||||
}
|
||||
|
||||
// structure defining information that might be exchanged between two peers.
|
||||
message UserMessage {
|
||||
string destination = 1; // Lookupkey
|
||||
string from = 2; // My public key for that contact
|
||||
string type = 3;
|
||||
bytes data = 4;
|
||||
|
||||
ConversationStatus Status = 5;
|
||||
|
||||
ContactCard contact = 6;
|
||||
|
||||
ServerCard knownServers = 7;
|
||||
|
||||
Group group = 8;
|
||||
|
||||
repeated File files = 9;
|
||||
|
||||
Location currentLocation = 10;
|
||||
|
||||
bytes appdata = 11;
|
||||
|
||||
Invitation invitation = 12;
|
||||
}
|
||||
|
||||
message File {
|
||||
string filename=1;
|
||||
uint64 size=2;
|
||||
uint32 chunk=3;
|
||||
bytes data=4;
|
||||
string filename=1; // the proposed filename
|
||||
uint64 size=2; // the file size
|
||||
uint32 chunk=3; // the chunk counter if file is sent by chunks
|
||||
bytes data=4; // the file/chunk content
|
||||
}
|
||||
|
||||
message Location {
|
||||
uint64 time=1;
|
||||
float latitude=2;
|
||||
float longitude=3;
|
||||
int32 altitude=4;
|
||||
}
|
@ -3,4 +3,10 @@ protoc -I=. --go_out=.. messages.proto
|
||||
mv ../forge.redroom.link/yves/meowlib/messages.pb.go ../
|
||||
rm -rf ../forge.redroom.link
|
||||
|
||||
protoc -I=. --dart_out=../../../flutter/meowlib/lib/ messages.proto
|
||||
protoc --plugin=protoc-gen-doc=/usr/bin/protoc-gen-doc \
|
||||
--doc_out=../doc/generated \
|
||||
--doc_opt=html,index.html \
|
||||
*.proto
|
||||
|
||||
protoc --plugin=protoc-gen-uml=/usr/bin/protoc-gen-uml \
|
||||
--uml_out=../doc/generated -I=. *.proto
|
||||
|
@ -21,7 +21,7 @@ type Identity struct {
|
||||
OwnerName string `json:"owner_name,omitempty"`
|
||||
OwnerPublicKey string `json:"owner_public_key,omitempty"`
|
||||
ArchiveClients []string `json:"archive_clients,omitempty"`
|
||||
KnownServers []meowlib.Server `json:"known_servers,omitempty"`
|
||||
KnownServers []meowlib.ServerCard `json:"known_servers,omitempty"`
|
||||
}
|
||||
|
||||
func CreateIdentity(ServerName string, ServerDesc string) *Identity {
|
||||
|
70
server/invitation.go
Normal file
70
server/invitation.go
Normal file
@ -0,0 +1,70 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
)
|
||||
|
||||
func (r *RedisRouter) CreateInvitation(invitation []byte, timeout int, password string, serverTimeout int, urlLen int) (string, time.Time) {
|
||||
id := r.createShortId(urlLen)
|
||||
if timeout > serverTimeout {
|
||||
timeout = serverTimeout
|
||||
}
|
||||
r.Client.Set("mwiv:"+id, invitation, time.Duration(timeout*1000000))
|
||||
if len(password) > 0 {
|
||||
r.Client.Set("mwpw:"+id, password, time.Duration(timeout*1000000))
|
||||
}
|
||||
return id, time.Now().Add(time.Duration(timeout * 1000000)).UTC()
|
||||
}
|
||||
|
||||
func (r *RedisRouter) GetInvitation(id string, password string) ([]byte, error) {
|
||||
passRequired := false
|
||||
expectedpass, err := r.Client.Get("mwpw:" + id).Result()
|
||||
if err != nil {
|
||||
passRequired = false
|
||||
} else {
|
||||
passRequired = true
|
||||
}
|
||||
if passRequired && password != expectedpass {
|
||||
return nil, errors.New("auth failed")
|
||||
}
|
||||
mwiv, err := r.Client.Get("mwiv:" + id).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(mwiv), nil
|
||||
}
|
||||
|
||||
func (r *RedisRouter) AnswerInvitation(id string, timeout int, invitation []byte, serverTimeout int) time.Time {
|
||||
if timeout > serverTimeout {
|
||||
timeout = serverTimeout
|
||||
}
|
||||
r.Client.Set("mwan:"+id, invitation, time.Duration(timeout*1000000))
|
||||
return time.Now().Add(time.Duration(timeout * 1000000)).UTC()
|
||||
}
|
||||
|
||||
func (r *RedisRouter) GetInvitationAnswer(id string) ([]byte, error) {
|
||||
mwan, err := r.Client.Get("mwiv:" + id).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(mwan), nil
|
||||
}
|
||||
|
||||
func (r *RedisRouter) createShortId(length int) string {
|
||||
id := ""
|
||||
alphabet := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
// for not in redis
|
||||
for {
|
||||
for i := 1; i <= length; i++ {
|
||||
id += string(alphabet[rand.Intn(61)])
|
||||
}
|
||||
if r.Client.Get("mwiv:"+id).Err() == redis.Nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return id
|
||||
}
|
@ -13,25 +13,43 @@ type RedisRouter struct {
|
||||
Name string
|
||||
ServerIdentity *Identity
|
||||
Client *redis.Client
|
||||
InvitationTimeout int
|
||||
Context context.Context
|
||||
}
|
||||
|
||||
func NewRedisRouter(server *Identity, url string, password string, db int) *RedisRouter {
|
||||
func NewRedisRouter(server *Identity, redisUrl string, password string, db int, invitationTimeout int) *RedisRouter {
|
||||
var r RedisRouter
|
||||
r.ServerIdentity = server
|
||||
r.Name = "Redis"
|
||||
r.Client = redis.NewClient(&redis.Options{
|
||||
Addr: url,
|
||||
Addr: redisUrl,
|
||||
Password: password,
|
||||
DB: db,
|
||||
})
|
||||
r.InvitationTimeout = invitationTimeout
|
||||
r.Context = context.Background()
|
||||
// set start for uptime
|
||||
err := r.Client.Set("statistics:start", time.Now().UTC().Format(time.RFC3339), 0).Err()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &r
|
||||
}
|
||||
|
||||
func (r *RedisRouter) Route(msg *meowlib.ToServerMessage) (*meowlib.FromServerMessage, error) {
|
||||
var from_server meowlib.FromServerMessage
|
||||
if len(msg.Messages) > 0 { // user message
|
||||
// update messages counter
|
||||
err := r.Client.Incr("statistics:messages:total").Err()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// user message
|
||||
if len(msg.Messages) > 0 {
|
||||
// update messages counter
|
||||
err := r.Client.Incr("statistics:messages:usermessages").Err()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, usrmsg := range msg.Messages {
|
||||
// serialize the message to store it as byte array into redis
|
||||
out, err := proto.Marshal(usrmsg)
|
||||
@ -42,7 +60,13 @@ func (r *RedisRouter) Route(msg *meowlib.ToServerMessage) (*meowlib.FromServerMe
|
||||
}
|
||||
from_server.UuidAck = msg.Uuid
|
||||
}
|
||||
// check for messages
|
||||
if len(msg.PullRequest) > 0 {
|
||||
// update messages counter
|
||||
err := r.Client.Incr("statistics:messages:messagelookups").Err()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, rq := range msg.PullRequest {
|
||||
msgcnt, err := r.Client.ZCount(rq.LookupKey, "-inf", "+inf").Result()
|
||||
if err != nil {
|
||||
@ -69,8 +93,13 @@ func (r *RedisRouter) Route(msg *meowlib.ToServerMessage) (*meowlib.FromServerMe
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// manage Matriochka
|
||||
if msg.MatriochkaMessage != nil {
|
||||
// update messages counter
|
||||
err := r.Client.Incr("statistics:messages:matriochka").Err()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
out, err := proto.Marshal(msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -81,9 +110,47 @@ func (r *RedisRouter) Route(msg *meowlib.ToServerMessage) (*meowlib.FromServerMe
|
||||
}
|
||||
from_server.UuidAck = msg.Uuid
|
||||
}
|
||||
|
||||
// Server list exchange
|
||||
if len(msg.KnownServers) > 0 {
|
||||
|
||||
}
|
||||
// Through server invitation process
|
||||
if msg.Invitation != nil {
|
||||
// update messages counter
|
||||
err := r.Client.Incr("statistics:messages:invitation").Err()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
switch msg.Invitation.Step {
|
||||
case 1: // create invitation
|
||||
url, expiry := r.CreateInvitation(msg.Invitation.Payload, int(msg.Invitation.Timeout), msg.Invitation.Password, r.InvitationTimeout, int(msg.Invitation.ShortcodeLen))
|
||||
from_server.Invitation.Shortcode = url
|
||||
from_server.Invitation.Expiry = expiry.UTC().Unix()
|
||||
case 2: // get invitation
|
||||
invitation, err := r.GetInvitation(msg.Invitation.Shortcode, msg.Invitation.Password)
|
||||
if err != nil {
|
||||
if err.Error() == "auth failed" {
|
||||
from_server.Invitation.Payload = []byte("authentication failure")
|
||||
} else {
|
||||
from_server.Invitation.Payload = []byte("invitation expired")
|
||||
}
|
||||
} else {
|
||||
from_server.Invitation.Payload = invitation
|
||||
}
|
||||
|
||||
/* should not happen
|
||||
case 3: // answer invitation
|
||||
expiry := r.AnswerInvitation(msg.Invitation.Id, int(msg.Invitation.Timeout), msg.Invitation.Payload, r.InvitationTimeout)
|
||||
from_server.Invitation.Expiry = expiry.UTC().Unix()
|
||||
case 4: // get answer
|
||||
answer, err := r.GetInvitationAnswer(msg.Invitation.Id)
|
||||
if err != nil {
|
||||
from_server.Invitation.Payload = []byte("invitation expired")
|
||||
} else {
|
||||
from_server.Invitation.Payload = answer
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
/*
|
||||
|
||||
|
Reference in New Issue
Block a user