Browse Source

Two factor authentication support (#630)

* Initial commit for 2FA support

Signed-off-by: Andrew <write@imaginarycode.com>

* Add vendored files

* Add missing depends

* A few clean ups

* Added improvements, proper encryption

* Better encryption key

* Simplify "key" generation

* Make 2FA enrollment page more robust

* Fix typo

* Rename twofa/2FA to TwoFactor

* UNIQUE INDEX -> UNIQUE
release/v1.1
Andrew 6 years ago
committed by Lunny Xiao
parent
commit
6dd096b7f0
  1. 13
      cmd/web.go
  2. 19
      models/error.go
  3. 1
      models/models.go
  4. 141
      models/twofactor.go
  5. 20
      modules/auth/user_form.go
  6. 27
      options/locale/locale_en-US.ini
  7. 177
      routers/user/auth.go
  8. 194
      routers/user/setting.go
  9. 27
      templates/user/auth/twofa.tmpl
  10. 26
      templates/user/auth/twofa_scratch.tmpl
  11. 3
      templates/user/settings/navbar.tmpl
  12. 48
      templates/user/settings/twofa.tmpl
  13. 33
      templates/user/settings/twofa_enroll.tmpl
  14. 21
      vendor/github.com/boombuler/barcode/LICENSE
  15. 18
      vendor/github.com/boombuler/barcode/README.md
  16. 21
      vendor/github.com/boombuler/barcode/barcode.go
  17. 66
      vendor/github.com/boombuler/barcode/qr/alphanumeric.go
  18. 23
      vendor/github.com/boombuler/barcode/qr/automatic.go
  19. 59
      vendor/github.com/boombuler/barcode/qr/blocks.go
  20. 416
      vendor/github.com/boombuler/barcode/qr/encoder.go
  21. 29
      vendor/github.com/boombuler/barcode/qr/errorcorrection.go
  22. 56
      vendor/github.com/boombuler/barcode/qr/numeric.go
  23. 170
      vendor/github.com/boombuler/barcode/qr/qrcode.go
  24. 27
      vendor/github.com/boombuler/barcode/qr/unicode.go
  25. 310
      vendor/github.com/boombuler/barcode/qr/versioninfo.go
  26. 115
      vendor/github.com/boombuler/barcode/scaledbarcode.go
  27. 48
      vendor/github.com/boombuler/barcode/utils/base1dcode.go
  28. 119
      vendor/github.com/boombuler/barcode/utils/bitlist.go
  29. 65
      vendor/github.com/boombuler/barcode/utils/galoisfield.go
  30. 103
      vendor/github.com/boombuler/barcode/utils/gfpoly.go
  31. 44
      vendor/github.com/boombuler/barcode/utils/reedsolomon.go
  32. 19
      vendor/github.com/boombuler/barcode/utils/runeint.go
  33. 202
      vendor/github.com/pquerna/otp/LICENSE
  34. 5
      vendor/github.com/pquerna/otp/NOTICE
  35. 60
      vendor/github.com/pquerna/otp/README.md
  36. 70
      vendor/github.com/pquerna/otp/doc.go
  37. 181
      vendor/github.com/pquerna/otp/hotp/hotp.go
  38. 200
      vendor/github.com/pquerna/otp/otp.go
  39. 191
      vendor/github.com/pquerna/otp/totp/totp.go
  40. 36
      vendor/vendor.json

13
cmd/web.go

@ -203,6 +203,12 @@ func runWeb(ctx *cli.Context) error {
m.Post("/sign_up", bindIgnErr(auth.RegisterForm{}), user.SignUpPost)
m.Get("/reset_password", user.ResetPasswd)
m.Post("/reset_password", user.ResetPasswdPost)
m.Group("/two_factor", func() {
m.Get("", user.TwoFactor)
m.Post("", bindIgnErr(auth.TwoFactorAuthForm{}), user.TwoFactorPost)
m.Get("/scratch", user.TwoFactorScratch)
m.Post("/scratch", bindIgnErr(auth.TwoFactorScratchAuthForm{}), user.TwoFactorScratchPost)
})
}, reqSignOut)
m.Group("/user/settings", func() {
@ -223,6 +229,13 @@ func runWeb(ctx *cli.Context) error {
Post(bindIgnErr(auth.NewAccessTokenForm{}), user.SettingsApplicationsPost)
m.Post("/applications/delete", user.SettingsDeleteApplication)
m.Route("/delete", "GET,POST", user.SettingsDelete)
m.Group("/two_factor", func() {
m.Get("", user.SettingsTwoFactor)
m.Post("/regenerate_scratch", user.SettingsTwoFactorRegenerateScratch)
m.Post("/disable", user.SettingsTwoFactorDisable)
m.Get("/enroll", user.SettingsTwoFactorEnroll)
m.Post("/enroll", bindIgnErr(auth.TwoFactorAuthForm{}), user.SettingsTwoFactorEnrollPost)
})
}, reqSignIn, func(ctx *context.Context) {
ctx.Data["PageIsUserSettings"] = true
})

19
models/error.go

@ -787,6 +787,25 @@ func (err ErrTeamAlreadyExist) Error() string {
return fmt.Sprintf("team already exists [org_id: %d, name: %s]", err.OrgID, err.Name)
}
//
// Two-factor authentication
//
// ErrTwoFactorNotEnrolled indicates that a user is not enrolled in two-factor authentication.
type ErrTwoFactorNotEnrolled struct {
UID int64
}
// IsErrTwoFactorNotEnrolled checks if an error is a ErrTwoFactorNotEnrolled.
func IsErrTwoFactorNotEnrolled(err error) bool {
_, ok := err.(ErrTwoFactorNotEnrolled)
return ok
}
func (err ErrTwoFactorNotEnrolled) Error() string {
return fmt.Sprintf("user not enrolled in 2FA [uid: %d]", err.UID)
}
// ____ ___ .__ .___
// | | \______ | | _________ __| _/
// | | /\____ \| | / _ \__ \ / __ |

1
models/models.go

@ -105,6 +105,7 @@ func init() {
new(Notification),
new(IssueUser),
new(LFSMetaObject),
new(TwoFactor),
)
gonicNames := []string{"SSL", "UID"}

141
models/twofactor.go

@ -0,0 +1,141 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package models
import (
"crypto/md5"
"crypto/subtle"
"encoding/base64"
"time"
"github.com/Unknwon/com"
"github.com/go-xorm/xorm"
"github.com/pquerna/otp/totp"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/setting"
)
// TwoFactor represents a two-factor authentication token.
type TwoFactor struct {
ID int64 `xorm:"pk autoincr"`
UID int64 `xorm:"UNIQUE"`
Secret string
ScratchToken string
Created time.Time `xorm:"-"`
CreatedUnix int64 `xorm:"INDEX"`
Updated time.Time `xorm:"-"` // Note: Updated must below Created for AfterSet.
UpdatedUnix int64 `xorm:"INDEX"`
}
// BeforeInsert will be invoked by XORM before inserting a record representing this object.
func (t *TwoFactor) BeforeInsert() {
t.CreatedUnix = time.Now().Unix()
}
// BeforeUpdate is invoked from XORM before updating this object.
func (t *TwoFactor) BeforeUpdate() {
t.UpdatedUnix = time.Now().Unix()
}
// AfterSet is invoked from XORM after setting the value of a field of this object.
func (t *TwoFactor) AfterSet(colName string, _ xorm.Cell) {
switch colName {
case "created_unix":
t.Created = time.Unix(t.CreatedUnix, 0).Local()
case "updated_unix":
t.Updated = time.Unix(t.UpdatedUnix, 0).Local()
}
}
// GenerateScratchToken recreates the scratch token the user is using.
func (t *TwoFactor) GenerateScratchToken() error {
token, err := base.GetRandomString(8)
if err != nil {
return err
}
t.ScratchToken = token
return nil
}
// VerifyScratchToken verifies if the specified scratch token is valid.
func (t *TwoFactor) VerifyScratchToken(token string) bool {
if len(token) == 0 {
return false
}
return subtle.ConstantTimeCompare([]byte(token), []byte(t.ScratchToken)) == 1
}
func (t *TwoFactor) getEncryptionKey() []byte {
k := md5.Sum([]byte(setting.SecretKey))
return k[:]
}
// SetSecret sets the 2FA secret.
func (t *TwoFactor) SetSecret(secret string) error {
secretBytes, err := com.AESEncrypt(t.getEncryptionKey(), []byte(secret))
if err != nil {
return err
}
t.Secret = base64.StdEncoding.EncodeToString(secretBytes)
return nil
}
// ValidateTOTP validates the provided passcode.
func (t *TwoFactor) ValidateTOTP(passcode string) (bool, error) {
decodedStoredSecret, err := base64.StdEncoding.DecodeString(t.Secret)
if err != nil {
return false, err
}
secret, err := com.AESDecrypt(t.getEncryptionKey(), decodedStoredSecret)
if err != nil {
return false, err
}
secretStr := string(secret)
return totp.Validate(passcode, secretStr), nil
}
// NewTwoFactor creates a new two-factor authentication token.
func NewTwoFactor(t *TwoFactor) error {
err := t.GenerateScratchToken()
if err != nil {
return err
}
_, err = x.Insert(t)
return err
}
// UpdateTwoFactor updates a two-factor authentication token.
func UpdateTwoFactor(t *TwoFactor) error {
_, err := x.Id(t.ID).AllCols().Update(t)
return err
}
// GetTwoFactorByUID returns the two-factor authentication token associated with
// the user, if any.
func GetTwoFactorByUID(uid int64) (*TwoFactor, error) {
twofa := &TwoFactor{UID: uid}
has, err := x.Get(twofa)
if err != nil {
return nil, err
} else if !has {
return nil, ErrTwoFactorNotEnrolled{uid}
}
return twofa, nil
}
// DeleteTwoFactorByID deletes two-factor authentication token by given ID.
func DeleteTwoFactorByID(id, userID int64) error {
cnt, err := x.Id(id).Delete(&TwoFactor{
UID: userID,
})
if err != nil {
return err
} else if cnt != 1 {
return ErrTwoFactorNotEnrolled{userID}
}
return nil
}

20
modules/auth/user_form.go

@ -173,3 +173,23 @@ type NewAccessTokenForm struct {
func (f *NewAccessTokenForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
return validate(errs, ctx.Data, f, ctx.Locale)
}
// TwoFactorAuthForm for logging in with 2FA token.
type TwoFactorAuthForm struct {
Passcode string `binding:"Required"`
}
// Validate valideates the fields
func (f *TwoFactorAuthForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
return validate(errs, ctx.Data, f, ctx.Locale)
}
// TwoFactorScratchAuthForm for logging in with 2FA scratch token.
type TwoFactorScratchAuthForm struct {
Token string `binding:"Required"`
}
// Validate valideates the fields
func (f *TwoFactorScratchAuthForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
return validate(errs, ctx.Data, f, ctx.Locale)
}

27
options/locale/locale_en-US.ini

@ -23,6 +23,9 @@ email = Email
password = Password
re_type = Re-Type
captcha = Captcha
twofa = Two-factor authentication
twofa_scratch = Two-factor scratch code
passcode = Passcode
repository = Repository
organization = Organization
@ -175,6 +178,12 @@ invalid_code = Sorry, your confirmation code has expired or not valid.
reset_password_helper = Click here to reset your password
password_too_short = Password length cannot be less then %d.
non_local_account = Non-local accounts cannot change passwords through Gitea.
verify = Verify
scratch_code = Scratch code
use_scratch_code = Use a scratch code
twofa_scratch_used = You have used your scratch code. You have been redirected to the two-factor settings page so you may remove your device enrollment or generate a new scratch code.
twofa_passcode_incorrect = Your passcode is not correct. If you misplaced your device, use your scratch code.
twofa_scratch_token_incorrect = Your scratch code is not correct.
[mail]
activate_account = Please activate your account
@ -266,6 +275,7 @@ social = Social Accounts
applications = Applications
orgs = Organizations
delete = Delete Account
twofa = Two-Factor Authentication
uid = Uid
public_profile = Public Profile
@ -351,6 +361,23 @@ access_token_deletion = Personal Access Token Deletion
access_token_deletion_desc = Delete this personal access token will remove all related accesses of application. Do you want to continue?
delete_token_success = Personal access token has been removed successfully! Don't forget to update your application as well.
twofa_desc = Gitea supports two-factor authentication to provide additional security for your account.
twofa_is_enrolled = Your account is <strong>enrolled</strong> into two-factor authentication.
twofa_not_enrolled = Your account is not currently enrolled into two-factor authentication.
twofa_disable = Disable two-factor authentication
twofa_scratch_token_regenerate = Regenerate scratch token
twofa_scratch_token_regenerated = Your scratch token has been regenerated. It is now %s. Keep it in a safe place.
twofa_enroll = Enroll into two-factor authentication
twofa_disable_note = If needed, you can disable two-factor authentication.
twofa_disable_desc = Disabling two-factor authentication will make your account less secure. Are you sure you want to proceed?
regenerate_scratch_token_desc = If you misplaced your scratch token, or had to use it to log in, you can reset it.
twofa_disabled = Two-factor authentication has been disabled.
scan_this_image = Scan this image with your authentication application:
or_enter_secret = Or enter the secret: %s
then_enter_passcode = Then enter the passcode the application gives you:
passcode_invalid = That passcode is invalid. Try again.
twofa_enrolled = Your account has now been enrolled in two-factor authentication. Make sure to save your scratch token (%s), as it will only be shown once!
delete_account = Delete Your Account
delete_prompt = The operation will delete your account permanently, and <strong>CANNOT</strong> be undone!
confirm_delete_account = Confirm Deletion

177
routers/user/auth.go

@ -5,6 +5,7 @@
package user
import (
"errors"
"fmt"
"net/url"
@ -27,6 +28,8 @@ const (
TplActivate base.TplName = "user/auth/activate"
tplForgotPassword base.TplName = "user/auth/forgot_passwd"
tplResetPassword base.TplName = "user/auth/reset_passwd"
tplTwofa base.TplName = "user/auth/twofa"
tplTwofaScratch base.TplName = "user/auth/twofa_scratch"
)
// AutoSignIn reads cookie and try to auto-login.
@ -69,15 +72,12 @@ func AutoSignIn(ctx *context.Context) (bool, error) {
return true, nil
}
// SignIn render sign in page
func SignIn(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("sign_in")
func checkAutoLogin(ctx *context.Context) bool {
// Check auto-login.
isSucceed, err := AutoSignIn(ctx)
if err != nil {
ctx.Handle(500, "AutoSignIn", err)
return
return true
}
redirectTo := ctx.Query("redirect_to")
@ -94,6 +94,18 @@ func SignIn(ctx *context.Context) {
} else {
ctx.Redirect(setting.AppSubURL + "/")
}
return true
}
return false
}
// SignIn render sign in page
func SignIn(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("sign_in")
// Check auto-login.
if checkAutoLogin(ctx) {
return
}
@ -119,13 +131,158 @@ func SignInPost(ctx *context.Context, form auth.SignInForm) {
return
}
if form.Remember {
// If this user is enrolled in 2FA, we can't sign the user in just yet.
// Instead, redirect them to the 2FA authentication page.
_, err = models.GetTwoFactorByUID(u.ID)
if err != nil {
if models.IsErrTwoFactorNotEnrolled(err) {
handleSignIn(ctx, u, form.Remember)
} else {
ctx.Handle(500, "UserSignIn", err)
}
return
}
// User needs to use 2FA, save data and redirect to 2FA page.
ctx.Session.Set("twofaUid", u.ID)
ctx.Session.Set("twofaRemember", form.Remember)
ctx.Redirect(setting.AppSubURL + "/user/two_factor")
}
// TwoFactor shows the user a two-factor authentication page.
func TwoFactor(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("twofa")
// Check auto-login.
if checkAutoLogin(ctx) {
return
}
// Ensure user is in a 2FA session.
if ctx.Session.Get("twofaUid") == nil {
ctx.Handle(500, "UserSignIn", errors.New("not in 2FA session"))
return
}
ctx.HTML(200, tplTwofa)
}
// TwoFactorPost validates a user's two-factor authentication token.
func TwoFactorPost(ctx *context.Context, form auth.TwoFactorAuthForm) {
ctx.Data["Title"] = ctx.Tr("twofa")
// Ensure user is in a 2FA session.
idSess := ctx.Session.Get("twofaUid")
if idSess == nil {
ctx.Handle(500, "UserSignIn", errors.New("not in 2FA session"))
return
}
id := idSess.(int64)
twofa, err := models.GetTwoFactorByUID(id)
if err != nil {
ctx.Handle(500, "UserSignIn", err)
return
}
// Validate the passcode with the stored TOTP secret.
ok, err := twofa.ValidateTOTP(form.Passcode)
if err != nil {
ctx.Handle(500, "UserSignIn", err)
return
}
if ok {
remember := ctx.Session.Get("twofaRemember").(bool)
u, err := models.GetUserByID(id)
if err != nil {
ctx.Handle(500, "UserSignIn", err)
return
}
handleSignIn(ctx, u, remember)
return
}
ctx.RenderWithErr(ctx.Tr("auth.twofa_passcode_incorrect"), tplTwofa, auth.TwoFactorAuthForm{})
}
// TwoFactorScratch shows the scratch code form for two-factor authentication.
func TwoFactorScratch(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("twofa_scratch")
// Check auto-login.
if checkAutoLogin(ctx) {
return
}
// Ensure user is in a 2FA session.
if ctx.Session.Get("twofaUid") == nil {
ctx.Handle(500, "UserSignIn", errors.New("not in 2FA session"))
return
}
ctx.HTML(200, tplTwofaScratch)
}
// TwoFactorScratchPost validates and invalidates a user's two-factor scratch token.
func TwoFactorScratchPost(ctx *context.Context, form auth.TwoFactorScratchAuthForm) {
ctx.Data["Title"] = ctx.Tr("twofa_scratch")
// Ensure user is in a 2FA session.
idSess := ctx.Session.Get("twofaUid")
if idSess == nil {
ctx.Handle(500, "UserSignIn", errors.New("not in 2FA session"))
return
}
id := idSess.(int64)
twofa, err := models.GetTwoFactorByUID(id)
if err != nil {
ctx.Handle(500, "UserSignIn", err)
return
}
// Validate the passcode with the stored TOTP secret.
if twofa.VerifyScratchToken(form.Token) {
// Invalidate the scratch token.
twofa.ScratchToken = ""
if err = models.UpdateTwoFactor(twofa); err != nil {
ctx.Handle(500, "UserSignIn", err)
return
}
remember := ctx.Session.Get("twofaRemember").(bool)
u, err := models.GetUserByID(id)
if err != nil {
ctx.Handle(500, "UserSignIn", err)
return
}
handleSignInFull(ctx, u, remember, false)
ctx.Flash.Info(ctx.Tr("auth.twofa_scratch_used"))
ctx.Redirect(setting.AppSubURL + "/user/settings/two_factor")
return
}
ctx.RenderWithErr(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplTwofaScratch, auth.TwoFactorScratchAuthForm{})
}
// This handles the final part of the sign-in process of the user.
func handleSignIn(ctx *context.Context, u *models.User, remember bool) {
handleSignInFull(ctx, u, remember, true)
}
func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyRedirect bool) {
if remember {
days := 86400 * setting.LogInRememberDays
ctx.SetCookie(setting.CookieUserName, u.Name, days, setting.AppSubURL)
ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd),
setting.CookieRememberName, u.Name, days, setting.AppSubURL)
}
ctx.Session.Delete("twofaUid")
ctx.Session.Delete("twofaRemember")
ctx.Session.Set("uid", u.ID)
ctx.Session.Set("uname", u.Name)
@ -141,11 +298,15 @@ func SignInPost(ctx *context.Context, form auth.SignInForm) {
if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 {
ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL)
ctx.Redirect(redirectTo)
if obeyRedirect {
ctx.Redirect(redirectTo)
}
return
}
ctx.Redirect(setting.AppSubURL + "/")
if obeyRedirect {
ctx.Redirect(setting.AppSubURL + "/")
}
}
// SignOut sign out from login status

194
routers/user/setting.go

@ -5,12 +5,19 @@
package user
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"strings"
"github.com/Unknwon/com"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"encoding/base64"
"html/template"
"image/png"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth"
@ -28,6 +35,8 @@ const (
tplSettingsSSHKeys base.TplName = "user/settings/sshkeys"
tplSettingsSocial base.TplName = "user/settings/social"
tplSettingsApplications base.TplName = "user/settings/applications"
tplSettingsTwofa base.TplName = "user/settings/twofa"
tplSettingsTwofaEnroll base.TplName = "user/settings/twofa_enroll"
tplSettingsDelete base.TplName = "user/settings/delete"
tplSecurity base.TplName = "user/security"
)
@ -437,6 +446,191 @@ func SettingsDeleteApplication(ctx *context.Context) {
})
}
// SettingsTwoFactor renders the 2FA page.
func SettingsTwoFactor(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsTwofa"] = true
enrolled := true
_, err := models.GetTwoFactorByUID(ctx.User.ID)
if err != nil {
if models.IsErrTwoFactorNotEnrolled(err) {
enrolled = false
} else {
ctx.Handle(500, "SettingsTwoFactor", err)
return
}
}
ctx.Data["TwofaEnrolled"] = enrolled
ctx.HTML(200, tplSettingsTwofa)
}
// SettingsTwoFactorRegenerateScratch regenerates the user's 2FA scratch code.
func SettingsTwoFactorRegenerateScratch(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsTwofa"] = true
t, err := models.GetTwoFactorByUID(ctx.User.ID)
if err != nil {
ctx.Handle(500, "SettingsTwoFactor", err)
return
}
if err = t.GenerateScratchToken(); err != nil {
ctx.Handle(500, "SettingsTwoFactor", err)
return
}
if err = models.UpdateTwoFactor(t); err != nil {
ctx.Handle(500, "SettingsTwoFactor", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.twofa_scratch_token_regenerated", t.ScratchToken))
ctx.Redirect(setting.AppSubURL + "/user/settings/two_factor")
}
// SettingsTwoFactorDisable deletes the user's 2FA settings.
func SettingsTwoFactorDisable(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsTwofa"] = true
t, err := models.GetTwoFactorByUID(ctx.User.ID)
if err != nil {
ctx.Handle(500, "SettingsTwoFactor", err)
return
}
if err = models.DeleteTwoFactorByID(t.ID, ctx.User.ID); err != nil {
ctx.Handle(500, "SettingsTwoFactor", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.twofa_disabled"))
ctx.Redirect(setting.AppSubURL + "/user/settings/two_factor")
}
func twofaGenerateSecretAndQr(ctx *context.Context) bool {
var otpKey *otp.Key
var err error
uri := ctx.Session.Get("twofaUri")
if uri != nil {
otpKey, err = otp.NewKeyFromURL(uri.(string))
}
if otpKey == nil {
err = nil // clear the error, in case the URL was invalid
otpKey, err = totp.Generate(totp.GenerateOpts{
Issuer: setting.AppName,
AccountName: ctx.User.Name,
})
if err != nil {
ctx.Handle(500, "SettingsTwoFactor", err)
return false
}
}
ctx.Data["TwofaSecret"] = otpKey.Secret()
img, err := otpKey.Image(320, 240)
if err != nil {
ctx.Handle(500, "SettingsTwoFactor", err)
return false
}
var imgBytes bytes.Buffer
if err = png.Encode(&imgBytes, img); err != nil {
ctx.Handle(500, "SettingsTwoFactor", err)
return false
}
ctx.Data["QrUri"] = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(imgBytes.Bytes()))
ctx.Session.Set("twofaSecret", otpKey.Secret())
ctx.Session.Set("twofaUri", otpKey.String())
return true
}
// SettingsTwoFactorEnroll shows the page where the user can enroll into 2FA.
func SettingsTwoFactorEnroll(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsTwofa"] = true
t, err := models.GetTwoFactorByUID(ctx.User.ID)
if t != nil {
// already enrolled
ctx.Handle(500, "SettingsTwoFactor", err)
return
}
if err != nil && !models.IsErrTwoFactorNotEnrolled(err) {
ctx.Handle(500, "SettingsTwoFactor", err)
return
}
if !twofaGenerateSecretAndQr(ctx) {
return
}
ctx.HTML(200, tplSettingsTwofaEnroll)
}
// SettingsTwoFactorEnrollPost handles enrolling the user into 2FA.
func SettingsTwoFactorEnrollPost(ctx *context.Context, form auth.TwoFactorAuthForm) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsTwofa"] = true
t, err := models.GetTwoFactorByUID(ctx.User.ID)
if t != nil {
// already enrolled
ctx.Handle(500, "SettingsTwoFactor", err)
return
}
if err != nil && !models.IsErrTwoFactorNotEnrolled(err) {
ctx.Handle(500, "SettingsTwoFactor", err)
return
}
if ctx.HasError() {
if !twofaGenerateSecretAndQr(ctx) {
return
}
ctx.HTML(200, tplSettingsTwofaEnroll)
return
}
secret := ctx.Session.Get("twofaSecret").(string)
if !totp.Validate(form.Passcode, secret) {
if !twofaGenerateSecretAndQr(ctx) {
return
}
ctx.Flash.Error(ctx.Tr("settings.passcode_invalid"))
ctx.HTML(200, tplSettingsTwofaEnroll)
return
}
t = &models.TwoFactor{
UID: ctx.User.ID,
}
err = t.SetSecret(secret)
if err != nil {
ctx.Handle(500, "SettingsTwoFactor", err)
return
}
err = t.GenerateScratchToken()
if err != nil {
ctx.Handle(500, "SettingsTwoFactor", err)
return
}
if err = models.NewTwoFactor(t); err != nil {
ctx.Handle(500, "SettingsTwoFactor", err)
return
}
ctx.Session.Delete("twofaSecret")
ctx.Session.Delete("twofaUri")
ctx.Flash.Success(ctx.Tr("settings.twofa_enrolled", t.ScratchToken))
ctx.Redirect(setting.AppSubURL + "/user/settings/two_factor")
}
// SettingsDelete render user suicide page and response for delete user himself
func SettingsDelete(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")

27
templates/user/auth/twofa.tmpl

@ -0,0 +1,27 @@
{{template "base/head" .}}
<div class="user signin">
<div class="ui middle very relaxed page grid">
<div class="column">
<form class="ui form" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<h3 class="ui top attached header">
{{.i18n.Tr "twofa"}}
</h3>
<div class="ui attached segment">
{{template "base/alert" .}}
<div class="required inline field">
<label for="passcode">{{.i18n.Tr "passcode"}}</label>
<input id="passcode" name="passcode" type="text" autocomplete="off" required>
</div>
<div class="inline field">
<label></label>
<button class="ui green button">{{.i18n.Tr "auth.verify"}}</button>
<a href="{{AppSubUrl}}/user/two_factor/scratch">{{.i18n.Tr "auth.use_scratch_code" | Str2html}}</a>
</div>
</div>
</form>
</div>
</div>
</div>
{{template "base/footer" .}}

26
templates/user/auth/twofa_scratch.tmpl

@ -0,0 +1,26 @@
{{template "base/head" .}}
<div class="user signin">
<div class="ui middle very relaxed page grid">
<div class="column">
<form class="ui form" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<h3 class="ui top attached header">
{{.i18n.Tr "twofa_scratch"}}
</h3>
<div class="ui attached segment">
{{template "base/alert" .}}
<div class="required inline field">
<label for="token">{{.i18n.Tr "auth.scratch_code"}}</label>
<input id="token" name="token" type="text" autocomplete="off" required>
</div>
<div class="inline field">
<label></label>
<button class="ui green button">{{.i18n.Tr "auth.verify"}}</button>
</div>
</div>
</form>
</div>
</div>
</div>
{{template "base/footer" .}}

3
templates/user/settings/navbar.tmpl

@ -19,6 +19,9 @@
<a class="{{if .PageIsSettingsApplications}}active{{end}} item" href="{{AppSubUrl}}/user/settings/applications">
{{.i18n.Tr "settings.applications"}}
</a>
<a class="{{if .PageIsSettingsTwofa}}active{{end}} item" href="{{AppSubUrl}}/user/settings/two_factor">
{{.i18n.Tr "settings.twofa"}}
</a>
<a class="{{if .PageIsSettingsDelete}}active{{end}} item" href="{{AppSubUrl}}/user/settings/delete">
{{.i18n.Tr "settings.delete"}}
</a>

48
templates/user/settings/twofa.tmpl

@ -0,0 +1,48 @@
{{template "base/head" .}}
<div class="user settings delete">
<div class="ui container">
<div class="ui grid">
{{template "user/settings/navbar" .}}
<div class="twelve wide column content">
{{template "base/alert" .}}
<h4 class="ui top attached header">
{{.i18n.Tr "settings.twofa"}}
</h4>
<div class="ui attached segment">
<p>{{.i18n.Tr "settings.twofa_desc"}}</p>
{{if .TwofaEnrolled}}
<p>{{$.i18n.Tr "settings.twofa_is_enrolled" | Str2html }}</p>
<form class="ui form" action="{{.Link}}/regenerate_scratch" method="post" enctype="multipart/form-data">
{{.CsrfTokenHtml}}
<p>{{.i18n.Tr "settings.regenerate_scratch_token_desc"}}</p>
<button class="ui blue button">{{$.i18n.Tr "settings.twofa_scratch_token_regenerate"}}</button>
</form>
<form class="ui form" action="{{.Link}}/disable" method="post" enctype="multipart/form-data" id="disable-form">
{{.CsrfTokenHtml}}
<p>{{.i18n.Tr "settings.twofa_disable_note"}}</p>
<div class="ui red button delete-button" data-type="form" data-form="#disable-form">{{$.i18n.Tr "settings.twofa_disable"}}</div>
</form>
{{else}}
<p>{{.i18n.Tr "settings.twofa_not_enrolled"}}</p>
<div class="inline field">
<a class="ui green button" href="{{.Link}}/enroll">{{$.i18n.Tr "settings.twofa_enroll"}}</a>
</div>
{{end}}
</div>
</div>
</div>
</div>
</div>
<div class="ui small basic delete modal">
<div class="ui icon header">
<i class="trash icon"></i>
{{.i18n.Tr "settings.twofa_disable"}}
</div>
<div class="content">
<p>{{.i18n.Tr "settings.twofa_disable_desc"}}</p>
</div>
{{template "base/delete_modal_actions" .}}
</div>
{{template "base/footer" .}}

33
templates/user/settings/twofa_enroll.tmpl

@ -0,0 +1,33 @@
{{template "base/head" .}}
<div class="user settings delete">
<div class="ui container">
<div class="ui grid">
{{template "user/settings/navbar" .}}
<div class="twelve wide column content">
{{template "base/alert" .}}
<h4 class="ui top attached header">
{{.i18n.Tr "settings.twofa_enroll"}}
</h4>
<div class="ui attached segment">
<p>{{.i18n.Tr "settings.scan_this_image"}}</p>
<img src="{{.QrUri}}" alt="{{.TwofaSecret}}">
<p>{{.i18n.Tr "settings.or_enter_secret" .TwofaSecret}}
<p>{{.i18n.Tr "settings.then_enter_passcode"}}
<form class="ui form" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<div class="inline required field {{if .Err_Passcode}}error{{end}}">
<label for="passcode">{{.i18n.Tr "passcode"}}</label>
<input id="passcode" name="passcode" autofocus required>
</div>
<div class="inline field">
<label></label>
<button class="ui green button">{{.i18n.Tr "auth.verify"}}</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{{template "base/footer" .}}

21
vendor/github.com/boombuler/barcode/LICENSE

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 Florian Sundermann
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

18
vendor/github.com/boombuler/barcode/README.md

@ -0,0 +1,18 @@
##Introduction##
This is a package for GO which can be used to create different types of barcodes.
##Supported Barcode Types##
* Aztec Code
* Codabar
* Code 128
* Code 39
* EAN 8
* EAN 13
* Datamatrix
* QR Codes
* 2 of 5
##Documentation##
See [GoDoc](https://godoc.org/github.com/boombuler/barcode)
To create a barcode use the Encode function from one of the subpackages.

21
vendor/github.com/boombuler/barcode/barcode.go

@ -0,0 +1,21 @@
package barcode
import "image"
// Contains some meta information about a barcode
type Metadata struct {
// the name of the barcode kind
CodeKind string
// contains 1 for 1D barcodes or 2 for 2D barcodes
Dimensions byte
}
// a rendered and encoded barcode
type Barcode interface {
image.Image
// returns some meta information about the barcode
Metadata() Metadata
// the data that was encoded in this barcode
Content() string
CheckSum() int
}

66
vendor/github.com/boombuler/barcode/qr/alphanumeric.go

@ -0,0 +1,66 @@
package qr
import (
"errors"
"fmt"
"strings"
"github.com/boombuler/barcode/utils"
)
const charSet string = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"
func stringToAlphaIdx(content string) <-chan int {
result := make(chan int)
go func() {
for _, r := range content {
idx := strings.IndexRune(charSet, r)
result <- idx
if idx < 0 {
break
}
}
close(result)
}()
return result
}
func encodeAlphaNumeric(content string, ecl ErrorCorrectionLevel) (*utils.BitList, *versionInfo, error) {
contentLenIsOdd := len(content)%2 == 1
contentBitCount := (len(content) / 2) * 11
if contentLenIsOdd {
contentBitCount += 6
}
vi := findSmallestVersionInfo(ecl, alphaNumericMode, contentBitCount)
if vi == nil {
return nil, nil, errors.New("To much data to encode")
}
res := new(utils.BitList)
res.AddBits(int(alphaNumericMode), 4)
res.AddBits(len(content), vi.charCountBits(alphaNumericMode))
encoder := stringToAlphaIdx(content)
for idx := 0; idx < len(content)/2; idx++ {
c1 := <-encoder
c2 := <-encoder
if c1 < 0 || c2 < 0 {
return nil, nil, fmt.Errorf("\"%s\" can not be encoded as %s", content, AlphaNumeric)
}
res.AddBits(c1*45+c2, 11)
}
if contentLenIsOdd {
c := <-encoder
if c < 0 {
return nil, nil, fmt.Errorf("\"%s\" can not be encoded as %s", content, AlphaNumeric)
}
res.AddBits(c, 6)
}
addPaddingAndTerminator(res, vi)
return res, vi, nil
}

23
vendor/github.com/boombuler/barcode/qr/automatic.go

@ -0,0 +1,23 @@
package qr
import (
"fmt"
"github.com/boombuler/barcode/utils"
)
func encodeAuto(content string, ecl ErrorCorrectionLevel) (*utils.BitList, *versionInfo, error) {
bits, vi, _ := Numeric.getEncoder()(content, ecl)
if bits != nil && vi != nil {
return bits, vi, nil
}
bits, vi, _ = AlphaNumeric.getEncoder()(content, ecl)
if bits != nil && vi != nil {
return bits, vi, nil
}
bits, vi, _ = Unicode.getEncoder()(content, ecl)
if bits != nil && vi != nil {
return bits, vi, nil
}
return nil, nil, fmt.Errorf("No encoding found to encode \"%s\"", content)
}

59
vendor/github.com/boombuler/barcode/qr/blocks.go

@ -0,0 +1,59 @@
package qr
type block struct {
data []byte
ecc []byte
}
type blockList []*block
func splitToBlocks(data <-chan byte, vi *versionInfo) blockList {
result := make(blockList, vi.NumberOfBlocksInGroup1+vi.NumberOfBlocksInGroup2)
for b := 0; b < int(vi.NumberOfBlocksInGroup1); b++ {
blk := new(block)
blk.data = make([]byte, vi.DataCodeWordsPerBlockInGroup1)
for cw := 0; cw < int(vi.DataCodeWordsPerBlockInGroup1); cw++ {
blk.data[cw] = <-data
}
blk.ecc = ec.calcECC(blk.data, vi.ErrorCorrectionCodewordsPerBlock)
result[b] = blk
}
for b := 0; b < int(vi.NumberOfBlocksInGroup2); b++ {
blk := new(block)
blk.data = make([]byte, vi.DataCodeWordsPerBlockInGroup2)
for cw := 0; cw < int(vi.DataCodeWordsPerBlockInGroup2); cw++ {
blk.data[cw] = <-data
}
blk.ecc = ec.calcECC(blk.data, vi.ErrorCorrectionCodewordsPerBlock)
result[int(vi.NumberOfBlocksInGroup1)+b] = blk
}
return result
}
func (bl blockList) interleave(vi *versionInfo) []byte {
var maxCodewordCount int
if vi.DataCodeWordsPerBlockInGroup1 > vi.DataCodeWordsPerBlockInGroup2 {
maxCodewordCount = int(vi.DataCodeWordsPerBlockInGroup1)
} else {
maxCodewordCount = int(vi.DataCodeWordsPerBlockInGroup2)
}
resultLen := (vi.DataCodeWordsPerBlockInGroup1+vi.ErrorCorrectionCodewordsPerBlock)*vi.NumberOfBlocksInGroup1 +
(vi.DataCodeWordsPerBlockInGroup2+vi.ErrorCorrectionCodewordsPerBlock)*vi.NumberOfBlocksInGroup2
result := make([]byte, 0, resultLen)
for i := 0; i < maxCodewordCount; i++ {
for b := 0; b < len(bl); b++ {
if len(bl[b].data) > i {
result = append(result, bl[b].data[i])
}
}
}
for i := 0; i < int(vi.ErrorCorrectionCodewordsPerBlock); i++ {
for b := 0; b < len(bl); b++ {
result = append(result, bl[b].ecc[i])
}
}
return result
}

416
vendor/github.com/boombuler/barcode/qr/encoder.go

@ -0,0 +1,416 @@
// Package qr can be used to create QR barcodes.
package qr
import (
"image"
"github.com/boombuler/barcode"
"github.com/boombuler/barcode/utils"
)
type encodeFn func(content string, eccLevel ErrorCorrectionLevel) (*utils.BitList, *versionInfo, error)
// Encoding mode for QR Codes.
type Encoding byte
const (
// Auto will choose ths best matching encoding
Auto Encoding = iota
// Numeric encoding only encodes numbers [0-9]
Numeric
// AlphaNumeric encoding only encodes uppercase letters, numbers and [Space], $, %, *, +, -, ., /, :
AlphaNumeric
// Unicode encoding encodes the string as utf-8
Unicode
// only for testing purpose
unknownEncoding
)
func (e Encoding) getEncoder() encodeFn {
switch e {
case Auto:
return encodeAuto
case Numeric:
return encodeNumeric
case AlphaNumeric:
return encodeAlphaNumeric
case Unicode:
return encodeUnicode
}
return nil
}
func (e Encoding) String() string {
switch e {
case Auto:
return "Auto"
case Numeric:
return "Numeric"
case AlphaNumeric:
return "AlphaNumeric"
case Unicode:
return "Unicode"
}
return ""
}
// Encode returns a QR barcode with the given content, error correction level and uses the given encoding
func Encode(content string, level ErrorCorrectionLevel, mode Encoding) (barcode.Barcode, error) {
bits, vi, err := mode.getEncoder()(content, level)
if err != nil {
return nil, err
}
blocks := splitToBlocks(bits.IterateBytes(), vi)
data := blocks.interleave(vi)
result := render(data, vi)
result.content = content
return result, nil
}
func render(data []byte, vi *versionInfo) *qrcode {
dim := vi.modulWidth()
results := make([]*qrcode, 8)
for i := 0; i < 8; i++ {
results[i] = newBarcode(dim)
}
occupied := newBarcode(dim)
setAll := func(x int, y int, val bool) {
occupied.Set(x, y, true)
for i := 0; i < 8; i++ {
results[i].Set(x, y, val)
}
}
drawFinderPatterns(vi, setAll)
drawAlignmentPatterns(occupied, vi, setAll)
//Timing Pattern:
var i int
for i = 0; i < dim; i++ {
if !occupied.Get(i, 6) {
setAll(i, 6, i%2 == 0)
}
if !occupied.Get(6, i) {
setAll(6, i, i%2 == 0)
}
}
// Dark Module
setAll(8, dim-8, true)
drawVersionInfo(vi, setAll)
drawFormatInfo(vi, -1, occupied.Set)
for i := 0; i < 8; i++ {
drawFormatInfo(vi, i, results[i].Set)
}
// Write the data
var curBitNo int
for pos := range iterateModules(occupied) {
var curBit bool
if curBitNo < len(data)*8 {
curBit = ((data[curBitNo/8] >> uint(7-(curBitNo%8))) & 1) == 1
} else {
curBit = false
}
for i := 0; i < 8; i++ {
setMasked(pos.X, pos.Y, curBit, i, results[i].Set)
}
curBitNo++
}
lowestPenalty := ^uint(0)
lowestPenaltyIdx := -1
for i := 0; i < 8; i++ {
p := results[i].calcPenalty()
if p < lowestPenalty {
lowestPenalty = p
lowestPenaltyIdx = i
}
}
return results[lowestPenaltyIdx]
}
func setMasked(x, y int, val bool, mask int, set func(int, int, bool)) {
switch mask {
case 0:
val = val != (((y + x) % 2) == 0)
break
case 1:
val = val != ((y % 2) == 0)
break
case 2:
val = val != ((x % 3) == 0)
break
case 3:
val = val != (((y + x) % 3) == 0)
break
case 4:
val = val != (((y/2 + x/3) % 2) == 0)
break
case 5:
val = val != (((y*x)%2)+((y*x)%3) == 0)
break
case 6:
val = val != ((((y*x)%2)+((y*x)%3))%2 == 0)
break
case 7:
val = val != ((((y+x)%2)+((y*x)%3))%2 == 0)
}
set(x, y, val)
}
func iterateModules(occupied *qrcode) <-chan image.Point {
result := make(chan image.Point)
allPoints := make(chan image.Point)
go func() {
curX := occupied.dimension - 1
curY := occupied.dimension - 1
isUpward := true
for true {
if isUpward {
allPoints <- image.Pt(curX, curY)
allPoints <- image.Pt(curX-1, curY)
curY--
if curY < 0 {
curY = 0
curX -= 2
if curX == 6 {
curX--
}
if curX < 0 {
break
}
isUpward = false
}
} else {
allPoints <- image.Pt(curX, curY)
allPoints <- image.Pt(curX-1, curY)
curY++
if curY >= occupied.dimension {
curY = occupied.dimension - 1
curX -= 2
if curX == 6 {
curX--
}
isUpward = true
if curX < 0 {
break
}
}
}
}
close(allPoints)
}()
go func() {
for pt := range allPoints {
if !occupied.Get(pt.X, pt.Y) {
result <- pt
}
}
close(result)
}()
return result
}
func drawFinderPatterns(vi *versionInfo, set func(int, int, bool)) {
dim := vi.modulWidth()
drawPattern := func(xoff int, yoff int) {
for x := -1; x < 8; x++ {
for y := -1; y < 8; y++ {
val := (x == 0 || x == 6 || y == 0 || y == 6 || (x > 1 && x < 5 && y > 1 && y < 5)) && (x <= 6 && y <= 6 && x >= 0 && y >= 0)
if x+xoff >= 0 && x+xoff < dim && y+yoff >= 0 && y+yoff < dim {
set(x+xoff, y+yoff, val)
}
}
}
}
drawPattern(0, 0)
drawPattern(0, dim-7)
drawPattern(dim-7, 0)
}
func drawAlignmentPatterns(occupied *qrcode, vi *versionInfo, set func(int, int, bool)) {
drawPattern := func(xoff int, yoff int) {
for x := -2; x <= 2; x++ {
for y := -2; y <= 2; y++ {
val := x == -2 || x == 2 || y == -2 || y == 2 || (x == 0 && y == 0)
set(x+xoff, y+yoff, val)
}
}
}
positions := vi.alignmentPatternPlacements()
for _, x := range positions {
for _, y := range positions {
if occupied.Get(x, y) {
continue
}
drawPattern(x, y)
}
}
}
var formatInfos = map[ErrorCorrectionLevel]map[int][]bool{
L: {
0: []bool{true, true, true, false, true, true, true, true, true, false, false, false, true, false, false},
1: []bool{true, true, true, false, false, true, false, true, true, true, true, false, false, true, true},
2: []bool{true, true, true, true, true, false, true, true, false, true, false, true, false, true, false},
3: []bool{true, true, true, true, false, false, false, true, false, false, true, true, true, false, true},
4: []bool{true, true, false, false, true, true, false, false, false, true, false, true, true, true, true},
5: []bool{true, true, false, false, false, true, true, false, false, false, true, true, false, false, false},
6: []bool{true, true, false, true, true, false, false, false, true, false, false, false, false, false, true},
7: []bool{true, true, false, true, false, false, true, false, true, true, true, false, true, true, false},
},
M: {
0: []bool{true, false, true, false, true, false, false, false, false, false, true, false, false, true, false},
1: []bool{true, false, true, false, false, false, true, false, false, true, false, false, true, false, true},
2: []bool{true, false, true, true, true, true, false, false, true, true, true, true, true, false, false},
3: []bool{true, false, true, true, false, true, true, false, true, false, false, true, false, true, true},
4: []bool{true, false, false, false, true, false, true, true, true, true, true, true, false, false, true},
5: []bool{true, false, false, false, false, false, false, true, true, false, false, true, true, true, false},
6: []bool{true, false, false, true, true, true, true, true, false, false, true, false, true, true, true},
7: []bool{true, false, false, true, false, true, false, true, false, true, false, false, false, false, false},
},
Q: {
0: []bool{false, true, true, false, true, false, true, false, true, false, true, true, true, true, true},
1: []bool{false, true, true, false, false, false, false, false, true, true, false, true, false, false, false},
2: []bool{false, true, true, true, true, true, true, false, false, true, true, false, false, false, true},
3: []bool{false, true, true, true, false, true, false, false, false, false, false, false, true, true, false},
4: []bool{false, true, false, false, true, false, false, true, false, true, true, false, true, false, false},
5: []bool{false, true, false, false, false, false, true, true, false, false, false, false, false, true, true},
6: []bool{false, true, false, true, true, true, false, true, true, false, true, true, false, true, false},
7: []bool{false, true, false, true, false, true, true, true, true, true, false, true, true, false, true},
},
H: {
0: []bool{false, false, true, false, true, true, false, true, false, false, false, true, false, false, true},
1: []bool{false, false, true, false, false, true, true, true, false, true, true, true, true, true, false},
2: []bool{false, false, true, true, true, false, false, true, true, true, false, false, true, true, true},
3: []bool{false, false, true, true, false, false, true, true, true, false, true, false, false, false, false},
4: []bool{false, false, false, false, true, true, true, false, true, true, false, false, false, true, false},
5: []bool{false, false, false, false, false, true, false, false, true, false, true, false, true, false, true},
6: []bool{false, false, false, true, true, false, true, false, false, false, false, true, true, false, false},
7: []bool{false, false, false, true, false, false, false, false, false, true, true, true, false, true, true},
},
}
func drawFormatInfo(vi *versionInfo, usedMask int, set func(int, int, bool)) {
var formatInfo []bool
if usedMask == -1 {
formatInfo = []bool{true, true, true, true, true, true, true, true, true, true, true, true, true, true, true} // Set all to true cause -1 --> occupied mask.
} else {
formatInfo = formatInfos[vi.Level][usedMask]
}
if len(formatInfo) == 15 {
dim := vi.modulWidth()
set(0, 8, formatInfo[0])
set(1, 8, formatInfo[1])
set(2, 8, formatInfo[2])
set(3, 8, formatInfo[3])
set(4, 8, formatInfo[4])
set(5, 8, formatInfo[5])
set(7, 8, formatInfo[6])
set(8, 8, formatInfo[7])
set(8, 7, formatInfo[8])
set(8, 5, formatInfo[9])
set(8, 4, formatInfo[10])
set(8, 3, formatInfo[11])
set(8, 2, formatInfo[12])
set(8, 1, formatInfo[13])
set(8, 0, formatInfo[14])
set(8, dim-1, formatInfo[0])
set(8, dim-2, formatInfo[1])
set(8, dim-3, formatInfo[2])
set(8, dim-4, formatInfo[3])
set(8, dim-5, formatInfo[4])
set(8, dim-6, formatInfo[5])
set(8, dim-7, formatInfo[6])
set(dim-8, 8, formatInfo[7])
set(dim-7, 8, formatInfo[8])
set(dim-6, 8, formatInfo[9])
set(dim-5, 8, formatInfo[10])
set(dim-4, 8, formatInfo[11])
set(dim-3, 8, formatInfo[12])
set(dim-2, 8, formatInfo[13])
set(dim-1, 8, formatInfo[14])
}
}
var versionInfoBitsByVersion = map[byte][]bool{
7: []bool{false, false, false, true, true, true, true, true, false, false, true, false, false, true, false, true, false, false},
8: []bool{false, false, true, false, false, false, false, true, false, true, true, false, true, true, true, true, false, false},
9: []bool{false, false, true, false, false, true, true, false, true, false, true, false, false, true, true, false, false, true},
10: []bool{false, false, true, false, true, false, false, true, false, false, true, true, false, true, false, false, true, true},
11: []bool{false, false, true, false, true, true, true, false, true, true, true, true, true, true, false, true, true, false},
12: []bool{false, false, true, true, false, false, false, true, true, true, false, true, true, false, false, false, true, false},
13: []bool{false, false, true, true, false, true, true, false, false, false, false, true, false, false, false, true, true, true},
14: []bool{false, false, true, true, true, false, false, true, true, false, false, false, false, false, true, true, false, true},
15: []bool{false, false, true, true, true, true, true, false, false, true, false, false, true, false, true, false, false, false},
16: []bool{false, true, false, false, false, false, true, false, true, true, false, true, true, true, true, false, false, false},
17: []bool{false, true, false, false, false, true, false, true, false, false, false, true, false, true, true, true, false, true},
18: []bool{false, true, false, false, true, false, true, false, true, false, false, false, false, true, false, true, true, true},