* Implement enabling / disabling of two factor authentication

* Implement retrieval / resetting of two factor authentication
This commit is contained in:
Marcel Schramm 2019-11-03 01:16:47 +01:00
parent 97a3b313e1
commit 602b84f722
No known key found for this signature in database
GPG Key ID: 05971054C70EEDC7
11 changed files with 462 additions and 34 deletions

View File

@ -152,6 +152,10 @@ func Run() {
window.RegisterCommand(serverLeaveCmd)
window.RegisterCommand(commandimpls.NewServerCommand(serverJoinCmd, serverLeaveCmd))
window.RegisterCommand(commandimpls.NewNickSetCmd(discord, window))
window.RegisterCommand(commandimpls.NewTFAEnableCommand(window, discord))
window.RegisterCommand(commandimpls.NewTFADisableCommand(discord))
window.RegisterCommand(commandimpls.NewTFABackupGetCmd(discord, window))
window.RegisterCommand(commandimpls.NewTFABackupResetCmd(discord, window))
})
}()

View File

@ -1 +1,178 @@
package commandimpls
import (
"fmt"
"github.com/Bios-Marcel/cordless/commands"
"github.com/Bios-Marcel/cordless/config"
"github.com/Bios-Marcel/cordless/ui"
"github.com/Bios-Marcel/cordless/ui/tviewutil"
"github.com/Bios-Marcel/cordless/util/text"
"github.com/Bios-Marcel/discordgo"
"io"
)
type TFAEnableCmd struct {
window *ui.Window
session *discordgo.Session
}
func NewTFAEnableCommand(window *ui.Window, session *discordgo.Session) *TFAEnableCmd {
return &TFAEnableCmd{window, session}
}
func (cmd *TFAEnableCmd) Execute(writer io.Writer, parameters []string) {
if cmd.session.MFA {
fmt.Fprintln(writer, "TFA is already enabled on this account.")
} else {
cmd.window.ShowTFASetup()
}
}
func (cmd *TFAEnableCmd) PrintHelp(writer io.Writer) {
fmt.Fprintln(writer, "TODO")
}
func (cmd *TFAEnableCmd) Name() string {
return "tfa-enable"
}
func (cmd *TFAEnableCmd) Aliases() []string {
return []string{"mfa-enable", "totp-enable", "2fa-enable", "mfa-activate", "totp-activate", "2fa-activate"}
}
type TFADisableCmd struct {
session *discordgo.Session
}
func NewTFADisableCommand(session *discordgo.Session) *TFADisableCmd {
return &TFADisableCmd{session}
}
func (cmd *TFADisableCmd) Execute(writer io.Writer, parameters []string) {
if cmd.session.State.User.MFAEnabled {
if len(parameters) != 1 {
fmt.Fprintln(writer, "Usage: tfa-disable <TFA Token>")
} else {
code, parseError := text.ParseTFACode(parameters[0])
if parseError != nil {
commands.PrintError(writer, "Error disabling Two-Factor-Authentication", parseError.Error())
}
disableError := cmd.session.TwoFactorDisable(code)
if disableError != nil {
commands.PrintError(writer, "Error disabling Two-Factor-Authentication", disableError.Error())
} else {
config.UpdateCurrentToken(cmd.session.Token)
configError := config.PersistConfig()
if configError != nil {
commands.PrintError(writer, "Error updating access token in configuration. You might have to log in again.", disableError.Error())
}
}
}
} else {
fmt.Fprintln(writer, "TFA isn't enabled on this account.")
}
}
func (cmd *TFADisableCmd) PrintHelp(writer io.Writer) {
fmt.Fprintln(writer, "TODO")
}
func (cmd *TFADisableCmd) Name() string {
return "tfa-disable"
}
func (cmd *TFADisableCmd) Aliases() []string {
return []string{"mfa-disable", "totp-disable", "2fa-disable", "mfa-activate", "totp-activate", "2fa-activate"}
}
type TFABackupGetCmd struct {
window *ui.Window
session *discordgo.Session
}
func NewTFABackupGetCmd(session *discordgo.Session, window *ui.Window) *TFABackupGetCmd {
return &TFABackupGetCmd{window, session}
}
func (cmd *TFABackupGetCmd) Execute(writer io.Writer, parameters []string) {
if !cmd.session.MFA {
fmt.Fprintln(writer, "Two-Factor-Authentication isn't enabled on this account.")
} else {
go func() {
currentPassword := cmd.window.PromptSecretInput("Retrieving TFA backup codes", "Please enter your current password.")
if currentPassword == "" {
fmt.Fprintln(writer, "["+tviewutil.ColorToHex(config.GetTheme().ErrorColor)+"]Empty password, aborting.")
} else {
codes, codeError := cmd.session.GetTwoFactorBackupCodes(currentPassword)
if codeError != nil {
commands.PrintError(writer, "Error retrieving TFA backup codes", codeError.Error())
} else {
for _, code := range codes {
fmt.Fprintf(writer, "Code: %s | Already used: %v\n", code.Code, code.Consumed)
}
}
}
cmd.window.ForceRedraw()
}()
}
}
func (cmd *TFABackupGetCmd) PrintHelp(writer io.Writer) {
fmt.Fprintln(writer, "TODO")
}
func (cmd *TFABackupGetCmd) Name() string {
return "tfa-backup-get"
}
func (cmd *TFABackupGetCmd) Aliases() []string {
return []string{"mfa-backup-get", "2fa-backup-get", "totp-backup-get"}
}
type TFABackupResetCmd struct {
window *ui.Window
session *discordgo.Session
}
func NewTFABackupResetCmd(session *discordgo.Session, window *ui.Window) *TFABackupResetCmd {
return &TFABackupResetCmd{window, session}
}
func (cmd *TFABackupResetCmd) Execute(writer io.Writer, parameters []string) {
if !cmd.session.MFA {
fmt.Fprintln(writer, "Two-Factor-Authentication isn't enabled on this account.")
} else {
go func() {
currentPassword := cmd.window.PromptSecretInput("Resetting TFA backup codes", "Please enter your current password.")
if currentPassword == "" {
fmt.Fprintln(writer, "["+tviewutil.ColorToHex(config.GetTheme().ErrorColor)+"]Empty password, aborting.")
} else {
codes, codeError := cmd.session.RegenerateTwoFactorBackupCodes(currentPassword)
if codeError != nil {
commands.PrintError(writer, "Error resetting TFA backup codes", codeError.Error())
} else {
fmt.Fprintln(writer, "Newly generated codes:")
for _, code := range codes {
fmt.Fprintf(writer, " Code: %s | Already used: %v\n", code.Code, code.Consumed)
}
}
}
cmd.window.ForceRedraw()
}()
}
}
func (cmd *TFABackupResetCmd) PrintHelp(writer io.Writer) {
fmt.Fprintln(writer, "TODO")
}
func (cmd *TFABackupResetCmd) Name() string {
return "tfa-backup-Reset"
}
func (cmd *TFABackupResetCmd) Aliases() []string {
return []string{"mfa-backup-reset", "2fa-backup-reset", "totp-backup-reset"}
}

