package auth // package for authenticating websockets // we use a homespun jwt knock-off because no tls on lan // authentication adds you to the AuthenticatedClients map // broadcasts get sent to members of this map import ( "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "goseg/config" "goseg/structs" "net" "net/http" "strings" "sync" "time" "github.com/gorilla/websocket" "golang.org/x/crypto/nacl/secretbox" ) var ( // maps a websocket conn to a tokenid // tokenid's can be referenced from the global conf AuthenticatedClients = struct { Conns map[string]*websocket.Conn sync.RWMutex }{ Conns: make(map[string]*websocket.Conn), } UnauthClients = struct { Conns map[string]*websocket.Conn sync.RWMutex }{ Conns: make(map[string]*websocket.Conn), } ) // check if websocket-token pair is auth'd func WsIsAuthenticated(conn *websocket.Conn, token string) bool { AuthenticatedClients.RLock() defer AuthenticatedClients.RUnlock() if AuthenticatedClients.Conns[token] == conn { return true } else { return false } } // quick check if websocket is authed at all for unauth broadcast (not for actual auth) func WsAuthCheck(conn *websocket.Conn) bool { AuthenticatedClients.RLock() defer AuthenticatedClients.RUnlock() for _, con := range AuthenticatedClients.Conns { if con == conn { config.Logger.Info("Client in auth map") return true } } config.Logger.Info("Client not in auth map") return false } // this takes a bool for auth/unauth -- also persists to config func AddToAuthMap(conn *websocket.Conn, token map[string]string, authed bool) error { tokenStr := token["token"] tokenId := token["id"] hashed := sha256.Sum256([]byte(tokenStr)) hash := hex.EncodeToString(hashed[:]) if authed { AuthenticatedClients.Lock() AuthenticatedClients.Conns[tokenId] = conn AuthenticatedClients.Unlock() UnauthClients.Lock() if _, ok := UnauthClients.Conns[tokenId]; ok { delete(UnauthClients.Conns, tokenId) } UnauthClients.Unlock() } else { UnauthClients.Lock() UnauthClients.Conns[tokenId] = conn UnauthClients.Unlock() AuthenticatedClients.Lock() if _, ok := AuthenticatedClients.Conns[tokenId]; ok { delete(AuthenticatedClients.Conns, tokenId) } AuthenticatedClients.Unlock() } now := time.Now().Format("2006-01-02_15:04:05") err := AddSession(tokenId, hash, now, authed) if err != nil { return err } return nil } // check the validity of the token func CheckToken(token map[string]string, conn *websocket.Conn, r *http.Request, setup bool) bool { // great you have token. we see if valid. if token["token"] == "" { return false } config.Logger.Info(fmt.Sprintf("Checking token %s",token["id"])) conf := config.Conf() key := conf.KeyFile res, err := KeyfileDecrypt(token["token"], key) if err != nil { config.Logger.Warn("Invalid token provided") return false } else { // so you decrypt. now we see the useragent and ip. var ip string if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" { ip = strings.Split(forwarded, ",")[0] } else { ip, _, _ = net.SplitHostPort(r.RemoteAddr) } userAgent := r.Header.Get("User-Agent") hashed := sha256.Sum256([]byte(token["token"])) hash := hex.EncodeToString(hashed[:]) // you in auth map? if WsIsAuthenticated(conn, hash) { if ip == res["ip"] && userAgent == res["user_agent"] { config.Logger.Info("Token authenticated") return true } else { config.Logger.Warn("Token doesn't match session!") return false } } else { config.Logger.Warn("Token isn't an authenticated session") return false } } return false } // create a new session token func CreateToken(conn *websocket.Conn, r *http.Request, setup bool) (map[string]string, error) { // extract conn info var ip string if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" { ip = strings.Split(forwarded, ",")[0] } else { ip, _, _ = net.SplitHostPort(r.RemoteAddr) } userAgent := r.Header.Get("User-Agent") conf := config.Conf() now := time.Now().Format("2006-01-02_15:04:05") // generate random strings for id, secret, and padding id := config.RandString(32) secret := config.RandString(128) padding := config.RandString(32) contents := map[string]string{ "id": id, "ip": ip, "user_agent": userAgent, "secret": secret, "padding": padding, "authorized": fmt.Sprintf("%v", setup), "created": now, } // encrypt the contents key := conf.KeyFile encryptedText, err := KeyfileEncrypt(contents, key) if err != nil { config.Logger.Error(fmt.Sprintf("failed to encrypt token: %v", err)) return nil, fmt.Errorf("failed to encrypt token: %v", err) } token := map[string]string{ "id": id, "token": encryptedText, } // Update sessions in the system's configuration AddToAuthMap(conn, token, setup) return token, nil } // take session details and add to SysConfig func AddSession(tokenID string, hash string, created string, authorized bool) error { session := structs.SessionInfo{ Hash: hash, Created: created, } if authorized { update := map[string]interface{}{ "Sessions": map[string]interface{}{ "Authorized": map[string]string{ "Hash": session.Hash, "Created": session.Created, }, }, } if err := config.UpdateConf(update); err != nil { return fmt.Errorf("Error adding session: %v", err) } if err := config.RemoveSession(tokenID, false); err != nil { return fmt.Errorf("Error removing session: %v", err) } } else { update := map[string]interface{}{ "Sessions": map[string]interface{}{ "Unauthorized": map[string]string{ "Hash": session.Hash, "Created": session.Created, }, }, } if err := config.UpdateConf(update); err != nil { return fmt.Errorf("Error adding session: %v", err) } if err := config.RemoveSession(tokenID, true); err != nil { return fmt.Errorf("Error removing session: %v", err) } } return nil } // encrypt the contents using stored keyfile val func KeyfileEncrypt(contents map[string]string, key string) (string, error) { contentBytes, err := json.Marshal(contents) if err != nil { return "", err } // convert key to bytes keyBytes := []byte(key) if len(keyBytes) != 32 { return "", fmt.Errorf("key must be 32 bytes in length") } var keyArray [32]byte copy(keyArray[:], keyBytes) // generate nonce var nonce [24]byte if _, err := rand.Read(nonce[:]); err != nil { return "", err } // encrypt contents encrypted := secretbox.Seal(nonce[:], contentBytes, &nonce, &keyArray) return base64.URLEncoding.EncodeToString(encrypted), nil } // decrypt routine func KeyfileDecrypt(encryptedText string, key string) (map[string]string, error) { // get bytes keyBytes := []byte(key) var keyArray [32]byte copy(keyArray[:], keyBytes) encryptedBytes, err := base64.URLEncoding.DecodeString(encryptedText) if err != nil { return nil, err } // get nonce var nonce [24]byte copy(nonce[:], encryptedBytes[:24]) // attempt decrypt decrypted, ok := secretbox.Open(nil, encryptedBytes[24:], &nonce, &keyArray) if !ok { return nil, fmt.Errorf("Decryption failed") } var contents map[string]string if err := json.Unmarshal(decrypted, &contents); err != nil { return nil, err } return contents, nil } // salted sha256 func Hasher(password string) string { conf := config.Conf() salt := conf.Salt toHash := salt + password res := sha256.Sum256([]byte(toHash)) return hex.EncodeToString(res[:]) } // check if pw matches sysconfig func AuthenticateLogin(password string) bool { conf := config.Conf() hash := Hasher(password) if hash == conf.PwHash { return true } else { config.Logger.Warn(fmt.Sprintf("debug: failed pw hash: %v", hash)) return false } }