package models
import (
"bytes"
"database/sql"
"errors"
"sort"
"strings"
"time"
"github.com/Queueue0/qpass/internal/crypto"
"github.com/google/uuid"
)
// e = encrypted
type Password struct {
ID int
UUID uuid.UUID
UserID uuid.UUID
ServiceName string
Username string
Password string
LastChanged time.Time
Deleted bool
EServiceName string
EUsername string
EPassword string
}
type PasswordList []Password
func (p *Password) decrypt(u User) error {
var err error
p.ServiceName, err = crypto.Decrypt(p.EServiceName, u.Key)
if err != nil {
return err
}
p.Username, err = crypto.Decrypt(p.EUsername, u.Key)
if err != nil {
return err
}
p.Password, err = crypto.Decrypt(p.EPassword, u.Key)
if err != nil {
return err
}
return nil
}
// Probably not the best way to write this...
func (p *Password) isDecrypted() bool {
return p.ServiceName != "" && p.Username != "" && p.Password != ""
}
// Notably not Equals(), because I'm only interested in if
// the UUID is the same
func (p *Password) IsSame(other Password) bool {
// UUIDs are just [16]byte with receiver functions
return bytes.Equal(p.UUID[:], other.UUID[:])
}
type PasswordModel struct {
DB *sql.DB
}
func (m *PasswordModel) Insert(u User, serviceName, username, password string) (int, error) {
eServiceName, err := crypto.Encrypt(serviceName, u.Key)
if err != nil {
return 0, err
}
eUsername, err := crypto.Encrypt(username, u.Key)
if err != nil {
return 0, err
}
ePassword, err := crypto.Encrypt(password, u.Key)
if err != nil {
return 0, err
}
UUID, err := uuid.NewRandom()
if err != nil {
return 0, err
}
stmt := `INSERT INTO passwords (uuid, userId, service, username, password) VALUES (?, ?, ?, ?, ?)`
result, err := m.DB.Exec(stmt, UUID.String(), u.ID.String(), eServiceName, eUsername, ePassword)
if err != nil {
return 0, err
}
id, err := result.LastInsertId()
if err != nil {
return 0, err
}
return int(id), nil
}
func (m *PasswordModel) Update(u User, id, serviceName, username, password string) error {
eServiceName, err := crypto.Encrypt(serviceName, u.Key)
if err != nil {
return err
}
eUsername, err := crypto.Encrypt(username, u.Key)
if err != nil {
return err
}
ePassword, err := crypto.Encrypt(password, u.Key)
if err != nil {
return err
}
stmt := `UPDATE passwords SET service = ?, username = ?, password = ?, last_changed = ? WHERE uuid = ?`
_, err = m.DB.Exec(stmt, eServiceName, eUsername, ePassword, time.Now(), id)
return err
}
func (m *PasswordModel) Get(id int, u User) (Password, error) {
stmt := `SELECT id, uuid, userId, service, username, password, last_changed, deleted FROM passwords WHERE id = ?`
r := m.DB.QueryRow(stmt, id)
p := Password{}
var uuidStr string
err := r.Scan(&p.ID, &uuidStr, &p.UserID, &p.EServiceName, &p.EUsername, &p.EPassword, &p.LastChanged, &p.Deleted)
if err != nil {
return Password{}, err
}
p.UUID, err = uuid.Parse(uuidStr)
if err != nil {
return Password{}, err
}
err = p.decrypt(u)
if err != nil {
return Password{}, err
}
return p, nil
}
func (m *PasswordModel) GetByUUID(UUID string) (Password, error) {
stmt := `SELECT id, uuid, userId, service, username, password, last_changed, deleted FROM passwords WHERE uuid = ?`
r := m.DB.QueryRow(stmt, UUID)
p := Password{}
var uuidStr string
var useridStr string
err := r.Scan(&p.ID, &uuidStr, &useridStr, &p.EServiceName, &p.EUsername, &p.EPassword, &p.LastChanged, &p.Deleted)
if err != nil {
return Password{}, err
}
p.UUID, err = uuid.Parse(uuidStr)
if err != nil {
return Password{}, err
}
p.UserID, err = uuid.Parse(useridStr)
if err != nil {
return Password{}, err
}
// No need to decrypt in cases where this function is used
return p, nil
}
func (m *PasswordModel) GetAllForUser(u User, includeDeleted bool) (PasswordList, error) {
stmt := `SELECT id, uuid, userId, service, username, password, last_changed, deleted FROM passwords WHERE userId = ?`
rows, err := m.DB.Query(stmt, u.ID.String())
if err != nil {
return nil, err
}
pws := PasswordList{}
for rows.Next() {
pw := Password{}
var uuidString string
var useridString string
err := rows.Scan(&pw.ID, &uuidString, &useridString, &pw.EServiceName, &pw.EUsername, &pw.EPassword, &pw.LastChanged, &pw.Deleted)
if err != nil {
return nil, err
}
if pw.Deleted && !includeDeleted {
continue
}
err = pw.decrypt(u)
if err != nil {
return nil, err
}
pw.UUID, err = uuid.Parse(uuidString)
if err != nil {
return nil, err
}
pw.UserID, err = uuid.Parse(useridString)
if err != nil {
return nil, err
}
pws = append(pws, pw)
}
pws.Sort()
return pws, nil
}
func (m *PasswordModel) GetAllEncryptedForUser(u User) (PasswordList, error) {
stmt := `SELECT id, uuid, userId, service, username, password, last_changed, deleted FROM passwords WHERE userId = ?`
rows, err := m.DB.Query(stmt, u.ID.String())
if err != nil {
return nil, err
}
pws := PasswordList{}
for rows.Next() {
pw := Password{}
var uuidString string
var useridString string
err := rows.Scan(&pw.ID, &uuidString, &useridString, &pw.EServiceName, &pw.EUsername, &pw.EPassword, &pw.LastChanged, &pw.Deleted)
if err != nil {
return nil, err
}
pw.UUID, err = uuid.Parse(uuidString)
if err != nil {
return nil, err
}
pw.UserID, err = uuid.Parse(useridString)
if err != nil {
return nil, err
}
pws = append(pws, pw)
}
return pws, nil
}
func (m *PasswordModel) DumbUpdate(p Password) error {
stmt := `UPDATE passwords SET service = ?, username = ?, password = ?, last_changed = ?, deleted = ? WHERE uuid = ?`
_, err := m.DB.Exec(stmt, p.EServiceName, p.EUsername, p.EPassword, p.LastChanged, p.Deleted, p.UUID.String())
return err
}
func (m *PasswordModel) DumbInsert(p Password) error {
stmt := `INSERT INTO passwords (uuid, userId, service, username, password, last_changed, deleted) VALUES (?, ?, ?, ?, ?, ?, ?)`
_, err := m.DB.Exec(stmt, p.UUID.String(), p.UserID.String(), p.EServiceName, p.EUsername, p.EPassword, p.LastChanged, p.Deleted)
return err
}
func (m *PasswordModel) Delete(UUID string) error {
_, err := m.DB.Exec("DELETE FROM passwords WHERE uuid = ?", UUID)
return err
}
func (m *PasswordModel) Exists(UUID string) (bool, error) {
row := m.DB.QueryRow("SELECT EXISTS(SELECT uuid FROM passwords WHERE uuid = ?)", UUID)
var result bool
err := row.Scan(&result)
return result, err
}
func (m *PasswordModel) ReplaceAllForUser(userID string, pwl PasswordList) error {
userUUID, err := uuid.Parse(userID)
if err != nil {
return err
}
for _, pw := range pwl {
if !bytes.Equal(userUUID[:], pw.UserID[:]) {
return errors.New("Passwords not for this user")
}
}
_, err = m.DB.Exec(`DELETE FROM passwords WHERE userId = ?`, userID)
if err != nil {
return err
}
for _, pw := range pwl {
err = m.DumbInsert(pw)
if err != nil {
return err
}
}
return nil
}
func (pl PasswordList) Search(searchTerm string) PasswordList {
if strings.TrimSpace(searchTerm) == "" {
return pl
}
searchTerm = strings.ToLower(searchTerm)
res := PasswordList{}
for _, p := range pl {
if !p.isDecrypted() {
continue
}
sn := strings.ToLower(p.ServiceName)
if strings.Contains(sn, searchTerm) {
res = append(res, p)
continue
}
// un := strings.ToLower(p.Username)
// if strings.Contains(un, searchTerm) {
// res = append(res, p)
// continue
// }
}
return res
}
func (pl PasswordList) Sort() {
sort.Slice(pl, func(i, j int) bool {
return pl[i].ServiceName < pl[j].ServiceName
})
}