diff --git a/account.go b/account.go index 6be12e7..c17fb2f 100644 --- a/account.go +++ b/account.go @@ -14,6 +14,7 @@ type Account struct { MatrixUser string AccountName string Protocol string + Config map[string]string Conn Connector JoinedRooms map[RoomID]bool @@ -77,7 +78,7 @@ func (a *Account) ezbrMessagef(format string, args ...interface{}) { ezbrSystemSend(a.MatrixUser, msg) } -func (a *Account) connect(config map[string]string, join_rooms []string) { +func (a *Account) connect(config map[string]string) { ezbrSystemSendf(a.MatrixUser, "Connecting to account %s (%s)", a.AccountName, a.Protocol) err := a.Conn.Configure(config) @@ -86,10 +87,6 @@ func (a *Account) connect(config map[string]string, join_rooms []string) { return } - for _, room := range join_rooms { - a.addAutojoin(RoomID(room)) - } - var autojoin []DbJoinedRoom db.Where(&DbJoinedRoom{ MxUserID: a.MatrixUser, diff --git a/db.go b/db.go index 602bd1f..c5f74ef 100644 --- a/db.go +++ b/db.go @@ -25,6 +25,8 @@ func InitDb() error { return err } + db.AutoMigrate(&DbAccountConfig{}) + db.AutoMigrate(&DbCache{}) db.AutoMigrate(&DbUserMap{}) @@ -42,6 +44,16 @@ func InitDb() error { return nil } +// Account configuration +type DbAccountConfig struct { + gorm.Model + + MxUserID string `gorm:"index:account_mxuserid"` + Name string + Protocol string + Config string +} + // Long-term cache entries type DbCache struct { gorm.Model @@ -56,7 +68,7 @@ type DbUserMap struct { Protocol string UserID connector.UserID - MxUserID string `gorm:"index:mxuserid"` + MxUserID string `gorm:"index:usermap_mxuserid"` } // Room mapping between Matrix rooms and outside rooms diff --git a/go.sum b/go.sum index 83d9db7..31d4c85 100644 --- a/go.sum +++ b/go.sum @@ -106,6 +106,7 @@ github.com/keybase/go-keybase-chat-bot v0.0.0-20190816161829-561f10822eb2/go.mod github.com/keybase/go-ps v0.0.0-20161005175911-668c8856d999/go.mod h1:hY+WOq6m2FpbvyrI93sMaypsttvaIL5nhVR92dTMUcQ= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +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/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -274,6 +275,7 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= diff --git a/main.go b/main.go index 1b3971e..a8a349c 100644 --- a/main.go +++ b/main.go @@ -24,7 +24,6 @@ import ( type ConfigAccount struct { Protocol string `json:"protocol"` - Rooms []string `json:"rooms"` Config map[string]string `json:"config"` } @@ -176,27 +175,21 @@ func main() { for user, accounts := range config.Accounts { for name, params := range accounts { - var conn connector.Connector - switch params.Protocol { - case "irc": - conn = &irc.IRC{} - case "xmpp": - conn = &xmpp.XMPP{} - case "mattermost": - conn = &mattermost.Mattermost{} - default: - log.Fatalf("Invalid protocol %s", params.Protocol) + conn := createConnector(params.Protocol) + if conn == nil { + log.Fatalf("Could not create connector for protocol %s", params.Protocol) } account := &Account{ MatrixUser: fmt.Sprintf("@%s:%s", user, config.MatrixDomain), AccountName: name, Protocol: params.Protocol, + Config: params.Config, Conn: conn, JoinedRooms: map[connector.RoomID]bool{}, } conn.SetHandler(account) AddAccount(account) - go account.connect(params.Config, params.Rooms) + go account.connect(params.Config) } } @@ -205,3 +198,16 @@ func main() { log.Fatal(err) } } + +func createConnector(protocol string) connector.Connector { + switch protocol { + case "irc": + return &irc.IRC{} + case "xmpp": + return &xmpp.XMPP{} + case "mattermost": + return &mattermost.Mattermost{} + default: + return nil + } +} diff --git a/util.go b/util.go index c1da665..323f99d 100644 --- a/util.go +++ b/util.go @@ -1,10 +1,14 @@ package main import ( + "crypto/rand" + "encoding/base64" + "encoding/json" "fmt" "unicode" log "github.com/sirupsen/logrus" + "golang.org/x/crypto/nacl/secretbox" . "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector" ) @@ -56,3 +60,41 @@ func safeStringForId(in string) string { } return id2 } + +// ---- Encoding and encryption of account config + +func encryptAccountConfig(config map[string]string, key *[32]byte) string { + bytes, err := json.Marshal(config) + if err != nil { + log.Fatal(err) + } + + var nonce [24]byte + _, err = rand.Read(nonce[:]) + if err != nil { + log.Fatal(err) + } + + crypto := secretbox.Seal([]byte{}, bytes, &nonce, key) + all := append(nonce[:], crypto...) + return base64.StdEncoding.EncodeToString(all) +} + +func decryptAccountConfig(data string, key *[32]byte) (map[string]string, error) { + bytes, err := base64.StdEncoding.DecodeString(data) + if err != nil { + return nil, err + } + + var nonce [24]byte + copy(nonce[:], bytes[:24]) + + decoded, ok := secretbox.Open([]byte{}, bytes[24:], &nonce, key) + if !ok { + return nil, fmt.Errorf("Invalid key") + } + + var config map[string]string + err = json.Unmarshal(decoded, &config) + return config, err +} diff --git a/web.go b/web.go index dafb8b1..bff8acc 100644 --- a/web.go +++ b/web.go @@ -9,13 +9,16 @@ import ( "github.com/gorilla/mux" "github.com/gorilla/sessions" + "golang.org/x/crypto/argon2" + "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector" "git.deuxfleurs.fr/Deuxfleurs/easybridge/mxlib" ) const SESSION_NAME = "easybridge_session" var sessionsStore sessions.Store = nil +var userKeys = map[string]*[32]byte{} func StartWeb() { session_key := make([]byte, 32) @@ -147,6 +150,12 @@ func handleLogin(w http.ResponseWriter, r *http.Request) *LoginInfo { return nil } + key := new([32]byte) + key_slice := argon2.IDKey([]byte(password), []byte("EZBRIDGE account store"), 3, 64*1024, 4, 32) + copy(key[:], key_slice[:]) + userKeys[mxid] = key + syncDbAccounts(mxid, key) + // Successfully logged in, save it to session session, err := sessionsStore.Get(r, SESSION_NAME) if err != nil { @@ -169,3 +178,58 @@ func handleLogin(w http.ResponseWriter, r *http.Request) *LoginInfo { return nil } } + +func syncDbAccounts(mxid string, key *[32]byte) { + accountsLock.Lock() + defer accountsLock.Unlock() + + // 1. Save all accounts that we have + var accounts map[string]*Account + if accts, ok := registeredAccounts[mxid]; ok { + accounts = accts + for name, acct := range accts { + var entry DbAccountConfig + db.Where(&DbAccountConfig{ + MxUserID: mxid, + Name: name, + }).Assign(&DbAccountConfig{ + Protocol: acct.Protocol, + Config: encryptAccountConfig(acct.Config, key), + }).FirstOrCreate(&entry) + } + } else { + accounts = make(map[string]*Account) + registeredAccounts[mxid] = accounts + } + + // 2. Load and start missing accounts + var allAccounts []DbAccountConfig + db.Where(&DbAccountConfig{MxUserID: mxid}).Find(&allAccounts) + for _, acct := range allAccounts { + if _, ok := accounts[acct.Name]; !ok { + config, err := decryptAccountConfig(acct.Config, key) + if err != nil { + ezbrSystemSendf("Could not decrypt stored configuration for account %s", acct.Name) + continue + } + conn := createConnector(acct.Protocol) + if conn == nil { + ezbrSystemSendf("Could not create connector for protocol %s", acct.Protocol) + continue + } + account := &Account{ + MatrixUser: mxid, + AccountName: acct.Name, + Protocol: acct.Protocol, + Config: config, + Conn: conn, + JoinedRooms: map[connector.RoomID]bool{}, + } + conn.SetHandler(account) + + accounts[acct.Name] = account + + go account.connect(config) + } + } +}