package main import ( "encoding/base64" "encoding/json" "errors" "fmt" "log" "os" "regexp" "strconv" "strings" "time" hibp "github.com/mattevans/pwned-passwords" ) type vpnSession struct { Time time.Time `json:"time"` Login string `json:"username"` Operation string `json:"operation"` Status string `json:"status"` Profile string `json:"profile"` TwoFA bool `json:"2fa_auth"` IP string `json:"client_ip"` PrivIP string `json:"private_ip"` AsNumber string `json:"as_number"` AsName string `json:"as_name"` NewAS bool `json:"as_new"` PwnedPasswd bool `json:"pwned_passwd"` Hostname string `json:"hostname"` TooMuchPwn bool `json:"too_much_pwn"` Mail string `json:"-"` cID int `json:"-"` kID int `json:"-"` port int `json:"-"` dev string `json:"-"` password string `json:"-"` otpCode string `json:"-"` localIP string `json:"-"` vpnserver string `json:"-"` pwnMail string `json:"-"` newAsMail string `json:"-"` MailFrom string `json:"-"` CcPwnPassword string `json:"-"` } func NewVPNSession(operation string) *vpnSession { v := vpnSession{ Time: time.Now().Round(time.Second), Status: "system failure", Operation: operation, } v.Hostname, _ = os.Hostname() return &v } func (c *vpnSession) String() string { if res, err := json.MarshalIndent(c, " ", " "); err == nil { return string(res) } return "" } func (c *vpnSession) b64Login() string { return base64.StdEncoding.EncodeToString([]byte(c.Login)) } func (c *vpnSession) baseHash(salt string, i int64) string { return fmt.Sprintf("%s%s%s%s", salt, c.Login, c.IP, i) } func (c *vpnSession) ParseSessionId(line string) error { var err error client_id := strings.Split(strings.Replace(line, ">CLIENT:CONNECT,", "", 1), ",") if c.cID, err = strconv.Atoi(client_id[0]); err != nil { return err } if c.kID, err = strconv.Atoi(client_id[1]); err != nil { return err } return nil } func (c *vpnSession) CheckPwn(password string) error { client := hibp.NewClient() pwned, err := client.Pwned.Compromised(password) if err != nil { return err } c.PwnedPasswd = pwned return nil } func (c *vpnSession) ParseEnv(infos *[]string) error { var err error r := regexp.MustCompile("[^a-zA-Z0-9./_@-]") for _, line := range *infos { p := strings.Split(strings.Replace(line, ">CLIENT:ENV,", "", 1), "=") switch p[0] { case "trusted_port": if c.port, err = strconv.Atoi(r.ReplaceAllString(p[1], "")); err != nil { return err } case "untrusted_port": if c.port, err = strconv.Atoi(r.ReplaceAllString(p[1], "")); err != nil { return err } case "trusted_ip": c.IP = r.ReplaceAllString(p[1], "") case "untrusted_ip": c.IP = r.ReplaceAllString(p[1], "") case "ifconfig_pool_remote_ip": c.PrivIP = r.ReplaceAllString(p[1], "") case "ifconfig_local": c.localIP = r.ReplaceAllString(p[1], "") case "password": switch { case strings.HasPrefix(p[1], "CRV1"): split := strings.Split(p[1], ":") if len(split) != 5 { break } c.password = split[2] c.otpCode = split[4] if c.otpCode == "" { c.otpCode = "***" } go c.CheckPwn(c.password) case strings.HasPrefix(p[1], "SCRV1"): split := strings.Split(p[1], ":") if len(split) != 3 { break } data, err := base64.StdEncoding.DecodeString(split[1]) if err != nil { break } c.password = string(data) data, err = base64.StdEncoding.DecodeString(split[2]) if err != nil { c.password = p[1] break } c.otpCode = string(data) if c.otpCode == "" { c.otpCode = "***" } default: c.password = p[1] c.otpCode = "" go c.CheckPwn(c.password) } case "username": c.Login = r.ReplaceAllString(p[1], "") case "dev": c.dev = r.ReplaceAllString(p[1], "") } } return nil } func (c *vpnSession) Auth(s *OpenVpnMgt) { var cmd []string var ip string var errIP error err, ok := c.auth(s) // if auth is ok, time to get an IP address if ok == 0 { ip, errIP = s.getIP(c) if errIP != nil { ok = -10 err = errIP } } switch { case ok == 0: cmd = []string{ fmt.Sprintf("client-auth %d %d", c.cID, c.kID), fmt.Sprintf("ifconfig-push %s %s", ip, c.localIP), } for _, r := range s.ldap[c.Profile].routes { cmd = append(cmd, fmt.Sprintf("push \"route %s vpn_gateway\"", r)) } cmd = append(cmd, "END") case ok < 0: cmd = []string{fmt.Sprintf("client-deny %d %d \"%s\" \"%s\"", c.cID, c.kID, err, err)} case ok == 1: cmd = []string{fmt.Sprintf( "client-deny %d %d \"Need OTP\" \"CRV1:R,E:%s:%s:OTP Code \"", c.cID, c.kID, c.password, c.b64Login())} } if err, _ := s.sendCommand(cmd, c.vpnserver); err != nil { c.LogPrintln(err) } return } // main authentication function. // returns 0 if auth is valid // returns 1 if an TOTP code is necessary // returns a negative if auth is not valid func (c *vpnSession) auth(s *OpenVpnMgt) (error, int) { // an empty password is not good if c.password == "" { c.Status = "Empty Password" return errors.New("Empty Password"), -1 } // check if the password is a valid token (see TOTP request) tokenPasswordOk, tokenPassword := s.TokenPassword(c) // password is a token. We remove it from the session object to // avoid checking it against the ldap if tokenPasswordOk { c.password = "" } // if the otp is not empty, we check it against the valid codes as soon as // possible otpvalidated := false if c.otpCode != "" { codes, err := s.GenerateOTP(c.Login) if err != nil { return err, -2 } for _, possible := range codes { if possible == c.otpCode { otpvalidated = true } } } c.Profile = "" login := []string{c.Login} pass := c.password for { n := c.Profile for k, ldap := range s.ldap { if ldap.upgradeFrom != c.Profile { continue } err, userOk, passOk, secondary := ldap.Auth(login, pass) // if there is an error, try the other configurations if err != nil { c.LogPrintln(err) continue } // we did find a valid User if userOk { // the login for the new auth level is given by the current one login = secondary if c.Mail == "" { c.Mail = secondary[0] } if passOk && c.Profile != "" { // it's at least the second auth level, and we have a valid // password on 2 different auth system. It's a dupplicate // password, let's log it log.Printf("User %s has a dupplicate password\n", c.Login) } // we have either a positive auth ok a previous valid one if passOk || c.Profile != "" || tokenPasswordOk { c.Profile = k } } } // no profile update this turn, no need to continue if n == c.Profile { break } } // no profile validated, we stop here if c.Profile == "" { c.Status = "fail (password)" return errors.New("Authentication Failed"), -3 } // check the MFA requested by the secured profile c.TwoFA = true switch s.ldap[c.Profile].mfaType { case "internal": if otpvalidated { return nil, 0 } // log that the failure is due to the OTP if c.otpCode == "" { c.Status = "Need OTP Code" } else { c.Status = "fail (OTP) : " } c.password = tokenPassword return errors.New("Need OTP Code"), 1 case "okta": //TODO implement okta MFA c.Status = "fail (Okta)" return nil, -4 default: c.TwoFA = false } // no MFA requested, the login is valid return nil, 0 }