diff options
Diffstat (limited to 'vendor/github.com/Rhymen/go-whatsapp/session.go')
-rw-r--r-- | vendor/github.com/Rhymen/go-whatsapp/session.go | 532 |
1 files changed, 532 insertions, 0 deletions
diff --git a/vendor/github.com/Rhymen/go-whatsapp/session.go b/vendor/github.com/Rhymen/go-whatsapp/session.go new file mode 100644 index 00000000..215fb599 --- /dev/null +++ b/vendor/github.com/Rhymen/go-whatsapp/session.go @@ -0,0 +1,532 @@ +package whatsapp + +import ( + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "strconv" + "strings" + "sync/atomic" + "time" + + "github.com/Rhymen/go-whatsapp/crypto/cbc" + "github.com/Rhymen/go-whatsapp/crypto/curve25519" + "github.com/Rhymen/go-whatsapp/crypto/hkdf" +) + +//represents the WhatsAppWeb client version +var waVersion = []int{2, 2142, 12} + +/* +Session contains session individual information. To be able to resume the connection without scanning the qr code +every time you should save the Session returned by Login and use RestoreWithSession the next time you want to login. +Every successful created connection returns a new Session. The Session(ClientToken, ServerToken) is altered after +every re-login and should be saved every time. +*/ +type Session struct { + ClientId string + ClientToken string + ServerToken string + EncKey []byte + MacKey []byte + Wid string +} + +type Info struct { + Battery int + Platform string + Connected bool + Pushname string + Wid string + Lc string + Phone *PhoneInfo + Plugged bool + Tos int + Lg string + Is24h bool +} + +type PhoneInfo struct { + Mcc string + Mnc string + OsVersion string + DeviceManufacturer string + DeviceModel string + OsBuildNumber string + WaVersion string +} + +func newInfoFromReq(info map[string]interface{}) *Info { + phoneInfo := info["phone"].(map[string]interface{}) + + ret := &Info{ + Battery: int(info["battery"].(float64)), + Platform: info["platform"].(string), + Connected: info["connected"].(bool), + Pushname: info["pushname"].(string), + Wid: info["wid"].(string), + Lc: info["lc"].(string), + Phone: &PhoneInfo{ + phoneInfo["mcc"].(string), + phoneInfo["mnc"].(string), + phoneInfo["os_version"].(string), + phoneInfo["device_manufacturer"].(string), + phoneInfo["device_model"].(string), + phoneInfo["os_build_number"].(string), + phoneInfo["wa_version"].(string), + }, + Plugged: info["plugged"].(bool), + Lg: info["lg"].(string), + Tos: int(info["tos"].(float64)), + } + + if is24h, ok := info["is24h"]; ok { + ret.Is24h = is24h.(bool) + } + + return ret +} + +/* +CheckCurrentServerVersion is based on the login method logic in order to establish the websocket connection and get +the current version from the server with the `admin init` command. This can be very useful for automations in which +you need to quickly perceive new versions (mostly patches) and update your application so it suddenly stops working. +*/ +func CheckCurrentServerVersion() ([]int, error) { + wac, err := NewConn(5 * time.Second) + if err != nil { + return nil, fmt.Errorf("fail to create connection") + } + + clientId := make([]byte, 16) + if _, err = rand.Read(clientId); err != nil { + return nil, fmt.Errorf("error creating random ClientId: %v", err) + } + + b64ClientId := base64.StdEncoding.EncodeToString(clientId) + login := []interface{}{"admin", "init", waVersion, []string{wac.longClientName, wac.shortClientName, wac.clientVersion}, b64ClientId, true} + loginChan, err := wac.writeJson(login) + if err != nil { + return nil, fmt.Errorf("error writing login: %s", err.Error()) + } + + // Retrieve an answer from the websocket + var r string + select { + case r = <-loginChan: + case <-time.After(wac.msgTimeout): + return nil, fmt.Errorf("login connection timed out") + } + + var resp map[string]interface{} + if err = json.Unmarshal([]byte(r), &resp); err != nil { + return nil, fmt.Errorf("error decoding login: %s", err.Error()) + } + + // Take the curr property as X.Y.Z and split it into as int slice + curr := resp["curr"].(string) + currArray := strings.Split(curr, ".") + version := make([]int, len(currArray)) + for i := range version { + version[i], _ = strconv.Atoi(currArray[i]) + } + + return version, nil +} + +/* +SetClientName sets the long and short client names that are sent to WhatsApp when logging in and displayed in the +WhatsApp Web device list. As the values are only sent when logging in, changing them after logging in is not possible. +*/ +func (wac *Conn) SetClientName(long, short string, version string) error { + if wac.session != nil && (wac.session.EncKey != nil || wac.session.MacKey != nil) { + return fmt.Errorf("cannot change client name after logging in") + } + wac.longClientName, wac.shortClientName, wac.clientVersion = long, short, version + return nil +} + +/* +SetClientVersion sets WhatsApp client version +Default value is 0.4.2080 +*/ +func (wac *Conn) SetClientVersion(major int, minor int, patch int) { + waVersion = []int{major, minor, patch} +} + +// GetClientVersion returns WhatsApp client version +func (wac *Conn) GetClientVersion() []int { + return waVersion +} + +/* +Login is the function that creates a new whatsapp session and logs you in. If you do not want to scan the qr code +every time, you should save the returned session and use RestoreWithSession the next time. Login takes a writable channel +as an parameter. This channel is used to push the data represented by the qr code back to the user. The received data +should be displayed as an qr code in a way you prefer. To print a qr code to console you can use: +github.com/Baozisoftware/qrcode-terminal-go Example login procedure: + wac, err := whatsapp.NewConn(5 * time.Second) + if err != nil { + panic(err) + } + + qr := make(chan string) + go func() { + terminal := qrcodeTerminal.New() + terminal.Get(<-qr).Print() + }() + + session, err := wac.Login(qr) + if err != nil { + fmt.Fprintf(os.Stderr, "error during login: %v\n", err) + } + fmt.Printf("login successful, session: %v\n", session) +*/ +func (wac *Conn) Login(qrChan chan<- string) (Session, error) { + session := Session{} + //Makes sure that only a single Login or Restore can happen at the same time + if !atomic.CompareAndSwapUint32(&wac.sessionLock, 0, 1) { + return session, ErrLoginInProgress + } + defer atomic.StoreUint32(&wac.sessionLock, 0) + + if wac.loggedIn { + return session, ErrAlreadyLoggedIn + } + + if err := wac.connect(); err != nil && err != ErrAlreadyConnected { + return session, err + } + + //logged in?!? + if wac.session != nil && (wac.session.EncKey != nil || wac.session.MacKey != nil) { + return session, fmt.Errorf("already logged in") + } + + clientId := make([]byte, 16) + _, err := rand.Read(clientId) + if err != nil { + return session, fmt.Errorf("error creating random ClientId: %v", err) + } + + session.ClientId = base64.StdEncoding.EncodeToString(clientId) + login := []interface{}{"admin", "init", waVersion, []string{wac.longClientName, wac.shortClientName, wac.clientVersion}, session.ClientId, true} + loginChan, err := wac.writeJson(login) + if err != nil { + return session, fmt.Errorf("error writing login: %v\n", err) + } + + var r string + select { + case r = <-loginChan: + case <-time.After(wac.msgTimeout): + return session, fmt.Errorf("login connection timed out") + } + + var resp map[string]interface{} + if err = json.Unmarshal([]byte(r), &resp); err != nil { + return session, fmt.Errorf("error decoding login resp: %v\n", err) + } + + var ref string + if rref, ok := resp["ref"].(string); ok { + ref = rref + } else { + return session, fmt.Errorf("error decoding login resp: invalid resp['ref']\n") + } + + priv, pub, err := curve25519.GenerateKey() + if err != nil { + return session, fmt.Errorf("error generating keys: %v\n", err) + } + + //listener for Login response + s1 := make(chan string, 1) + wac.listener.Lock() + wac.listener.m["s1"] = s1 + wac.listener.Unlock() + + qrChan <- fmt.Sprintf("%v,%v,%v", ref, base64.StdEncoding.EncodeToString(pub[:]), session.ClientId) + + var resp2 []interface{} + select { + case r1 := <-s1: + wac.loginSessionLock.Lock() + defer wac.loginSessionLock.Unlock() + if err := json.Unmarshal([]byte(r1), &resp2); err != nil { + return session, fmt.Errorf("error decoding qr code resp: %v", err) + } + case <-time.After(time.Duration(resp["ttl"].(float64)) * time.Millisecond): + return session, fmt.Errorf("qr code scan timed out") + } + + info := resp2[1].(map[string]interface{}) + + wac.Info = newInfoFromReq(info) + + session.ClientToken = info["clientToken"].(string) + session.ServerToken = info["serverToken"].(string) + session.Wid = info["wid"].(string) + s := info["secret"].(string) + decodedSecret, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return session, fmt.Errorf("error decoding secret: %v", err) + } + + var pubKey [32]byte + copy(pubKey[:], decodedSecret[:32]) + + sharedSecret := curve25519.GenerateSharedSecret(*priv, pubKey) + + hash := sha256.New + + nullKey := make([]byte, 32) + h := hmac.New(hash, nullKey) + h.Write(sharedSecret) + + sharedSecretExtended, err := hkdf.Expand(h.Sum(nil), 80, "") + if err != nil { + return session, fmt.Errorf("hkdf error: %v", err) + } + + //login validation + checkSecret := make([]byte, 112) + copy(checkSecret[:32], decodedSecret[:32]) + copy(checkSecret[32:], decodedSecret[64:]) + h2 := hmac.New(hash, sharedSecretExtended[32:64]) + h2.Write(checkSecret) + if !hmac.Equal(h2.Sum(nil), decodedSecret[32:64]) { + return session, fmt.Errorf("abort login") + } + + keysEncrypted := make([]byte, 96) + copy(keysEncrypted[:16], sharedSecretExtended[64:]) + copy(keysEncrypted[16:], decodedSecret[64:]) + + keyDecrypted, err := cbc.Decrypt(sharedSecretExtended[:32], nil, keysEncrypted) + if err != nil { + return session, fmt.Errorf("error decryptAes: %v", err) + } + + session.EncKey = keyDecrypted[:32] + session.MacKey = keyDecrypted[32:64] + wac.session = &session + wac.loggedIn = true + + return session, nil +} + +//TODO: GoDoc +/* +Basically the old RestoreSession functionality +*/ +func (wac *Conn) RestoreWithSession(session Session) (_ Session, err error) { + if wac.loggedIn { + return Session{}, ErrAlreadyLoggedIn + } + old := wac.session + defer func() { + if err != nil { + wac.session = old + } + }() + wac.session = &session + + if err = wac.Restore(); err != nil { + wac.session = nil + return Session{}, err + } + return *wac.session, nil +} + +/*//TODO: GoDoc +RestoreWithSession is the function that restores a given session. It will try to reestablish the connection to the +WhatsAppWeb servers with the provided session. If it succeeds it will return a new session. This new session has to be +saved because the Client and Server-Token will change after every login. Logging in with old tokens is possible, but not +suggested. If so, a challenge has to be resolved which is just another possible point of failure. +*/ +func (wac *Conn) Restore() error { + //Makes sure that only a single Login or Restore can happen at the same time + if !atomic.CompareAndSwapUint32(&wac.sessionLock, 0, 1) { + return ErrLoginInProgress + } + defer atomic.StoreUint32(&wac.sessionLock, 0) + + if wac.session == nil { + return ErrInvalidSession + } + + if err := wac.connect(); err != nil && err != ErrAlreadyConnected { + return err + } + + if wac.loggedIn { + return ErrAlreadyLoggedIn + } + + //listener for Conn or challenge; s1 is not allowed to drop + s1 := make(chan string, 1) + wac.listener.Lock() + wac.listener.m["s1"] = s1 + wac.listener.Unlock() + + //admin init + init := []interface{}{"admin", "init", waVersion, []string{wac.longClientName, wac.shortClientName, wac.clientVersion}, wac.session.ClientId, true} + initChan, err := wac.writeJson(init) + if err != nil { + return fmt.Errorf("error writing admin init: %v\n", err) + } + + //admin login with takeover + login := []interface{}{"admin", "login", wac.session.ClientToken, wac.session.ServerToken, wac.session.ClientId, "takeover"} + loginChan, err := wac.writeJson(login) + if err != nil { + return fmt.Errorf("error writing admin login: %v\n", err) + } + + select { + case r := <-initChan: + var resp map[string]interface{} + if err = json.Unmarshal([]byte(r), &resp); err != nil { + return fmt.Errorf("error decoding login connResp: %v\n", err) + } + + if int(resp["status"].(float64)) != 200 { + wac.timeTag = "" + return fmt.Errorf("init responded with %d", resp["status"]) + } + case <-time.After(wac.msgTimeout): + wac.timeTag = "" + return fmt.Errorf("restore session init timed out") + } + + //wait for s1 + var connResp []interface{} + select { + case r1 := <-s1: + if err := json.Unmarshal([]byte(r1), &connResp); err != nil { + wac.timeTag = "" + return fmt.Errorf("error decoding s1 message: %v\n", err) + } + case <-time.After(wac.msgTimeout): + wac.timeTag = "" + //check for an error message + select { + case r := <-loginChan: + var resp map[string]interface{} + if err = json.Unmarshal([]byte(r), &resp); err != nil { + return fmt.Errorf("error decoding login connResp: %v\n", err) + } + if int(resp["status"].(float64)) != 200 { + return fmt.Errorf("admin login responded with %d", int(resp["status"].(float64))) + } + default: + // not even an error message – assume timeout + return fmt.Errorf("restore session connection timed out") + } + } + + //check if challenge is present + if len(connResp) == 2 && connResp[0] == "Cmd" && connResp[1].(map[string]interface{})["type"] == "challenge" { + s2 := make(chan string, 1) + wac.listener.Lock() + wac.listener.m["s2"] = s2 + wac.listener.Unlock() + + if err := wac.resolveChallenge(connResp[1].(map[string]interface{})["challenge"].(string)); err != nil { + wac.timeTag = "" + return fmt.Errorf("error resolving challenge: %v\n", err) + } + + select { + case r := <-s2: + if err := json.Unmarshal([]byte(r), &connResp); err != nil { + wac.timeTag = "" + return fmt.Errorf("error decoding s2 message: %v\n", err) + } + case <-time.After(wac.msgTimeout): + wac.timeTag = "" + return fmt.Errorf("restore session challenge timed out") + } + } + + //check for login 200 --> login success + select { + case r := <-loginChan: + var resp map[string]interface{} + if err = json.Unmarshal([]byte(r), &resp); err != nil { + wac.timeTag = "" + return fmt.Errorf("error decoding login connResp: %v\n", err) + } + + if int(resp["status"].(float64)) != 200 { + wac.timeTag = "" + return fmt.Errorf("admin login responded with %d", resp["status"]) + } + case <-time.After(wac.msgTimeout): + wac.timeTag = "" + return fmt.Errorf("restore session login timed out") + } + + info := connResp[1].(map[string]interface{}) + + wac.Info = newInfoFromReq(info) + + //set new tokens + wac.session.ClientToken = info["clientToken"].(string) + wac.session.ServerToken = info["serverToken"].(string) + wac.session.Wid = info["wid"].(string) + wac.loggedIn = true + + return nil +} + +func (wac *Conn) resolveChallenge(challenge string) error { + decoded, err := base64.StdEncoding.DecodeString(challenge) + if err != nil { + return err + } + + h2 := hmac.New(sha256.New, wac.session.MacKey) + h2.Write([]byte(decoded)) + + ch := []interface{}{"admin", "challenge", base64.StdEncoding.EncodeToString(h2.Sum(nil)), wac.session.ServerToken, wac.session.ClientId} + challengeChan, err := wac.writeJson(ch) + if err != nil { + return fmt.Errorf("error writing challenge: %v\n", err) + } + + select { + case r := <-challengeChan: + var resp map[string]interface{} + if err := json.Unmarshal([]byte(r), &resp); err != nil { + return fmt.Errorf("error decoding login resp: %v\n", err) + } + if int(resp["status"].(float64)) != 200 { + return fmt.Errorf("challenge responded with %d\n", resp["status"]) + } + case <-time.After(wac.msgTimeout): + return fmt.Errorf("connection timed out") + } + + return nil +} + +/* +Logout is the function to logout from a WhatsApp session. Logging out means invalidating the current session. +The session can not be resumed and will disappear on your phone in the WhatsAppWeb client list. +*/ +func (wac *Conn) Logout() error { + login := []interface{}{"admin", "Conn", "disconnect"} + _, err := wac.writeJson(login) + if err != nil { + return fmt.Errorf("error writing logout: %v\n", err) + } + + wac.loggedIn = false + + return nil +} |