mirror of
https://github.com/crunchy-labs/crunchy-cli.git
synced 2026-01-21 12:12:00 -06:00
Add basic encrypted login credentials support
This commit is contained in:
parent
b491ba0f58
commit
a4ec163275
4 changed files with 205 additions and 11 deletions
|
|
@ -1,15 +1,22 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/ByteDream/crunchyroll-go/v2"
|
"github.com/ByteDream/crunchyroll-go/v2"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
loginPersistentFlag bool
|
loginPersistentFlag bool
|
||||||
|
loginEncryptFlag bool
|
||||||
|
|
||||||
loginSessionIDFlag bool
|
loginSessionIDFlag bool
|
||||||
)
|
)
|
||||||
|
|
@ -33,6 +40,10 @@ func init() {
|
||||||
"persistent",
|
"persistent",
|
||||||
false,
|
false,
|
||||||
"If the given credential should be stored persistent")
|
"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,
|
loginCmd.Flags().BoolVar(&loginSessionIDFlag,
|
||||||
"session-id",
|
"session-id",
|
||||||
|
|
@ -49,20 +60,74 @@ func loginCredentials(user, password string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(c.SessionID), 0600); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if loginPersistentFlag {
|
if loginPersistentFlag {
|
||||||
if configDir, err := os.UserConfigDir(); err != nil {
|
if configDir, err := os.UserConfigDir(); err != nil {
|
||||||
return fmt.Errorf("could not save credentials persistent: %w", err)
|
return fmt.Errorf("could not save credentials persistent: %w", err)
|
||||||
} else {
|
} 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)
|
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
|
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 {
|
if !loginPersistentFlag {
|
||||||
out.Info("Due to security reasons, you have to login again on the next reboot")
|
out.Info("Due to security reasons, you have to login again on the next reboot")
|
||||||
|
|
|
||||||
48
cmd/crunchyroll-go/cmd/unix.go
Normal file
48
cmd/crunchyroll-go/cmd/unix.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/ByteDream/crunchyroll-go/v2"
|
"github.com/ByteDream/crunchyroll-go/v2"
|
||||||
"github.com/ByteDream/crunchyroll-go/v2/utils"
|
"github.com/ByteDream/crunchyroll-go/v2/utils"
|
||||||
|
|
@ -167,13 +170,49 @@ func loadCrunchy() {
|
||||||
}
|
}
|
||||||
split := strings.SplitN(string(body), "\n", 2)
|
split := strings.SplitN(string(body), "\n", 2)
|
||||||
if len(split) == 1 || split[1] == "" {
|
if len(split) == 1 || split[1] == "" {
|
||||||
split[0] = url.QueryEscape(split[0])
|
if strings.HasPrefix(split[0], "aes:") {
|
||||||
if crunchy, err = crunchyroll.LoginWithSessionID(split[0], systemLocale(true), client); err != nil {
|
encrypted := body[4:]
|
||||||
out.StopProgress(err.Error())
|
|
||||||
os.Exit(1)
|
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 {
|
if crunchy, err = crunchyroll.LoginWithCredentials(split[0], split[1], systemLocale(true), client); err != nil {
|
||||||
out.StopProgress(err.Error())
|
out.StopProgress(err.Error())
|
||||||
os.Exit(1)
|
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
|
// 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)
|
os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(crunchy.SessionID), 0600)
|
||||||
}
|
}
|
||||||
|
|
||||||
out.StopProgress("Logged in")
|
out.StopProgress("Logged in")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
41
cmd/crunchyroll-go/cmd/windows.go
Normal file
41
cmd/crunchyroll-go/cmd/windows.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue