commit cb7e5027ede5d09542bc0df7f06a23cc5655d3fd Author: Carl Kittelberger Date: Mon Sep 4 10:36:34 2017 +0200 Initial commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ab4604b --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +# Binaries +*.exe +/pixelqr + +# Test files +*.test diff --git a/internal/qrgrid_to_ansimage.go b/internal/qrgrid_to_ansimage.go new file mode 100644 index 0000000..e386543 --- /dev/null +++ b/internal/qrgrid_to_ansimage.go @@ -0,0 +1,52 @@ +package internal + +import ( + "github.com/eliukblau/pixterm/ansimage" + "github.com/qpliu/qrencode-go/qrencode" +) + +func ConvertGridToANSImage(grid *qrencode.BitGrid) (s string, err error) { + // HACK - ansimage has an image offset bug which hides the first two rows + img, err := ansimage.New(2+2+((grid.Height()+1)/2)*2, 2+grid.Width()) + if err != nil { + return + } + + // white borders + for x := 0; x < 1+grid.Width()+1; x++ { + err = img.SetAt(2, x, 255, 255, 255) + if err != nil { + return + } + err = img.SetAt(2+grid.Height()+1, x, 255, 255, 255) + if err != nil { + return + } + } + for y := 1; y < grid.Height()+3; y++ { + err = img.SetAt(y, 0, 255, 255, 255) + if err != nil { + return + } + err = img.SetAt(y, grid.Width()+1, 255, 255, 255) + if err != nil { + return + } + } + + for y := 0; y < grid.Height(); y++ { + for x := 0; x < grid.Width(); x++ { + val := uint8(0) + if !grid.Get(x, y) { + val = 255 + } + // HACK - ansimage has an image offset bug which hides the first two rows + err = img.SetAt(y+3, x+1, val, val, val) + if err != nil { + return + } + } + } + s = img.Render() + return +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..f8f62b5 --- /dev/null +++ b/main.go @@ -0,0 +1,170 @@ +package main + +import ( + "encoding/base32" + "errors" + "fmt" + "io/ioutil" + "log" + "net/url" + "os" + "time" + + "gopkg.in/alecthomas/kingpin.v2" + + "github.com/qpliu/qrencode-go/qrencode" + + "git.icedream.tech/icedream/pixelqr/internal" +) + +const ( + Algorithm_SHA1 = "sha1" + Algorithm_SHA256 = "sha256" + Algorithm_SHA512 = "sha512" + Algorithm_Default = Algorithm_SHA1 + + ECLevel_L = "l" + ECLevel_M = "m" + ECLevel_Q = "q" + ECLevel_H = "h" + + Type_TOTP = "totp" + Type_HOTP = "hotp" + + Digits_Default = 6 + + Period_Default = 30 * time.Second +) + +var ( + ecLevelMapping = map[string]qrencode.ECLevel{ + ECLevel_L: qrencode.ECLevelL, + ECLevel_M: qrencode.ECLevelM, + ECLevel_Q: qrencode.ECLevelQ, + ECLevel_H: qrencode.ECLevelH, + } +) + +var ( + app = kingpin.New("pixelqr", "Generates QR code from any input and displays it in your ANSI true-color compatible terminal.") + + flagECLevel = app.Flag("ec", "Sets the error correction level for the generated code."). + Default(ECLevel_L). + Enum(ECLevel_L, ECLevel_M, ECLevel_Q, ECLevel_H) + + cmdText = app.Command("text", "Generate QR code from plain text file or stdin.") + cmdTextArgInput = cmdText.Arg("input", "The input file to encode."). + ExistingFile() + + cmdOtp = app.Command("otp", "Generate QR code for ") + cmdOtpFlagType = cmdOtp.Flag("type", "The type of the key."). + Default(Type_TOTP). + Enum(Type_TOTP, Type_HOTP) + cmdOtpFlagLabel = cmdOtp.Flag("label", "The name for the OTP key."). + String() + cmdOtpFlagIssuer = cmdOtp.Flag("issuer", "Indicates the provider or service this account is associated with."). + Default("pixelqr").String() + cmdOtpFlagAlgorithm = cmdOtp.Flag("algorithm", "Determines the key generation algorithm. Not supported by all authenticator apps."). + Default(fmt.Sprintf("%s", Algorithm_Default)). + Enum(Algorithm_SHA1, Algorithm_SHA256, Algorithm_SHA512) + cmdOtpFlagDigits = cmdOtp.Flag("digits", "Determines how long of a one-time passcode to display to the user."). + Default(fmt.Sprintf("%d", Digits_Default)). + Uint8() + cmdOtpFlagCounter = cmdOtp.Flag("counter", "Required if type is hotp, sets the initial counter value for code generation."). + Int() + cmdOtpFlagPeriod = cmdOtp.Flag("period", "Only if type is totp, defines a period that a TOTP code will be valid for. Not supported by all authenticator apps."). + Default(Period_Default.String()). + Duration() + cmdOtpFlagBase32Encoded = cmdOtp.Flag("base32encoded", "Set this to true if the secret master key you pass in has already been base32-encoded."). + Bool() + cmdOtpArgSecret = cmdOtp.Arg("secret", "The secret master key."). + Required().String() +) + +var ( + ErrCommandNotFound = errors.New("command not found") +) + +func main() { + var err error + defer func() { + if err == nil { + return + } + log.Fatal(err) + }() + + commandName, err := (app.Parse(os.Args[1:])) + if err != nil { + return + } + + switch commandName { + case cmdText.FullCommand(): // plain text -> qr + var inputFile *os.File + if cmdTextArgInput == nil || *cmdTextArgInput == "-" || *cmdTextArgInput == "" { + inputFile = os.Stdin + } else { + inputFile, err = os.Open(*cmdTextArgInput) + } + if err != nil { + return + } + defer inputFile.Close() + input, err := ioutil.ReadAll(inputFile) + if err != nil { + return + } + generateCliQR(string(input)) + + case cmdOtp.FullCommand(): // otp -> qr + // https://github.com/google/google-authenticator/wiki/Key-Uri-Format + // otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example + query := url.Values{} + query.Set("issuer", *cmdOtpFlagIssuer) + if !*cmdOtpFlagBase32Encoded { + // base32-encode secret before generating url + query.Set("secret", base32.StdEncoding.EncodeToString([]byte(*cmdOtpArgSecret))) + } else { + // secret already base32-encoded + query.Set("secret", *cmdOtpArgSecret) + } + if cmdOtpFlagAlgorithm != nil && *cmdOtpFlagAlgorithm != Algorithm_Default { + query.Set("algorithm", *cmdOtpFlagAlgorithm) + } + if cmdOtpFlagDigits != nil && *cmdOtpFlagDigits != Digits_Default { + query.Set("digits", fmt.Sprintf("%d", *cmdOtpFlagDigits)) + } + /*if cmdOtpFlagLabel != nil && len(*cmdOtpFlagLabel) > 0 { + query.Set("label"] = *cmdOtpFlagLabel + }*/ + if cmdOtpFlagPeriod != nil && *cmdOtpFlagPeriod != Period_Default { + periodSeconds := int(*cmdOtpFlagPeriod / time.Second) + query.Set("period", fmt.Sprintf("%d", periodSeconds)) + } + url := &url.URL{ + Scheme: "otpauth", + Host: *cmdOtpFlagType, + Path: *cmdOtpFlagLabel, + RawQuery: query.Encode(), + } + log.Println("Generated URL:", url.String()) + generateCliQR(url.String()) + + default: // other + err = ErrCommandNotFound + } +} + +func generateCliQR(input string) { + grid, err := qrencode.Encode(input, ecLevelMapping[*flagECLevel]) + if err != nil { + return + } + + s, err := internal.ConvertGridToANSImage(grid) + + fmt.Println(s) + + return +}