diff --git a/cmd/bowser-create-account/bowser-create-account.go b/cmd/bowser-create-account/bowser-create-account.go index b929970..3dc032e 100644 --- a/cmd/bowser-create-account/bowser-create-account.go +++ b/cmd/bowser-create-account/bowser-create-account.go @@ -8,21 +8,47 @@ package main import ( "bufio" + "crypto/aes" + "crypto/cipher" "crypto/rand" + "crypto/sha1" "encoding/base32" + "encoding/base64" "encoding/json" "flag" "fmt" + "io" "os" "github.com/b1naryth1ef/bowser/lib" "github.com/mdp/qrterminal" "golang.org/x/crypto/bcrypt" + "golang.org/x/crypto/pbkdf2" "golang.org/x/crypto/ssh/terminal" ) var configPath = flag.String("config", "config.json", "path to config file") +func encryptTOTP(password []byte, salt []byte, totp []byte) ([]byte, error) { + dk := pbkdf2.Key(password, salt, 10000, 32, sha1.New) + + block, err := aes.NewCipher(dk) + if err != nil { + return nil, err + } + + ciphertext := make([]byte, aes.BlockSize+len(totp)) + iv := ciphertext[:aes.BlockSize] + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return nil, err + } + + stream := cipher.NewCFBEncrypter(block, iv) + stream.XORKeyStream(ciphertext[aes.BlockSize:], totp) + + return []byte(base64.URLEncoding.EncodeToString(ciphertext)), nil +} + func readPassword(attempts int) string { // Create a raw terminal so we can read the password without echo oldState, err := terminal.MakeRaw(0) @@ -72,7 +98,7 @@ func main() { totpRaw := make([]byte, 32) _, err := rand.Read(totpRaw) if err != nil { - fmt.Println("Failed to generate TOTP token") + fmt.Printf("Failed to generate TOTP token: %s\n", err) return } @@ -88,12 +114,19 @@ func main() { fmt.Printf("Please scan the above QR code with your TOTP app (or enter manually: `%s`)", totpEncoded) reader.ReadString('\n') + // Now encrypt the TOTP token with the password + totpEncrypted, err := encryptTOTP([]byte(password), []byte(username[:len(username)-1]), []byte(totpEncoded)) + if err != nil { + fmt.Printf("Failed to encrypt TOTP token: %v\n", err) + return + } + // Create a new account struct account := bowser.Account{ Username: username[:len(username)-1], Password: string(bcryptHash), SSHKeysRaw: []string{sshKey[:len(sshKey)-1]}, - MFA: bowser.AccountMFA{TOTP: string(totpEncoded)}, + MFA: bowser.AccountMFA{TOTP: string(totpEncrypted)}, } // If the configuration path was passed, we can attempt to append this to the diff --git a/lib/config.go b/lib/config.go index ef566cb..7425477 100644 --- a/lib/config.go +++ b/lib/config.go @@ -1,9 +1,15 @@ package bowser import ( + "crypto/aes" + "crypto/cipher" + "crypto/sha1" + "encoding/base64" "encoding/json" "io/ioutil" "regexp" + + "golang.org/x/crypto/pbkdf2" ) type AccountMFA struct { @@ -72,3 +78,25 @@ func (c *Config) SaveAccounts(acts []Account) (err error) { err = ioutil.WriteFile(c.AccountsPath, data, 644) return } + +func (am *AccountMFA) decryptTOTP(password []byte, salt []byte) (string, error) { + dk := pbkdf2.Key(password, salt, 10000, 32, sha1.New) + + ciphertext, _ := base64.URLEncoding.DecodeString(am.TOTP) + + block, err := aes.NewCipher(dk) + if err != nil { + return "", err + } + + if len(ciphertext) < aes.BlockSize { + return "", err + } + iv := ciphertext[:aes.BlockSize] + ciphertext = ciphertext[aes.BlockSize:] + + stream := cipher.NewCFBDecrypter(block, iv) + stream.XORKeyStream(ciphertext, ciphertext) + + return string(ciphertext), nil +} diff --git a/lib/session.go b/lib/session.go index efba238..8f7fefb 100644 --- a/lib/session.go +++ b/lib/session.go @@ -7,12 +7,10 @@ import ( "net" "sync" - _ "github.com/pquerna/otp/totp" "github.com/satori/go.uuid" "go.uber.org/zap" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" - _ "golang.org/x/crypto/ssh/terminal" ) // An account key represents a mapping of ssh public key to account diff --git a/lib/sshd.go b/lib/sshd.go index a99f839..0533dc2 100644 --- a/lib/sshd.go +++ b/lib/sshd.go @@ -217,6 +217,15 @@ func (s *SSHDState) Run() { if account.MFA.TOTP != "" { var verified bool + decryptedTOTP, err := account.MFA.decryptTOTP([]byte(passwordAnswer[0]), []byte(account.Username)) + if err != nil { + s.log.Warn( + "Failed to decrypt TOTP token", + zap.String("username", conn.User()), + zap.Error(err)) + return nil, badPasswordError + } + for i := 0; i < 3; i++ { mfaAnswer, err := client(conn.User(), "", []string{"MFA Code: "}, []bool{true}) @@ -224,7 +233,7 @@ func (s *SSHDState) Run() { continue } - if totp.Validate(mfaAnswer[0], account.MFA.TOTP) { + if totp.Validate(mfaAnswer[0], decryptedTOTP) { verified = true break }