From a4ec163275ab6fb6961cc6e208d959ee02399a80 Mon Sep 17 00:00:00 2001 From: bytedream Date: Fri, 20 May 2022 22:57:07 +0200 Subject: [PATCH] Add basic encrypted login credentials support --- cmd/crunchyroll-go/cmd/login.go | 75 ++++++++++++++++++++++++++++--- cmd/crunchyroll-go/cmd/unix.go | 48 ++++++++++++++++++++ cmd/crunchyroll-go/cmd/utils.go | 52 ++++++++++++++++++--- cmd/crunchyroll-go/cmd/windows.go | 41 +++++++++++++++++ 4 files changed, 205 insertions(+), 11 deletions(-) create mode 100644 cmd/crunchyroll-go/cmd/unix.go create mode 100644 cmd/crunchyroll-go/cmd/windows.go diff --git a/cmd/crunchyroll-go/cmd/login.go b/cmd/crunchyroll-go/cmd/login.go index c9fc923..565dac0 100644 --- a/cmd/crunchyroll-go/cmd/login.go +++ b/cmd/crunchyroll-go/cmd/login.go @@ -1,15 +1,22 @@ package cmd import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha256" "fmt" "github.com/ByteDream/crunchyroll-go/v2" "github.com/spf13/cobra" + "io" "os" "path/filepath" ) var ( loginPersistentFlag bool + loginEncryptFlag bool loginSessionIDFlag bool ) @@ -33,6 +40,10 @@ func init() { "persistent", false, "If the given credential should be stored persistent") + loginCmd.Flags().BoolVar(&loginEncryptFlag, + "encrypt", + false, + "Encrypt the given credentials (won't do anything if --session-id is given)") loginCmd.Flags().BoolVar(&loginSessionIDFlag, "session-id", @@ -49,20 +60,74 @@ func loginCredentials(user, password string) error { return err } + if err = os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(c.SessionID), 0600); err != nil { + return err + } + if loginPersistentFlag { if configDir, err := os.UserConfigDir(); err != nil { return fmt.Errorf("could not save credentials persistent: %w", err) } else { + var credentials []byte + + if loginEncryptFlag { + var passwd []byte + + for { + fmt.Print("Enter password: ") + passwd, err = readLineSilent() + if err != nil { + return err + } + fmt.Println() + + fmt.Print("Enter password again: ") + repasswd, err := readLineSilent() + if err != nil { + return err + } + fmt.Println() + + if !bytes.Equal(passwd, repasswd) { + fmt.Println("Passwords does not match, try again") + continue + } + + hashedPassword := sha256.Sum256(passwd) + block, err := aes.NewCipher(hashedPassword[:]) + if err != nil { + out.Err("Failed to create block: %w", err) + os.Exit(1) + } + gcm, err := cipher.NewGCM(block) + if err != nil { + out.Err("Failed to create gcm: %w", err) + os.Exit(1) + } + nonce := make([]byte, gcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + out.Err("Failed to fill nonce: %w", err) + os.Exit(1) + } + + b := gcm.Seal(nonce, nonce, []byte(fmt.Sprintf("%s\n%s", user, password)), nil) + credentials = append([]byte("aes:"), b...) + + break + } + } else { + credentials = []byte(fmt.Sprintf("%s\n%s", user, password)) + } + os.MkdirAll(filepath.Join(configDir, "crunchyroll-go"), 0755) - if err = os.WriteFile(filepath.Join(configDir, "crunchyroll-go", "crunchy"), []byte(fmt.Sprintf("%s\n%s", user, password)), 0600); err != nil { + if err = os.WriteFile(filepath.Join(configDir, "crunchyroll-go", "crunchy"), credentials, 0600); err != nil { return err } - out.Info("The login information will be stored permanently UNENCRYPTED on your drive (%s)", filepath.Join(configDir, "crunchyroll-go", "crunchy")) + if !loginEncryptFlag { + out.Info("The login information will be stored permanently UNENCRYPTED on your drive (%s)", filepath.Join(configDir, "crunchyroll-go", "crunchy")) + } } } - if err = os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(c.SessionID), 0600); err != nil { - return err - } if !loginPersistentFlag { out.Info("Due to security reasons, you have to login again on the next reboot") diff --git a/cmd/crunchyroll-go/cmd/unix.go b/cmd/crunchyroll-go/cmd/unix.go new file mode 100644 index 0000000..962088f --- /dev/null +++ b/cmd/crunchyroll-go/cmd/unix.go @@ -0,0 +1,48 @@ +//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos + +package cmd + +import ( + "bufio" + "os" + "os/exec" + "syscall" +) + +// https://github.com/bgentry/speakeasy/blob/master/speakeasy_unix.go +var stty string + +func init() { + var err error + if stty, err = exec.LookPath("stty"); err != nil { + panic(err) + } +} + +func readLineSilent() ([]byte, error) { + pid, err := setEcho(false) + if err != nil { + return nil, err + } + defer setEcho(true) + + syscall.Wait4(pid, nil, 0, nil) + + l, _, err := bufio.NewReader(os.Stdin).ReadLine() + return l, err +} + +func setEcho(on bool) (pid int, err error) { + fds := []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()} + + if on { + pid, err = syscall.ForkExec(stty, []string{"stty", "echo"}, &syscall.ProcAttr{Files: fds}) + } else { + pid, err = syscall.ForkExec(stty, []string{"stty", "-echo"}, &syscall.ProcAttr{Files: fds}) + } + + if err != nil { + return 0, err + } + return +} diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go index 2a632ff..d7eba3d 100644 --- a/cmd/crunchyroll-go/cmd/utils.go +++ b/cmd/crunchyroll-go/cmd/utils.go @@ -1,6 +1,9 @@ package cmd import ( + "crypto/aes" + "crypto/cipher" + "crypto/sha256" "fmt" "github.com/ByteDream/crunchyroll-go/v2" "github.com/ByteDream/crunchyroll-go/v2/utils" @@ -167,13 +170,49 @@ func loadCrunchy() { } split := strings.SplitN(string(body), "\n", 2) if len(split) == 1 || split[1] == "" { - split[0] = url.QueryEscape(split[0]) - if crunchy, err = crunchyroll.LoginWithSessionID(split[0], systemLocale(true), client); err != nil { - out.StopProgress(err.Error()) - os.Exit(1) + if strings.HasPrefix(split[0], "aes:") { + encrypted := body[4:] + + out.StopProgress("Credentials are encrypted") + fmt.Print("Enter password to encrypt it: ") + passwd, err := readLineSilent() + fmt.Println() + if err != nil { + out.Err("Failed to read password; %w", err) + os.Exit(1) + } + out.SetProgress("Logging in") + + hashedPassword := sha256.Sum256(passwd) + block, err := aes.NewCipher(hashedPassword[:]) + if err != nil { + out.Err("Failed to create block: %w", err) + os.Exit(1) + } + gcm, err := cipher.NewGCM(block) + if err != nil { + out.Err("Failed to create gcm: %w", err) + os.Exit(1) + } + nonce, c := encrypted[:gcm.NonceSize()], encrypted[gcm.NonceSize():] + + b, err := gcm.Open(nil, nonce, c, nil) + if err != nil { + out.StopProgress("Invalid password") + os.Exit(1) + } + split = strings.SplitN(string(b), "\n", 2) + } else { + split[0] = url.QueryEscape(split[0]) + if crunchy, err = crunchyroll.LoginWithSessionID(split[0], systemLocale(true), client); err != nil { + out.StopProgress(err.Error()) + os.Exit(1) + } + out.Debug("Logged in with session id %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", split[0]) } - out.Debug("Logged in with session id %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", split[0]) - } else { + } + + if len(split) == 2 { if crunchy, err = crunchyroll.LoginWithCredentials(split[0], split[1], systemLocale(true), client); err != nil { out.StopProgress(err.Error()) os.Exit(1) @@ -183,6 +222,7 @@ func loadCrunchy() { // it seems like that crunchyroll has also a little cooldown if a user logs in too often in a short time os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(crunchy.SessionID), 0600) } + out.StopProgress("Logged in") return } diff --git a/cmd/crunchyroll-go/cmd/windows.go b/cmd/crunchyroll-go/cmd/windows.go new file mode 100644 index 0000000..d6eecb1 --- /dev/null +++ b/cmd/crunchyroll-go/cmd/windows.go @@ -0,0 +1,41 @@ +//go:build windows + +package cmd + +import ( + "bufio" + "os" + "syscall" +) + +// https://github.com/bgentry/speakeasy/blob/master/speakeasy_windows.go +func readLineSilent() ([]byte, error) { + var oldMode uint32 + + if err := syscall.GetConsoleMode(syscall.Stdin, &oldMode); err != nil { + return nil, err + } + + newMode := oldMode &^ 0x0004 + + err := setConsoleMode(syscall.Stdin, newMode) + defer setConsoleMode(syscall.Stdin, oldMode) + + if err != nil { + return nil, err + } + + l, _, err := bufio.NewReader(os.Stdin).ReadLine() + if err != nil { + return nil, err + } + return l, err +} + +func setConsoleMode(console syscall.Handle, mode uint32) error { + dll := syscall.MustLoadDLL("kernel32") + proc := dll.MustFindProc("SetConsoleMode") + _, _, err := proc.Call(uintptr(console), uintptr(mode)) + + return err +}