// Copyright Martin Dosch. // Use of this source code is governed by the BSD-2-clause // license that can be found in the LICENSE file. package main import ( "bytes" "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/gob" "errors" "fmt" "log" "math/big" "net/url" "os" "regexp" "runtime" "strings" "github.com/google/uuid" // BSD-3-Clause "github.com/xmppo/go-xmpp" // BSD-3-Clause "golang.org/x/crypto/scrypt" // BSD-3-Clause ) func validUTF8(s string) string { // Remove invalid code points. s = strings.ToValidUTF8(s, "�") reg := regexp.MustCompile(`[\x{0000}-\x{0008}\x{000B}\x{000C}\x{000E}-\x{001F}]`) s = reg.ReplaceAllString(s, "�") return s } func validURI(s string) (*url.URL, error) { // Check if URI is valid uri, err := url.ParseRequestURI(s) return uri, fmt.Errorf("validURI: %w", err) } func readFile(path string) (*bytes.Buffer, error) { file, err := os.Open(path) if err != nil { return nil, fmt.Errorf("readFile: %w", err) } buffer := new(bytes.Buffer) _, err = buffer.ReadFrom(file) if err != nil { return nil, fmt.Errorf("readFile: %w", err) } err = file.Close() if err != nil { fmt.Println("error while closing file:", err) } return buffer, nil } func getFastData(jid string, password string) (xmpp.Fast, error) { folder := strings.Replace(strings.Replace(jid, "@", "_at_", -1), ".", "_", -1) var fast xmpp.Fast fastPath, err := getDataPath(folder) if err != nil { return xmpp.Fast{}, fmt.Errorf("getFastData: failed to read fast cache folder: %w", err) } fastFileLoc := fastPath + "fast.bin" buf, err := readFile(fastFileLoc) if err != nil { return xmpp.Fast{}, fmt.Errorf("getFastData: failed to read fast cache file: %w", err) } decBuf := bytes.NewBuffer(buf.Bytes()) decoder := gob.NewDecoder(decBuf) err = decoder.Decode(&fast) if err != nil { return xmpp.Fast{}, fmt.Errorf("getFastData: failed to read fast cache file: %w", err) } salt := make([]byte, 32) key, err := scrypt.Key([]byte(password), salt, 32768, 8, 1, 32) if err != nil { return xmpp.Fast{}, fmt.Errorf("getFastData: failed to create aes key: %w", err) } c, err := aes.NewCipher([]byte(key)) if err != nil { return xmpp.Fast{}, fmt.Errorf("getFastData: failed to read fast cache file: %w", err) } gcm, err := cipher.NewGCM(c) if err != nil { return xmpp.Fast{}, fmt.Errorf("getFastData: failed to read fast cache file: %w", err) } nonceSize := gcm.NonceSize() cryptBuf := []byte(fast.Token) nonce, cryptBuf := cryptBuf[:nonceSize], cryptBuf[nonceSize:] tokenBuf, err := gcm.Open(nil, []byte(nonce), cryptBuf, nil) if err != nil { return xmpp.Fast{}, fmt.Errorf("getFastData: failed to read fast cache file: %w", err) } fast.Token = string(tokenBuf) return fast, nil } func writeFastData(jid string, password string, fast xmpp.Fast) error { var encBuf bytes.Buffer folder := strings.Replace(strings.Replace(jid, "@", "_at_", -1), ".", "_", -1) fastPath, err := getDataPath(folder) if err != nil { return fmt.Errorf("writeFastData: failed to write fast cache file: %w", err) } fastFileLoc := fastPath + "fast.bin" salt := make([]byte, 32) key, err := scrypt.Key([]byte(password), salt, 32768, 8, 1, 32) if err != nil { return fmt.Errorf("writeFastData: failed to create aes cipher: %w", err) } c, err := aes.NewCipher(key) if err != nil { return fmt.Errorf("writeFastData: failed to create aes cipher: %w", err) } gcm, err := cipher.NewGCM(c) if err != nil { return fmt.Errorf("writeFastData: failed to create aes cipher: %w", err) } nonce := make([]byte, gcm.NonceSize()) _, err = rand.Read(nonce) if err != nil { return fmt.Errorf("writeFastData: failed to create aes cipher: %w", err) } buf := gcm.Seal(nonce, nonce, []byte(fast.Token), nil) fast.Token = string(buf) encode := gob.NewEncoder(&encBuf) err = encode.Encode(fast) if err != nil { return fmt.Errorf("writeFastData: failed to create fast token file: %w", err) } file, err := os.Create(fastFileLoc) if err != nil { return fmt.Errorf("writeFastData: failed to create fast token file: %w", err) } defer file.Close() if runtime.GOOS != "windows" { _ = file.Chmod(os.FileMode(defaultFileRights)) } else { _ = file.Chmod(os.FileMode(defaultFileRightsWin)) } _, err = file.Write(encBuf.Bytes()) if err != nil { return fmt.Errorf("writeFastData: failed to write fast token file: %w", err) } return nil } func getClientID(jid string) (string, error) { var clientID string folder := strings.Replace(strings.Replace(jid, "@", "_at_", -1), ".", "_", -1) clientIDLoc, err := getClientIDLoc(folder) if err != nil { return strError, err } buf, err := readFile(clientIDLoc) if err != nil { clientID = uuid.NewString() file, err := os.Create(clientIDLoc) if err != nil { return strEmpty, fmt.Errorf("getClientID: failed to create clientid file: %w", err) } defer file.Close() if runtime.GOOS != "windows" { _ = file.Chmod(os.FileMode(defaultFileRights)) } else { _ = file.Chmod(os.FileMode(defaultFileRightsWin)) } _, err = file.Write([]byte(clientID)) if err != nil { return strEmpty, fmt.Errorf("getClientID: failed to write client id file: %w", err) } } else { clientID = buf.String() } return clientID, nil } func getDataPath(folder string) (string, error) { var err error var homeDir, dataDir string switch { case os.Getenv("$XDG_DATA_HOME") != "": dataDir = os.Getenv("$XDG_DATA_HOME") case os.Getenv("$XDG_HOME") != "": homeDir = os.Getenv("$XDG_HOME") dataDir = homeDir + "/.local/share" case os.Getenv("$HOME") != "": homeDir = os.Getenv("$HOME") dataDir = homeDir + "/.local/share" default: homeDir, err = os.UserHomeDir() if err != nil { return strError, fmt.Errorf("getDataPath: failed to determine user dir: %w", err) } if homeDir == "" { return strError, errors.New("getDataPath: received empty string for home directory") } dataDir = homeDir + "/.local/share" } if folder != "" && !strings.HasSuffix(folder, "/") { folder = fmt.Sprintf("%s/", folder) } dataDir = fmt.Sprintf("%s/go-sendxmpp/%s", dataDir, folder) if _, err = os.Stat(dataDir); os.IsNotExist(err) { err = os.MkdirAll(dataDir, defaultDirRights) if err != nil { return strError, fmt.Errorf("getDataPath: could not create folder: %w", err) } } return dataDir, nil } func getClientIDLoc(folder string) (string, error) { dataDir, err := getDataPath(folder) if err != nil { return strError, fmt.Errorf("getClientIDLoc: %w", err) } dataFile := dataDir + "clientid" return dataFile, nil } func getRpad(messageLength int) string { rpadRunes := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") length := defaultRpadMultiple - messageLength%defaultRpadMultiple max := big.NewInt(int64(len(rpadRunes))) rpad := make([]rune, length) for i := range rpad { randInt, err := rand.Int(rand.Reader, max) if err != nil { log.Fatal(err) } rpad[i] = rpadRunes[randInt.Int64()] } return string(rpad) } func getID() string { return uuid.NewString() } func getShortID() string { return uuid.NewString()[:4] }