summaryrefslogtreecommitdiffstats
path: root/vendor/github.com/Rhymen/go-whatsapp/session.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/Rhymen/go-whatsapp/session.go')
-rw-r--r--vendor/github.com/Rhymen/go-whatsapp/session.go532
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
+}