peers separate
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
ycc 2024-05-28 16:47:04 +02:00
parent c1883f1524
commit e674a0cb33
11 changed files with 164 additions and 79 deletions

View File

@ -36,8 +36,8 @@ func ReadCallRequestResponseMessage(data []byte, srvuid string) (*meowlib.VideoD
return serverMsg.VideoData, "", nil return serverMsg.VideoData, "", nil
} }
func BuildCallMessage(videodata *meowlib.VideoData, srvuid string, peer_idx int, replyToUid string, filelist []string) ([]byte, string, error) { func BuildCallMessage(videodata *meowlib.VideoData, srvuid string, peer_uid string, replyToUid string, filelist []string) ([]byte, string, error) {
peer := client.GetConfig().GetIdentity().Peers[peer_idx] peer := client.GetConfig().GetIdentity().Peers.GetFromUid(peer_uid)
// Creating User message // Creating User message
usermessage, err := peer.BuildSimpleUserMessage(nil) usermessage, err := peer.BuildSimpleUserMessage(nil)

View File

@ -91,14 +91,16 @@ func InvitationAnswerFile(invitationFile string, nickname string, myNickname str
func InvitationAnswerMessage(invitationId string, invitationServerUid string, timeout int) ([]byte, string, error) { func InvitationAnswerMessage(invitationId string, invitationServerUid string, timeout int) ([]byte, string, error) {
// find the peer with that invitation id // find the peer with that invitation id
var peer *client.Peer /*var peer *client.Peer
for i := len(client.GetConfig().GetIdentity().Peers) - 1; i >= 0; i-- { //! to allow self invitation : testing only, findinc the received peer before myself for i := len(client.GetConfig().GetIdentity().Peers) - 1; i >= 0; i-- { //! to allow self invitation : testing only, findinc the received peer before myself
// for i := 0; i < len(client.GetConfig().GetIdentity().Peers); i++ { // for i := 0; i < len(client.GetConfig().GetIdentity().Peers); i++ {
if client.GetConfig().GetIdentity().Peers[i].InvitationId == invitationId { if client.GetConfig().GetIdentity().Peers[i].InvitationId == invitationId {
peer = client.GetConfig().GetIdentity().Peers[i] peer = client.GetConfig().GetIdentity().Peers[i]
break break
} }
} }*/
peer := client.GetConfig().GetIdentity().Peers.GetFromInvitationId(invitationId)
if peer == nil { if peer == nil {
// declare a custom go error for no peer found // declare a custom go error for no peer found
return nil, "InvitationAnswerMessage: loop for peer", errors.New("no peer with that invitation id") return nil, "InvitationAnswerMessage: loop for peer", errors.New("no peer with that invitation id")

View File

@ -73,12 +73,14 @@ func InvitationCreateMessage(invitationId string, invitationServerUid string, ti
// lookup for peer with "invitation_id" // lookup for peer with "invitation_id"
var myContact *meowlib.ContactCard var myContact *meowlib.ContactCard
for i := 0; i < len(client.GetConfig().GetIdentity().Peers); i++ { /* for i := 0; i < len(client.GetConfig().GetIdentity().Peers); i++ {
if client.GetConfig().GetIdentity().Peers[i].InvitationId == invitationId { if client.GetConfig().GetIdentity().Peers[i].InvitationId == invitationId {
myContact = client.GetConfig().GetIdentity().Peers[i].GetMyContact() myContact = client.GetConfig().GetIdentity().Peers[i].GetMyContact()
break break
} }
} }*/
peer := client.GetConfig().GetIdentity().Peers.GetFromInvitationId(invitationId)
myContact = peer.GetMyContact()
// todo handle not found !! // todo handle not found !!
// lookup for message server with "invitation_server" // lookup for message server with "invitation_server"
invitationServer, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(invitationServerUid) //.GetServerByIdx(int(jsoninv["invitation_server"].(float64))) invitationServer, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(invitationServerUid) //.GetServerByIdx(int(jsoninv["invitation_server"].(float64)))
@ -122,14 +124,20 @@ func InvitationCreateReadResponse(invitationServerUid string, invitationResponse
// invitationId: the invitation id of the peer // invitationId: the invitation id of the peer
// url: the url of the invitation we got from the server // url: the url of the invitation we got from the server
func InvitationSetUrlInfo(invitationId string, url string, expiry int64) { func InvitationSetUrlInfo(invitationId string, url string, expiry int64) {
id := client.GetConfig().GetIdentity()
// lookup for peer with "invitation_id"
peer := id.Peers.GetFromInvitationId(invitationId)
peer.InvitationUrl = url
peer.InvitationExpiry = time.Unix(expiry, 0)
id.Peers.StorePeer(peer)
for i := 0; i < len(client.GetConfig().GetIdentity().Peers); i++ { /* for i := 0; i < len(client.GetConfig().GetIdentity().Peers); i++ {
if client.GetConfig().GetIdentity().Peers[i].InvitationId == invitationId { if client.GetConfig().GetIdentity().Peers[i].InvitationId == invitationId {
client.GetConfig().GetIdentity().Peers[i].InvitationUrl = url client.GetConfig().GetIdentity().Peers[i].InvitationUrl = url
client.GetConfig().GetIdentity().Peers[i].InvitationExpiry = time.Unix(expiry, 0) client.GetConfig().GetIdentity().Peers[i].InvitationExpiry = time.Unix(expiry, 0)
break break
}
} }
} client.GetConfig().GetIdentity().Save()*/
client.GetConfig().GetIdentity().Save()
} }

View File

@ -30,9 +30,9 @@ func messageBuildPostprocess(msg *meowlib.UserMessage, srvuid string, peer *clie
return data, "", nil return data, "", nil
} }
func PrepareUserMessage(message string, srvuid string, peer_idx int, replyToUid string, filelist []string) ([]byte, string, error) { func PrepareUserMessage(message string, srvuid string, peer_uid string, replyToUid string, filelist []string) ([]byte, string, error) {
peer := client.GetConfig().GetIdentity().Peers[peer_idx] peer := client.GetConfig().GetIdentity().Peers.GetFromUid(peer_uid)
// Creating User message // Creating User message
usermessage, err := peer.BuildSimpleUserMessage([]byte(message)) usermessage, err := peer.BuildSimpleUserMessage([]byte(message))
@ -51,9 +51,9 @@ func PrepareUserMessage(message string, srvuid string, peer_idx int, replyToUid
} }
func BuildAckMessage(messageUid string, srvuid string, peer_idx int, received int64, processed int64) ([]byte, string, error) { func BuildAckMessage(messageUid string, srvuid string, peer_uid string, received int64, processed int64) ([]byte, string, error) {
peer := client.GetConfig().GetIdentity().Peers[peer_idx] peer := client.GetConfig().GetIdentity().Peers.GetFromUid(peer_uid)
srv, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(srvuid) srv, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(srvuid)
if err != nil { if err != nil {
return nil, "PrepareServerMessage : LoadServer", err return nil, "PrepareServerMessage : LoadServer", err

View File

@ -4,9 +4,9 @@ import (
"forge.redroom.link/yves/meowlib/client" "forge.redroom.link/yves/meowlib/client"
) )
func LoadMessagesHistory(peer_id int) ([]client.InternalUserMessage, string, error) { func LoadMessagesHistory(peer_uid string) ([]client.InternalUserMessage, string, error) {
id := client.GetConfig().GetIdentity() id := client.GetConfig().GetIdentity()
peer := id.Peers[peer_id] peer := id.Peers.GetFromUid(peer_uid)
msgs, err := peer.LoadMessagesHistory(0, 0, 50) msgs, err := peer.LoadMessagesHistory(0, 0, 50)
if err != nil { if err != nil {
return nil, "LoadLastMessages: LoadMessagesHistory", err return nil, "LoadLastMessages: LoadMessagesHistory", err

View File

@ -21,7 +21,7 @@ type Identity struct {
Avatars []Avatar `json:"avatars,omitempty"` Avatars []Avatar `json:"avatars,omitempty"`
RootKp meowlib.KeyPair `json:"id_kp,omitempty"` RootKp meowlib.KeyPair `json:"id_kp,omitempty"`
Status string `json:"status,omitempty"` Status string `json:"status,omitempty"`
Peers PeerList `json:"peers,omitempty"` Peers PeerStorage `json:"peers,omitempty"`
HiddenPeers [][]byte `json:"hidden_peers,omitempty"` HiddenPeers [][]byte `json:"hidden_peers,omitempty"`
Personae PeerList `json:"faces,omitempty"` Personae PeerList `json:"faces,omitempty"`
Device meowlib.KeyPair `json:"device,omitempty"` Device meowlib.KeyPair `json:"device,omitempty"`
@ -96,7 +96,7 @@ func (id *Identity) InvitePeer(MyName string, ContactName string, MessageServerU
peer.MyPullServers = MessageServerUids peer.MyPullServers = MessageServerUids
peer.MyName = MyName peer.MyName = MyName
peer.InvitationMessage = InvitationMessage peer.InvitationMessage = InvitationMessage
id.Peers = append(id.Peers, &peer) id.Peers.StorePeer(&peer)
return &peer, nil return &peer, nil
} }
@ -104,14 +104,15 @@ func (id *Identity) InvitePeer(MyName string, ContactName string, MessageServerU
// Checks if the received contact card is an answer to an invitation, returns true if it is, and the proposed and received nicknames // Checks if the received contact card is an answer to an invitation, returns true if it is, and the proposed and received nicknames
func (id *Identity) CheckInvitation(ReceivedContact *meowlib.ContactCard) (isAnswer bool, proposedNick string, receivedNick string, invitationMessage string) { func (id *Identity) CheckInvitation(ReceivedContact *meowlib.ContactCard) (isAnswer bool, proposedNick string, receivedNick string, invitationMessage string) {
// invitation Id found, this is an answer to an invitation // invitation Id found, this is an answer to an invitation
for _, p := range id.Peers { /*for _, p := range id.Peers {
if p.InvitationId == ReceivedContact.InvitationId { if p.InvitationId == ReceivedContact.InvitationId {
return true, p.Name, ReceivedContact.Name, ReceivedContact.InvitationMessage return true, p.Name, ReceivedContact.Name, ReceivedContact.InvitationMessage
} }
} }
// it's an invitation // it's an invitation
return false, "", ReceivedContact.Name, ReceivedContact.InvitationMessage return false, "", ReceivedContact.Name, ReceivedContact.InvitationMessage*/
return id.Peers.CheckInvitation(ReceivedContact)
} }
// Answers an invitation, returns the newly created peer including infos to provide a ContactCard // Answers an invitation, returns the newly created peer including infos to provide a ContactCard
@ -146,14 +147,14 @@ func (id *Identity) AnswerInvitation(MyName string, ContactName string, MessageS
peer.MyPullServers = MessageServerIdxs peer.MyPullServers = MessageServerIdxs
peer.MyName = MyName peer.MyName = MyName
peer.InvitationId = ReceivedContact.InvitationId peer.InvitationId = ReceivedContact.InvitationId
id.Peers = append(id.Peers, &peer) id.Peers.StorePeer(&peer)
return &peer return &peer
} }
// Finalizes an invitation, returns nil if successful // Finalizes an invitation, returns nil if successful
func (id *Identity) FinalizeInvitation(ReceivedContact *meowlib.ContactCard) error { func (id *Identity) FinalizeInvitation(ReceivedContact *meowlib.ContactCard) error {
for i, p := range id.Peers { /*for i, p := range id.Peers {
if p.InvitationId == ReceivedContact.InvitationId { if p.InvitationId == ReceivedContact.InvitationId {
//id.Peers[i].Name = ReceivedContact.Name //id.Peers[i].Name = ReceivedContact.Name
id.Peers[i].ContactEncryption = ReceivedContact.EncryptionPublicKey id.Peers[i].ContactEncryption = ReceivedContact.EncryptionPublicKey
@ -167,7 +168,9 @@ func (id *Identity) FinalizeInvitation(ReceivedContact *meowlib.ContactCard) err
return nil return nil
} }
} }
return errors.New("no matching contact found for invitationId " + ReceivedContact.InvitationId) return errors.New("no matching contact found for invitationId " + ReceivedContact.InvitationId)*/
return id.Peers.FinalizeInvitation(ReceivedContact)
} }
// LoadIdentity loads an identity from an encrypted file // LoadIdentity loads an identity from an encrypted file
@ -225,6 +228,7 @@ func (id *Identity) TryUnlockHidden(password string) error {
return errors.New("no peer found") return errors.New("no peer found")
} }
/*
func (id *Identity) HidePeer(peerIdx int, password string) error { func (id *Identity) HidePeer(peerIdx int, password string) error {
serializedPeer, err := json.Marshal(id.Peers[peerIdx]) serializedPeer, err := json.Marshal(id.Peers[peerIdx])
if err != nil { if err != nil {
@ -239,13 +243,14 @@ func (id *Identity) HidePeer(peerIdx int, password string) error {
// remove clear text peer // remove clear text peer
id.Peers = append(id.Peers[:peerIdx], id.Peers[peerIdx+1:]...) id.Peers = append(id.Peers[:peerIdx], id.Peers[peerIdx+1:]...)
return nil return nil
} }*/
func (id *Identity) generateRandomHiddenStuff() { func (id *Identity) generateRandomHiddenStuff() {
r := rand.New(rand.NewSource(time.Now().UnixNano())) r := rand.New(rand.NewSource(time.Now().UnixNano()))
count := r.Intn(maxHiddenCount) + 1 count := r.Intn(maxHiddenCount) + 1
for i := 1; i < count; i++ { for i := 1; i < count; i++ {
var p Peer var p Peer
p.Uid = uuid.New().String()
p.Name = randomLenString(4, 20) p.Name = randomLenString(4, 20)
p.MyEncryptionKp = meowlib.NewKeyPair() p.MyEncryptionKp = meowlib.NewKeyPair()
p.MyIdentity = meowlib.NewKeyPair() p.MyIdentity = meowlib.NewKeyPair()
@ -254,9 +259,10 @@ func (id *Identity) generateRandomHiddenStuff() {
p.ContactPublicKey = p.MyLookupKp.Public p.ContactPublicKey = p.MyLookupKp.Public
p.ContactEncryption = p.MyIdentity.Public p.ContactEncryption = p.MyIdentity.Public
p.ContactLookupKey = p.MyEncryptionKp.Public p.ContactLookupKey = p.MyEncryptionKp.Public
p.dbPassword = randomLenString(8, 14)
// p.Contact.AddUrls([]string{randomLenString(14, 60), randomLenString(14, 60)}) // todo add servers // p.Contact.AddUrls([]string{randomLenString(14, 60), randomLenString(14, 60)}) // todo add servers
id.Peers = append(id.Peers, &p) id.Peers.StorePeer(&p)
id.HidePeer(0, randomLenString(8, 14)) //id.HidePeer(0, randomLenString(8, 14))
// TODO Add random conversations // TODO Add random conversations
} }
} }
@ -285,7 +291,11 @@ func (id *Identity) GetRequestJobs() []*RequestsJob {
srvs[server.GetServerCard().GetUid()] = &rj srvs[server.GetServerCard().GetUid()] = &rj
} }
// add ids to the map // add ids to the map
for _, peer := range id.Peers { peers, err := id.Peers.GetPeers()
if err != nil {
return nil
}
for _, peer := range peers {
// check if peer inviation is accepted // check if peer inviation is accepted
for _, server := range peer.MyPullServers { for _, server := range peer.MyPullServers {
srvs[server].LookupKeys = append(srvs[server].LookupKeys, peer.MyLookupKp) srvs[server].LookupKeys = append(srvs[server].LookupKeys, peer.MyLookupKp)

View File

@ -7,6 +7,7 @@ import (
"testing" "testing"
"forge.redroom.link/yves/meowlib" "forge.redroom.link/yves/meowlib"
"github.com/google/uuid"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -32,17 +33,21 @@ func createId() *Identity {
if err != nil { if err != nil {
log.Fatal("Save failed") log.Fatal("Save failed")
} }
var p Peer for i := range 10 {
p.Name = "testName" var p Peer
p.MyEncryptionKp = meowlib.NewKeyPair() p.Uid = uuid.New().String()
p.MyIdentity = meowlib.NewKeyPair() p.Name = "testName_" + strconv.Itoa(i)
p.MyLookupKp = meowlib.NewKeyPair() p.MyEncryptionKp = meowlib.NewKeyPair()
p.Name = "foo" p.MyIdentity = meowlib.NewKeyPair()
p.ContactPublicKey = p.MyLookupKp.Public p.MyLookupKp = meowlib.NewKeyPair()
p.ContactEncryption = p.MyIdentity.Public p.Name = "foo_" + strconv.Itoa(i)
p.ContactLookupKey = p.MyEncryptionKp.Public p.ContactPublicKey = meowlib.NewKeyPair().Public
//p.Contact.AddUrls([]string{"http:/127.0.0.1/meow", "tcp://localhost:1234"}) //todo add servers p.ContactEncryption = meowlib.NewKeyPair().Public
id.Peers = append(id.Peers, &p) p.ContactLookupKey = meowlib.NewKeyPair().Public
p.MyPullServers = []string{"server1", "server2"}
//p.Contact.AddUrls([]string{"http:/127.0.0.1/meow", "tcp://localhost:1234"}) //todo add servers
id.Peers.StorePeer(&p)
}
return id return id
} }
@ -72,42 +77,28 @@ func TestLoad(t *testing.T) {
} }
func TestHidePeer(t *testing.T) { func TestHidePeer(t *testing.T) {
id := createId() /*
name := id.Peers[0].Name id := createId()
assert.Equal(t, len(id.Peers), 1) name := id.Peers[0].Name
h := len(id.HiddenPeers) assert.Equal(t, len(id.Peers), 1)
id.HidePeer(0, "mypassword") h := len(id.HiddenPeers)
assert.Equal(t, len(id.Peers), 0) id.HidePeer(0, "mypassword")
assert.Equal(t, len(id.HiddenPeers), h+1) assert.Equal(t, len(id.Peers), 0)
id.TryUnlockHidden("mypassword") assert.Equal(t, len(id.HiddenPeers), h+1)
assert.Equal(t, len(id.unlockedHiddenPeers), 1) id.TryUnlockHidden("mypassword")
assert.Equal(t, id.unlockedHiddenPeers[0].Name, name) assert.Equal(t, len(id.unlockedHiddenPeers), 1)
if exists("test.id") { assert.Equal(t, id.unlockedHiddenPeers[0].Name, name)
os.Remove("test.id")
} if exists("test.id") {
os.Remove("test.id")
}
*/
} }
// test GetRequestJobs // test GetRequestJobs
func TestGetRequestJobs(t *testing.T) { func TestGetRequestJobs(t *testing.T) {
// Create a mock Identity object // Create a mock Identity object
id := &Identity{ id := createId()
Peers: []*Peer{
{
MyPullServers: []string{"server1", "server2"},
MyLookupKp: meowlib.NewKeyPair(),
},
{
MyPullServers: []string{"server3", "server4"},
MyLookupKp: meowlib.NewKeyPair(),
},
},
unlockedHiddenPeers: []*Peer{
{
MyPullServers: []string{"server5", "server6"},
MyLookupKp: meowlib.NewKeyPair(),
},
},
}
id.MessageServers = ServerStorage{ id.MessageServers = ServerStorage{
DbFile: "test.db", DbFile: "test.db",
} }

View File

@ -14,11 +14,15 @@ func TestStoreMessage(t *testing.T) {
id := createId() id := createId()
var um meowlib.UserMessage var um meowlib.UserMessage
um.Data = []byte("blabla") um.Data = []byte("blabla")
err := StoreMessage(id.Peers[0], &um, []string{}, GetConfig().memoryPassword) peers, err := id.Peers.GetPeers()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
messages, err := GetMessagesHistory(id.Peers[0], 0, 0, 10, GetConfig().memoryPassword) err = StoreMessage(peers[0], &um, []string{}, GetConfig().memoryPassword)
if err != nil {
log.Fatal(err)
}
messages, err := GetMessagesHistory(peers[0], 0, 0, 10, GetConfig().memoryPassword)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -42,8 +46,9 @@ func TestStoreMessage(t *testing.T) {
func TestManyStoreMessage(t *testing.T) { func TestManyStoreMessage(t *testing.T) {
id := createId() id := createId()
peers, err := id.Peers.GetPeers()
// test with zero messages // test with zero messages
messages, err := GetMessagesHistory(id.Peers[0], 0, 0, 10, GetConfig().memoryPassword) messages, err := GetMessagesHistory(peers[0], 0, 0, 10, GetConfig().memoryPassword)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -51,12 +56,12 @@ func TestManyStoreMessage(t *testing.T) {
for i := 1; i < 100; i++ { for i := 1; i < 100; i++ {
var um meowlib.UserMessage var um meowlib.UserMessage
um.Data = []byte(randomLenString(20, 200)) um.Data = []byte(randomLenString(20, 200))
err := StoreMessage(id.Peers[0], &um, []string{}, GetConfig().memoryPassword) err := StoreMessage(peers[0], &um, []string{}, GetConfig().memoryPassword)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
messages, err = GetMessagesHistory(id.Peers[0], 0, 0, 10, GetConfig().memoryPassword) messages, err = GetMessagesHistory(peers[0], 0, 0, 10, GetConfig().memoryPassword)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@ -4,6 +4,7 @@ import (
"strconv" "strconv"
"testing" "testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -15,9 +16,13 @@ func TestGetFromPublicKey(t *testing.T) {
id.Save() id.Save()
for i := 1; i < 10; i++ { for i := 1; i < 10; i++ {
var p Peer var p Peer
p.Uid = uuid.New().String()
p.Name = "test" + strconv.Itoa(i) p.Name = "test" + strconv.Itoa(i)
p.ContactPublicKey = "stringToFind" + strconv.Itoa(i) p.ContactPublicKey = "stringToFind" + strconv.Itoa(i)
id.Peers = append(id.Peers, &p) err := id.Peers.StorePeer(&p)
if err != nil {
t.Fatal("StorePeer failed")
}
} }
p5 := id.Peers.GetFromPublicKey("stringToFind5") p5 := id.Peers.GetFromPublicKey("stringToFind5")
assert.Equal(t, p5.Name, "test5") assert.Equal(t, p5.Name, "test5")

View File

@ -12,6 +12,7 @@ import (
"forge.redroom.link/yves/meowlib" "forge.redroom.link/yves/meowlib"
"github.com/dgraph-io/badger" "github.com/dgraph-io/badger"
"github.com/google/uuid"
) )
type PeerStorage struct { type PeerStorage struct {
@ -22,7 +23,12 @@ type PeerStorage struct {
// Open the badger database from struct PeerStorage // Open the badger database from struct PeerStorage
func (ps *PeerStorage) open() error { func (ps *PeerStorage) open() error {
if ps.DbFile == "" {
ps.DbFile = uuid.New().String()
}
if ps.cache == nil {
ps.cache = make(map[string]*Peer)
}
opts := badger.DefaultOptions(filepath.Join(GetConfig().StoragePath, GetConfig().GetIdentity().Uuid, ps.DbFile)) opts := badger.DefaultOptions(filepath.Join(GetConfig().StoragePath, GetConfig().GetIdentity().Uuid, ps.DbFile))
opts.Logger = nil opts.Logger = nil
var err error var err error
@ -55,6 +61,8 @@ func (ps *PeerStorage) StorePeer(peer *Peer) error {
} }
shakey := sha256.Sum256([]byte(peer.Uid)) shakey := sha256.Sum256([]byte(peer.Uid))
key := shakey[:] key := shakey[:]
// add it to cache
ps.cache[peer.Uid] = peer
// then store it in the database // then store it in the database
return ps.db.Update(func(txn *badger.Txn) error { return ps.db.Update(func(txn *badger.Txn) error {
return txn.Set(key, data) return txn.Set(key, data)

56
client/test.id Normal file
View File

@ -0,0 +1,56 @@
-----BEGIN PGP MESSAGE-----
Version: GopenPGP 2.7.5
Comment: https://gopenpgp.org
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
oSo1
=gq+d
-----END PGP MESSAGE-----