diff --git a/app/app.go b/app/app.go index c095ee1..016d6ec 100644 --- a/app/app.go +++ b/app/app.go @@ -157,7 +157,6 @@ func (ac *applicationCore) LoadProfiles(password string, timeline bool, loadProf } for _, file := range files { - // Attempt to load an encrypted database profileDirectory := path.Join(ac.directory, "profiles", file.Name()) profile, err := peer.FromEncryptedDatabase(profileDirectory, password) @@ -167,7 +166,7 @@ func (ac *applicationCore) LoadProfiles(password string, timeline bool, loadProf loadProfileFn(profile) } - // On failure + // On failure attempt to load a legacy profile if err != nil { eventBus := event.NewEventManager() @@ -176,19 +175,11 @@ func (ac *applicationCore) LoadProfiles(password string, timeline bool, loadProf continue } - profile := profileStore.GetProfileCopy(timeline) - - _, exists := ac.eventBuses[profile.Onion] - if exists { - eventBus.Shutdown() - log.Errorf("profile for onion %v already exists", profile.Onion) - continue - } - - ac.coremutex.Lock() - ac.eventBuses[profile.Onion] = eventBus - ac.coremutex.Unlock() + legacyProfile := profileStore.GetProfileCopy(timeline) + cps, err := peer.CreateEncryptedStore(profileDirectory, password) + profile := peer.ImportLegacyProfile(legacyProfile, cps) + loadProfileFn(profile) } } return nil @@ -198,15 +189,21 @@ func (ac *applicationCore) LoadProfiles(password string, timeline bool, loadProf func (app *application) LoadProfiles(password string) { count := 0 app.applicationCore.LoadProfiles(password, true, func(profile peer.CwtchPeer) { - eventBus := event.NewEventManager() - app.eventBuses[profile.GetOnion()] = eventBus - profile.Init(app.eventBuses[profile.GetOnion()]) app.appmutex.Lock() - app.peers[profile.GetOnion()] = profile - app.engines[profile.GetOnion()], _ = profile.GenerateProtocolEngine(app.acn, app.eventBuses[profile.GetOnion()]) + // Only attempt to finalize the profile if we don't have one loaded... + if app.peers[profile.GetOnion()] == nil { + eventBus := event.NewEventManager() + app.eventBuses[profile.GetOnion()] = eventBus + profile.Init(app.eventBuses[profile.GetOnion()]) + app.peers[profile.GetOnion()] = profile + app.engines[profile.GetOnion()], _ = profile.GenerateProtocolEngine(app.acn, app.eventBuses[profile.GetOnion()]) + app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.GetOnion(), event.Created: event.False})) + count++ + } else { + // Otherwise shutdown the connections + profile.Shutdown() + } app.appmutex.Unlock() - app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.GetOnion(), event.Created: event.False})) - count++ }) if count == 0 { message := event.NewEventList(event.AppError, event.Error, event.AppErrLoaded0) diff --git a/model/constants/attributes.go b/model/constants/attributes.go index ba984c4..64c6917 100644 --- a/model/constants/attributes.go +++ b/model/constants/attributes.go @@ -38,3 +38,11 @@ const AttrErr = "error" // AttrSentTimestamp - conversation attribute for the time the message was (nominally) sent const AttrSentTimestamp = "sent" + +// Legacy MessageFlags + +// AttrRejected - conversation attribute for storing rejected prompts (for invites) +const AttrRejected = "rejected-invite" + +// AttrDownloaded - conversation attribute for storing downloaded prompts (for file downloads) +const AttrDownloaded = "file-downloaded" diff --git a/peer/cwtch_peer.go b/peer/cwtch_peer.go index 297d0c3..09bedca 100644 --- a/peer/cwtch_peer.go +++ b/peer/cwtch_peer.go @@ -233,6 +233,7 @@ func NewProfileWithEncryptedStorage(name string, cps *CwtchProfileStorage) Cwtch cp := new(cwtchPeer) cp.shutdown = false cp.storage = cps + cp.queue = event.NewQueue() cp.state = make(map[string]connections.ConnectionState) pub, priv, _ := ed25519.GenerateKey(rand.Reader) @@ -250,22 +251,106 @@ func FromEncryptedStorage(cps *CwtchProfileStorage) CwtchPeer { cp := new(cwtchPeer) cp.shutdown = false cp.storage = cps + cp.queue = event.NewQueue() cp.state = make(map[string]connections.ConnectionState) + // At some point we may want to populate caches here, for now we will assume hitting the + // database directly is tolerable return cp } -// FromProfile generates a new peer from a profile. +// ImportLegacyProfile generates a new peer from a profile. // Deprecated - Only to be used for importing new profiles -func FromProfile(profile *model.Profile, cps *CwtchProfileStorage) CwtchPeer { +func ImportLegacyProfile(profile *model.Profile, cps *CwtchProfileStorage) CwtchPeer { cp := new(cwtchPeer) cp.shutdown = false cp.storage = cps - + cp.queue = event.NewQueue() // Store all the Necessary Base Attributes In The Database cp.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name, profile.Name) + cp.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, profile.Name) + cp.storage.StoreProfileKeyValue(TypeAttribute, attr.PublicScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Onion)).ToString(), []byte(tor.GetTorV3Hostname(profile.Ed25519PublicKey))) cp.storage.StoreProfileKeyValue(TypePrivateKey, "Ed25519PrivateKey", profile.Ed25519PrivateKey) cp.storage.StoreProfileKeyValue(TypePublicKey, "Ed25519PublicKey", profile.Ed25519PublicKey) + for k, v := range profile.Attributes { + parts := strings.SplitN(k, ".", 2) + if len(parts) != 2 { + scope := attr.IntoScope(parts[0]) + zone, path := attr.ParseZone(parts[1]) + cp.SetScopedZonedAttribute(scope, zone, path, v) + } else { + log.Errorf("could not import legacy style attribute %v", k) + } + } + + for _, contact := range profile.Contacts { + var conversationID int + var err error + if contact.Authorization == model.AuthApproved { + conversationID, err = cp.NewContactConversation(contact.Onion, model.DefaultP2PAccessControl(), true) + } else if contact.Authorization == model.AuthBlocked { + conversationID, err = cp.NewContactConversation(contact.Onion, model.AccessControl{Blocked: true, Read: false, Append: false}, true) + } else { + conversationID, err = cp.NewContactConversation(contact.Onion, model.DefaultP2PAccessControl(), false) + } + + if err == nil { + for key, value := range contact.Attributes { + switch key { + case "name": + cp.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Name)), value) + case "local.profile.name": + cp.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Name)), value) + case event.SaveHistoryKey: + cp.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(event.SaveHistoryKey)), value) + case string(model.BundleType): + cp.AddServer(value) + case string(model.KeyTypeTokenOnion): + //ignore + case string(model.KeyTypeServerOnion): + // ignore + case string(model.KeyTypePrivacyPass): + // ignore + default: + log.Errorf("could not import conversation attribute %v %v", key, value) + } + } + for _, message := range contact.Timeline.GetMessages() { + // By definition anything stored in legacy timelines in acknowledged + attr := model.Attributes{constants.AttrAck: event.True, constants.AttrSentTimestamp: message.Timestamp.Format(time.RFC3339Nano)} + if message.Flags&0x01 == 0x01 { + attr[constants.AttrRejected] = event.True + } + if message.Flags&0x02 == 0x02 { + attr[constants.AttrDownloaded] = event.True + } + cp.storage.InsertMessage(conversationID, 0, message.Message, attr, model.GenerateRandomID(), model.CalculateContentHash(message.PeerID, message.Message)) + } + } + } + + for _, group := range profile.Groups { + invite, err := group.Invite() + if err == nil { + // Automatically grab all the important fields... + // Including Name... + conversationID, err := cp.ImportGroup(invite) + if err == nil { + for _, message := range group.Timeline.GetMessages() { + // By definition anything stored in legacy timelines in acknowledged + attr := model.Attributes{constants.AttrAck: event.True, constants.AttrSentTimestamp: message.Timestamp.Format(time.RFC3339Nano)} + if message.Flags&0x01 == 0x01 { + attr[constants.AttrRejected] = event.True + } + if message.Flags&0x02 == 0x02 { + attr[constants.AttrDownloaded] = event.True + } + cp.storage.InsertMessage(conversationID, 0, message.Message, attr, base64.StdEncoding.EncodeToString(message.Signature), model.CalculateContentHash(message.PeerID, message.Message)) + } + } + } + } + return cp } @@ -331,7 +416,6 @@ func (cp *cwtchPeer) Init(eventBus event.Manager) { // InitForEvents // Status: Ready for 1.5 func (cp *cwtchPeer) InitForEvents(eventBus event.Manager, toBeHandled []event.Type) { - cp.queue = event.NewQueue() go cp.eventHandler() cp.eventBus = eventBus diff --git a/peer/storage.go b/peer/storage.go index 755efe7..c84a321 100644 --- a/peer/storage.go +++ b/peer/storage.go @@ -113,6 +113,39 @@ func CreateEncryptedStorePeer(profileDirectory string, name string, password str return NewProfileWithEncryptedStorage(name, cps), nil } +// CreateEncryptedStore creates a encrypted datastore +func CreateEncryptedStore(profileDirectory string, password string) (*CwtchProfileStorage, error) { + log.Debugf("Initializing Encrypted Storage Directory") + _, _, err := initV2Directory(profileDirectory, password) + if err != nil { + return nil, err + } + + log.Debugf("Opening Encrypted Database") + db, err := openEncryptedDatabase(profileDirectory, password) + if db == nil || err != nil { + return nil, fmt.Errorf("unable to open encrypted database: error: %v", err) + } + + log.Debugf("Initializing Database") + err = initializeDatabase(db) + + if err != nil { + db.Close() + return nil, err + } + + log.Debugf("Creating Cwtch Profile Backed By Encrypted Database") + + cps, err := NewCwtchProfileStorage(db) + if err != nil { + db.Close() + return nil, err + } + + return cps, nil +} + // FromEncryptedDatabase constructs a Cwtch Profile from an existing Encrypted Database func FromEncryptedDatabase(profileDirectory string, password string) (CwtchPeer, error) { log.Debugf("Loading Encrypted Profile")