package v1 import ( "cwtch.im/cwtch/event" "cwtch.im/cwtch/model" "encoding/json" "git.openprivacy.ca/openprivacy/log" "io/ioutil" "os" "path" ) const profileFilename = "profile" const version = "1" const versionFile = "VERSION" const saltFile = "SALT" //ProfileStoreV1 storage for profiles and message streams that uses in memory key and fs stored salt instead of in memory password type ProfileStoreV1 struct { fs FileStore directory string profile *model.Profile key [32]byte salt [128]byte } // CheckPassword returns true if the given password produces the same key as the current stored key, otherwise false. func (ps *ProfileStoreV1) CheckPassword(checkpass string) bool { oldkey := CreateKey(checkpass, ps.salt[:]) return oldkey == ps.key } // InitV1Directory generates a key and salt from a password, writes a SALT and VERSION file and returns the key and salt func InitV1Directory(directory, password string) ([32]byte, [128]byte, error) { os.Mkdir(directory, 0700) key, salt, err := CreateKeySalt(password) if err != nil { log.Errorf("Could not create key for profile store from password: %v\n", err) return [32]byte{}, [128]byte{}, err } if err = ioutil.WriteFile(path.Join(directory, versionFile), []byte(version), 0600); err != nil { log.Errorf("Could not write version file: %v", err) return [32]byte{}, [128]byte{}, err } if err = ioutil.WriteFile(path.Join(directory, saltFile), salt[:], 0600); err != nil { log.Errorf("Could not write salt file: %v", err) return [32]byte{}, [128]byte{}, err } return key, salt, nil } // CreateProfileWriterStore creates a profile store backed by a filestore listening for events and saving them // directory should be $appDir/profiles/$rand func CreateProfileWriterStore(eventManager event.Manager, directory, password string, profile *model.Profile) *ProfileStoreV1 { key, salt, err := InitV1Directory(directory, password) if err != nil { return nil } ps := &ProfileStoreV1{fs: NewFileStore(directory, profileFilename, key), key: key, salt: salt, directory: directory, profile: profile} return ps } // LoadProfileWriterStore loads a profile store from filestore listening for events and saving them // directory should be $appDir/profiles/$rand func LoadProfileWriterStore(eventManager event.Manager, directory, password string) (*ProfileStoreV1, error) { salt, err := ioutil.ReadFile(path.Join(directory, saltFile)) if err != nil { return nil, err } key := CreateKey(password, salt) ps := &ProfileStoreV1{fs: NewFileStore(directory, profileFilename, key), key: key, directory: directory, profile: nil} copy(ps.salt[:], salt) err = ps.load() if err != nil { return nil, err } return ps, nil } // ReadProfile reads a profile from storqage and returns the profile // directory should be $appDir/profiles/$rand func ReadProfile(directory string, key [32]byte, salt [128]byte) (*model.Profile, error) { os.Mkdir(directory, 0700) ps := &ProfileStoreV1{fs: NewFileStore(directory, profileFilename, key), key: key, salt: salt, directory: directory, profile: nil} err := ps.load() if err != nil { return nil, err } profile := ps.GetProfileCopy(true) return profile, nil } // NewProfile creates a new profile for use in the profile store. func NewProfile(name string) *model.Profile { profile := model.GenerateNewProfile(name) return profile } // GetNewPeerMessage is for AppService to call on Reload events, to reseed the AppClient with the loaded peers func (ps *ProfileStoreV1) GetNewPeerMessage() *event.Event { message := event.NewEventList(event.NewPeer, event.Identity, ps.profile.LocalID, event.Key, string(ps.key[:]), event.Salt, string(ps.salt[:])) return &message } // load instantiates a cwtchPeer from the file store func (ps *ProfileStoreV1) load() error { decrypted, err := ps.fs.Read() if err != nil { return err } cp := new(model.Profile) err = json.Unmarshal(decrypted, &cp) if err == nil { ps.profile = cp // TODO 2020.06.09: v1 update, Remove on v2 // if we already have the contact it can be assumed "approved" unless blocked for _, contact := range cp.Contacts { if contact.Authorization == "" { if contact.DeprecatedBlocked { contact.Authorization = model.AuthBlocked } else { contact.Authorization = model.AuthApproved } } // Check if there is any saved history... saveHistory, keyExists := contact.GetAttribute(event.SaveHistoryKey) if !keyExists { contact.SetAttribute(event.SaveHistoryKey, event.DeleteHistoryDefault) } if saveHistory == event.SaveHistoryConfirmed { ss := NewStreamStore(ps.directory, contact.LocalID, ps.key) cp.Contacts[contact.Onion].Timeline.SetMessages(ss.Read()) } } for gid, group := range cp.Groups { if group.Version == 0 { log.Infof("group %v is of unsupported version 0. dropping group...\n", group.GroupID) delete(cp.Groups, gid) continue } ss := NewStreamStore(ps.directory, group.LocalID, ps.key) cp.Groups[gid].Timeline.SetMessages(ss.Read()) cp.Groups[gid].Timeline.Sort() } } return err } // GetProfileCopy returns a copy of the stored profile func (ps *ProfileStoreV1) GetProfileCopy(timeline bool) *model.Profile { return ps.profile.GetCopy(timeline) }