View File

@ -35,7 +35,7 @@ const (
)
var (
currentConfig = Config{
currentConfig = &Config{
Times: HourMinuteAndSeconds,
UseRandomUserColors: false,
ShowUserContainer: true,
@ -190,7 +190,7 @@ func GetConfigDirectory() (string, error) {
//GetConfig returns the currently loaded configuration.
func GetConfig() *Config {
return &currentConfig
return currentConfig
}
//LoadConfig loads the configuration initially and returns it.
@ -212,7 +212,7 @@ func LoadConfig() (*Config, error) {
defer configFile.Close()
decoder := json.NewDecoder(configFile)
configLoadError := decoder.Decode(&currentConfig)
configLoadError := decoder.Decode(currentConfig)
//io.EOF would mean empty, therefore we use defaults.
if configLoadError != nil && configLoadError != io.EOF {
@ -222,6 +222,18 @@ func LoadConfig() (*Config, error) {
return GetConfig(), nil
}
// UpdateCurrentToken updates the current token and all accounts where the
// token was also used.
func UpdateCurrentToken(newToken string) {
oldToken := currentConfig.Token
currentConfig.Token = newToken
for _, account := range currentConfig.Accounts {
if account.Token == oldToken {
account.Token = newToken
}
}
}
//PersistConfig saves the current configuration onto the filesystem.
func PersistConfig() error {
configFilePath, configError := GetConfigFile()
@ -229,7 +241,7 @@ func PersistConfig() error {
return configError
}
configAsJSON, jsonError := json.MarshalIndent(&currentConfig, "", " ")
configAsJSON, jsonError := json.MarshalIndent(currentConfig, "", " ")
if jsonError != nil {
return jsonError
}

4
go.mod
View File

@ -4,7 +4,7 @@ go 1.12
require (
github.com/Bios-Marcel/discordemojimap v0.0.0-20190404160132-506fd0e8d912
github.com/Bios-Marcel/discordgo v0.20.4-0.20191101185414-2d27b68262e6
github.com/Bios-Marcel/discordgo v0.20.4-0.20191103001117-f2fe84a74c31
github.com/Bios-Marcel/goclipimg v0.0.0-20190417192721-b58a8831f27d
github.com/Bios-Marcel/shortnotforlong v1.0.0
github.com/Bios-Marcel/tview v0.0.0-20191024171520-41147a2f8cf9
@ -16,6 +16,7 @@ require (
github.com/google/go-github/v28 v28.1.1
github.com/gopherjs/gopherwasm v1.1.0 // indirect
github.com/mattn/go-runewidth v0.0.4
github.com/mdp/qrterminal/v3 v3.0.0
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/pkg/errors v0.8.1
github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d
@ -23,4 +24,5 @@ require (
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 // indirect
gopkg.in/sourcemap.v1 v1.0.5 // indirect
gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2 // indirect
rsc.io/qr v0.2.0
)

19
go.sum
View File

@ -4,6 +4,16 @@ github.com/Bios-Marcel/discordgo v0.20.4-0.20191012193840-26cddf403c14 h1:rMm2kL
github.com/Bios-Marcel/discordgo v0.20.4-0.20191012193840-26cddf403c14/go.mod h1:bLnfQU0j/SejmPozgW5GepmKvd8CrbMIml2I0IZENVE=
github.com/Bios-Marcel/discordgo v0.20.4-0.20191101185414-2d27b68262e6 h1:Ed7t7xaw30EJqu6jqpAwHBjjq3A+de5+7qUwEkzcfd0=
github.com/Bios-Marcel/discordgo v0.20.4-0.20191101185414-2d27b68262e6/go.mod h1:bLnfQU0j/SejmPozgW5GepmKvd8CrbMIml2I0IZENVE=
github.com/Bios-Marcel/discordgo v0.20.4-0.20191102211109-f4dba5d0b18f h1:N2FNUB8sJKxg5JET8etmjxf0ewfVrNeUvbuo33ht26w=
github.com/Bios-Marcel/discordgo v0.20.4-0.20191102211109-f4dba5d0b18f/go.mod h1:bLnfQU0j/SejmPozgW5GepmKvd8CrbMIml2I0IZENVE=
github.com/Bios-Marcel/discordgo v0.20.4-0.20191102212203-083fab879a73 h1:x4QupyUFrYcrFHBUCoYxWeRQRQEPSGNaPzfedy3rmAM=
github.com/Bios-Marcel/discordgo v0.20.4-0.20191102212203-083fab879a73/go.mod h1:bLnfQU0j/SejmPozgW5GepmKvd8CrbMIml2I0IZENVE=
github.com/Bios-Marcel/discordgo v0.20.4-0.20191102220146-ec7184f674af h1:/LlHPzi6ReVpumlHG4kqMWAf0HaLJIkICsF7tWiVsZg=
github.com/Bios-Marcel/discordgo v0.20.4-0.20191102220146-ec7184f674af/go.mod h1:bLnfQU0j/SejmPozgW5GepmKvd8CrbMIml2I0IZENVE=
github.com/Bios-Marcel/discordgo v0.20.4-0.20191102224637-3522593afefe h1:FH0yMRNPaJEfFHy5om0sQLPLsqV5fK+Yx6u0cF0kzxA=
github.com/Bios-Marcel/discordgo v0.20.4-0.20191102224637-3522593afefe/go.mod h1:bLnfQU0j/SejmPozgW5GepmKvd8CrbMIml2I0IZENVE=
github.com/Bios-Marcel/discordgo v0.20.4-0.20191103001117-f2fe84a74c31 h1:P6AYRO9r8YplA9SllHVIXRBBqQNWI0DjVqQIsHTApEw=
github.com/Bios-Marcel/discordgo v0.20.4-0.20191103001117-f2fe84a74c31/go.mod h1:bLnfQU0j/SejmPozgW5GepmKvd8CrbMIml2I0IZENVE=
github.com/Bios-Marcel/goclipimg v0.0.0-20190417192721-b58a8831f27d h1:vfrX8l3fHuaP7gRgEc7mX/lHVTlfQcQeaIdSPmT6Ej0=
github.com/Bios-Marcel/goclipimg v0.0.0-20190417192721-b58a8831f27d/go.mod h1:u7z9t086HoIbA/uuoA2KcRDhKS47DRYCDZ39aegTR4c=
github.com/Bios-Marcel/shortnotforlong v1.0.0 h1:K4JJ5U3+D8LXoAiH0QbfMujWuqKo+NAEZKuQ2RHPqqE=
@ -64,10 +74,16 @@ github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJS
github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4=
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mdp/qrterminal v1.0.1 h1:07+fzVDlPuBlXS8tB0ktTAyf+Lp1j2+2zK3fBOL5b7c=
github.com/mdp/qrterminal v1.0.1/go.mod h1:Z33WhxQe9B6CdW37HaVqcRKzP+kByF3q/qLxOGe12xQ=
github.com/mdp/qrterminal/v3 v3.0.0 h1:ywQqLRBXWTktytQNDKFjhAvoGkLVN3J2tAFZ0kMd9xQ=
github.com/mdp/qrterminal/v3 v3.0.0/go.mod h1:NJpfAs7OAm77Dy8EkWrtE4aq+cE6McoLXlBqXQEwvE0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
@ -103,6 +119,7 @@ golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAG
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e h1:nFYrTHrdrAOpShe27kaFHjsqYSEQ0KWqdWLu3xuZJts=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -119,3 +136,5 @@ gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2 h1:MZF6J7CV6s/h0HBkfqebrYfKCVEo5iN+wzE4QhV3Evo=
gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2/go.mod h1:s1Sn2yZos05Qfs7NKt867Xe18emOmtsO3eAKbDaon0o=
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=

View File

@ -1,5 +0,0 @@
package main
func main() {
$END$
}

View File

@ -3,13 +3,12 @@ package ui
import (
"errors"
"github.com/Bios-Marcel/cordless/ui/tviewutil"
"github.com/Bios-Marcel/cordless/util/text"
"github.com/Bios-Marcel/discordgo"
"github.com/Bios-Marcel/tview"
"github.com/atotto/clipboard"
"github.com/gdamore/tcell"
"os"
"strconv"
"strings"
)
const splashText = `
@ -90,7 +89,7 @@ func NewLogin(app *tview.Application, configDir string) *Login {
splashScreen.SetTextAlign(tview.AlignCenter)
splashScreen.SetText(tviewutil.Escape(splashText + "\n\nConfig lies at: " + configDir))
login.AddItem(splashScreen, 12, 0, false)
login.AddItem(createCenteredComponent(login.messageText, 66), 0, 1, false)
login.AddItem(tviewutil.CreateCenteredComponent(login.messageText, 66), 0, 1, false)
login.messageText.SetDynamicColors(true)
@ -229,16 +228,16 @@ func NewLogin(app *tview.Application, configDir string) *Login {
})
loginChoiceView := tview.NewFlex().SetDirection(tview.FlexRow)
loginChoiceView.AddItem(createCenteredComponent(login.loginTypeTokenButton, 66), 1, 0, true)
loginChoiceView.AddItem(tviewutil.CreateCenteredComponent(login.loginTypeTokenButton, 66), 1, 0, true)
loginChoiceView.AddItem(tview.NewBox(), 1, 0, false)
loginChoiceView.AddItem(createCenteredComponent(tview.NewTextView().SetText("or").SetTextAlign(tview.AlignCenter), 66), 1, 0, false)
loginChoiceView.AddItem(tviewutil.CreateCenteredComponent(tview.NewTextView().SetText("or").SetTextAlign(tview.AlignCenter), 66), 1, 0, false)
loginChoiceView.AddItem(tview.NewBox(), 1, 0, false)
loginChoiceView.AddItem(createCenteredComponent(login.loginTypePasswordButton, 66), 1, 0, false)
loginChoiceView.AddItem(tviewutil.CreateCenteredComponent(login.loginTypePasswordButton, 66), 1, 0, false)
passwordInputView := tview.NewFlex().SetDirection(tview.FlexRow)
passwordInputView.AddItem(createCenteredComponent(login.usernameInput, 68), 3, 0, false)
passwordInputView.AddItem(createCenteredComponent(login.passwordInput, 68), 3, 0, false)
passwordInputView.AddItem(createCenteredComponent(login.tfaTokenInput, 68), 3, 0, false)
passwordInputView.AddItem(tviewutil.CreateCenteredComponent(login.usernameInput, 68), 3, 0, false)
passwordInputView.AddItem(tviewutil.CreateCenteredComponent(login.passwordInput, 68), 3, 0, false)
passwordInputView.AddItem(tviewutil.CreateCenteredComponent(login.tfaTokenInput, 68), 3, 0, false)
login.AddItem(login.content, 0, 0, false)
login.AddItem(tview.NewBox(), 0, 1, false)
@ -255,15 +254,6 @@ func configureInputComponent(component *tview.InputField) {
component.SetFieldWidth(66)
}
func createCenteredComponent(component tview.Primitive, width int) tview.Primitive {
padding := tview.NewFlex().SetDirection(tview.FlexColumn)
padding.AddItem(tview.NewBox(), 0, 1, false)
padding.AddItem(component, width, 0, false)
padding.AddItem(tview.NewBox(), 0, 1, false)
return padding
}
func (login *Login) attemptLogin() {
//Following two lines are little hack to prevent anything from being leftover
//in the terminal buffer from the previous view. This is a bug in the tview
@ -289,16 +279,17 @@ func (login *Login) attemptLogin() {
// Even if the login is supposed to be without two-factor-authentication, we
// attempt parsing a 2fa code, since the underlying rest-call can also handle
// non-2fa login calls.
var mfaToken int64
mfaTokenText := strings.ReplaceAll(login.tfaTokenInput.GetText(), " ", "")
if mfaTokenText != "" {
input := login.tfaTokenInput.GetText()
var mfaTokenText string
if input != "" {
var parseError error
mfaToken, parseError = strconv.ParseInt(mfaTokenText, 10, 32)
mfaTokenText, parseError = text.ParseTFACode(input)
if parseError != nil {
login.sessionChannel <- &loginAttempt{nil, errors.New("[red]Two-Factor-Authentication Code incorrect.\n\n[red]Correct example: 564 231")}
}
}
session, loginError := discordgo.NewWithPasswordAndMFA(userAgent, login.usernameInput.GetText(), login.passwordInput.GetText(), int(mfaToken))
session, loginError := discordgo.NewWithPasswordAndMFA(userAgent, login.usernameInput.GetText(), login.passwordInput.GetText(), mfaTokenText)
login.sessionChannel <- &loginAttempt{session, loginError}
}
@ -349,5 +340,5 @@ func (login *Login) showTokenLogin() {
func (login *Login) showView(view tview.Primitive, size int) {
login.content.RemoveAllItems()
login.ResizeItem(login.content, size, 0)
login.content.AddItem(createCenteredComponent(view, 68), size, 0, false)
login.content.AddItem(tviewutil.CreateCenteredComponent(view, 68), size, 0, false)
}

View File

@ -3,6 +3,8 @@ package ui
import (
"bytes"
"fmt"
"github.com/Bios-Marcel/cordless/util/text"
"github.com/mdp/qrterminal/v3"
"log"
"regexp"
"strings"
@ -2066,6 +2068,88 @@ func (window *Window) ExecuteCommand(input string) {
}
}
// ShowTFASetup generates a new TFA-Secret and shows a QR-Code. The QR-Code can
// be scanned and the resulting TFA-Token can be entered into cordless and used
// to enable TFA on this account.
func (window *Window) ShowTFASetup() {
tfaSecret := text.GenerateBase32Key()
qrURL := fmt.Sprintf("otpauth://totp/Discord:%s?secret=%s&issuer=Discord", window.session.State.User.Email, tfaSecret)
qrCodeText := text.GenerateQRCode(qrURL, qrterminal.M)
qrCodeImage := tview.NewTextView().SetText(qrCodeText).SetTextAlign(tview.AlignCenter)
qrCodeView := tview.NewFlex().SetDirection(tview.FlexRow)
qrCodeView.AddItem(qrCodeImage, strings.Count(qrCodeText, "\n")+1, 0, false)
defaultInstructions := "1. Scan the QR-Code with your 2FA application\n2. Enter the code generated on your 2FA device\n3. Hit Enter!"
message := tview.NewTextView().SetText(defaultInstructions).SetDynamicColors(true)
qrCodeView.AddItem(tviewutil.CreateCenteredComponent(message, 68), 0, 1, false)
tokenInput := tview.NewInputField()
tokenInput.SetBorder(true)
tokenInput.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyEnter {
code, codeError := text.ParseTFACode(tokenInput.GetText())
if codeError != nil {
message.SetText(fmt.Sprintf("%s\n\n[red]Code invalid:\n\t[red]%s", defaultInstructions, codeError))
return nil
}
//panic(fmt.Sprintf("Secret: %s\nCode: %s", tfaSecret, code))
backupCodes, tfaError := window.session.TwoFactorEnable(tfaSecret, code)
if tfaError != nil {
message.SetText(fmt.Sprintf("%s\n\n[red]Error setting up Two-Factor-Authentication:\n\t[red]%s", defaultInstructions, tfaError))
return nil
}
//The token is being updated internally, therefore we need to update our config.
config.UpdateCurrentToken(window.session.Token)
configError := config.PersistConfig()
if configError != nil {
log.Println(fmt.Sprintf("Error settings new token: %s\n\t%s", window.session.Token, configError))
}
var backupCodesAsString string
for index, backupCode := range backupCodes {
if index != 0 {
backupCodesAsString += "\n"
}
backupCodesAsString += backupCode.Code
}
clipboard.WriteAll(backupCodesAsString)
successText := tview.NewTextView().SetTextAlign(tview.AlignCenter)
successText.SetText("Setting up Two-Factor-Authentication was a success.\n\n" +
"The backup codes have been put into your clipboard." +
"If you need to view your backup codes again, just run `tfa backup` in the cordless CLI.\n\n" +
"Currently cordless doesn't support applying backup codes.")
successView := tview.NewFlex().SetDirection(tview.FlexRow)
okayButton := tview.NewButton("Okay")
okayButton.SetSelectedFunc(func() {
window.app.SetRoot(window.rootContainer, true)
})
successView.AddItem(successText, 0, 1, false)
successView.AddItem(okayButton, 1, 0, false)
window.app.SetRoot(tviewutil.CreateCenteredComponent(successView, 68), true)
window.app.SetFocus(okayButton)
return nil
}
if event.Key() == tcell.KeyESC {
window.app.SetRoot(window.rootContainer, true)
window.app.ForceDraw()
return nil
}
return event
})
qrCodeView.AddItem(tviewutil.CreateCenteredComponent(tokenInput, 68), 3, 0, false)
window.app.SetRoot(qrCodeView, true)
window.app.SetFocus(tokenInput)
}
func (window *Window) startEditingMessage(message *discordgo.Message) {
if message.Author.ID == window.session.State.User.ID {
window.messageInput.SetText(message.Content)

23
util/text/qrcode.go Normal file
View File

@ -0,0 +1,23 @@
package text
import (
"bytes"
"github.com/mdp/qrterminal/v3"
"rsc.io/qr"
)
func GenerateQRCode(text string, redundancyLevel qr.Level) string {
buffer := bytes.NewBufferString("")
qrConfig := qrterminal.Config{
Level: redundancyLevel,
Writer: buffer,
HalfBlocks: true,
BlackChar: qrterminal.BLACK_BLACK,
WhiteBlackChar: qrterminal.WHITE_BLACK,
WhiteChar: qrterminal.WHITE_WHITE,
BlackWhiteChar: qrterminal.BLACK_WHITE,
QuietZone: 1,
}
qrterminal.GenerateWithConfig(text, qrConfig)
return buffer.String()
}

45
util/text/totp.go Normal file
View File

@ -0,0 +1,45 @@
package text
import (
"errors"
"fmt"
"math/rand"
"strconv"
"strings"
)
// ParseTFACodes takes an arbitrary string and checks whether it's a valid 6
// digit number for usage as a tfa code.
func ParseTFACode(text string) (string, error) {
var mfaToken int64
mfaTokenText := strings.ReplaceAll(text, " ", "")
if mfaTokenText != "" {
var parseError error
mfaToken, parseError = strconv.ParseInt(mfaTokenText, 10, 32)
if parseError != nil {
return "", errors.New("token has to be a 6 digit number between 000000 and 999999")
}
if mfaToken > 999999 || mfaToken < 0 {
return "", errors.New("token has to be a 6 digit number between 000000 and 999999")
}
return fmt.Sprintf("%06d", mfaToken), nil
}
return "", errors.New("tfa code must not be empty")
}
// GenerateBase32Key generates a 16 character key containing 2-7 and A-Z.
func GenerateBase32Key() string {
tfaSecretRaw := make([]rune, 16, 16)
availableCharacters := [...]rune{
'2', '3', '4', '5', '6', '7',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
}
for i := 0; i < 16; i++ {
tfaSecretRaw[i] = availableCharacters[rand.Int31n(int32(len(availableCharacters)))]
}
return string(tfaSecretRaw)
}

76
util/text/totp_test.go Normal file
View File

@ -0,0 +1,76 @@
package text
import "testing"
func TestParseTFACode(t *testing.T) {
tests := []struct {
name string
text string
want string
wantErr bool
}{
{
name: "empty",
text: "",
want: "",
wantErr: true,
}, {
name: "empty, but spaces",
text: " ",
want: "",
wantErr: true,
}, {
name: "negative number of length 7 including the minus",
text: "-100000",
want: "",
wantErr: true,
}, {
name: "negative number of length 6 including the minus",
text: "-10000",
want: "",
wantErr: true,
}, {
name: "negative 0",
text: "-0",
want: "000000",
wantErr: false,
}, {
name: "0",
text: "0",
want: "000000",
wantErr: false,
}, {
name: "upper limit",
text: "999999",
want: "999999",
wantErr: false,
}, {
name: "above upper limit",
text: "1000000",
want: "",
wantErr: true,
}, {
name: "non numeric",
text: "javascript is good",
want: "",
wantErr: true,
}, {
name: "correct with spaces",
text: " 123456 ",
want: "123456",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseTFACode(tt.text)
if (err != nil) != tt.wantErr {
t.Errorf("ParseTFACode() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("ParseTFACode() got = %v, want %v", got, tt.want)
}
})
}
}