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
This commit is contained in:
parent
64375d875b
commit
6dd096b7f0
13
cmd/web.go
13
cmd/web.go
@ -203,6 +203,12 @@ func runWeb(ctx *cli.Context) error {
|
|||||||
m.Post("/sign_up", bindIgnErr(auth.RegisterForm{}), user.SignUpPost)
|
m.Post("/sign_up", bindIgnErr(auth.RegisterForm{}), user.SignUpPost)
|
||||||
m.Get("/reset_password", user.ResetPasswd)
|
m.Get("/reset_password", user.ResetPasswd)
|
||||||
m.Post("/reset_password", user.ResetPasswdPost)
|
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)
|
}, reqSignOut)
|
||||||
|
|
||||||
m.Group("/user/settings", func() {
|
m.Group("/user/settings", func() {
|
||||||
@ -223,6 +229,13 @@ func runWeb(ctx *cli.Context) error {
|
|||||||
Post(bindIgnErr(auth.NewAccessTokenForm{}), user.SettingsApplicationsPost)
|
Post(bindIgnErr(auth.NewAccessTokenForm{}), user.SettingsApplicationsPost)
|
||||||
m.Post("/applications/delete", user.SettingsDeleteApplication)
|
m.Post("/applications/delete", user.SettingsDeleteApplication)
|
||||||
m.Route("/delete", "GET,POST", user.SettingsDelete)
|
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) {
|
}, reqSignIn, func(ctx *context.Context) {
|
||||||
ctx.Data["PageIsUserSettings"] = true
|
ctx.Data["PageIsUserSettings"] = true
|
||||||
})
|
})
|
||||||
|
@ -787,6 +787,25 @@ func (err ErrTeamAlreadyExist) Error() string {
|
|||||||
return fmt.Sprintf("team already exists [org_id: %d, name: %s]", err.OrgID, err.Name)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
// ____ ___ .__ .___
|
// ____ ___ .__ .___
|
||||||
// | | \______ | | _________ __| _/
|
// | | \______ | | _________ __| _/
|
||||||
// | | /\____ \| | / _ \__ \ / __ |
|
// | | /\____ \| | / _ \__ \ / __ |
|
||||||
|
@ -105,6 +105,7 @@ func init() {
|
|||||||
new(Notification),
|
new(Notification),
|
||||||
new(IssueUser),
|
new(IssueUser),
|
||||||
new(LFSMetaObject),
|
new(LFSMetaObject),
|
||||||
|
new(TwoFactor),
|
||||||
)
|
)
|
||||||
|
|
||||||
gonicNames := []string{"SSL", "UID"}
|
gonicNames := []string{"SSL", "UID"}
|
||||||
|
141
models/twofactor.go
Normal file
141
models/twofactor.go
Normal file
@ -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
|
||||||
|
}
|
@ -173,3 +173,23 @@ type NewAccessTokenForm struct {
|
|||||||
func (f *NewAccessTokenForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
|
func (f *NewAccessTokenForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
|
||||||
return validate(errs, ctx.Data, f, ctx.Locale)
|
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)
|
||||||
|
}
|
||||||
|
@ -23,6 +23,9 @@ email = Email
|
|||||||
password = Password
|
password = Password
|
||||||
re_type = Re-Type
|
re_type = Re-Type
|
||||||
captcha = Captcha
|
captcha = Captcha
|
||||||
|
twofa = Two-factor authentication
|
||||||
|
twofa_scratch = Two-factor scratch code
|
||||||
|
passcode = Passcode
|
||||||
|
|
||||||
repository = Repository
|
repository = Repository
|
||||||
organization = Organization
|
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
|
reset_password_helper = Click here to reset your password
|
||||||
password_too_short = Password length cannot be less then %d.
|
password_too_short = Password length cannot be less then %d.
|
||||||
non_local_account = Non-local accounts cannot change passwords through Gitea.
|
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]
|
[mail]
|
||||||
activate_account = Please activate your account
|
activate_account = Please activate your account
|
||||||
@ -266,6 +275,7 @@ social = Social Accounts
|
|||||||
applications = Applications
|
applications = Applications
|
||||||
orgs = Organizations
|
orgs = Organizations
|
||||||
delete = Delete Account
|
delete = Delete Account
|
||||||
|
twofa = Two-Factor Authentication
|
||||||
uid = Uid
|
uid = Uid
|
||||||
|
|
||||||
public_profile = Public Profile
|
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?
|
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.
|
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_account = Delete Your Account
|
||||||
delete_prompt = The operation will delete your account permanently, and <strong>CANNOT</strong> be undone!
|
delete_prompt = The operation will delete your account permanently, and <strong>CANNOT</strong> be undone!
|
||||||
confirm_delete_account = Confirm Deletion
|
confirm_delete_account = Confirm Deletion
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
package user
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
@ -27,6 +28,8 @@ const (
|
|||||||
TplActivate base.TplName = "user/auth/activate"
|
TplActivate base.TplName = "user/auth/activate"
|
||||||
tplForgotPassword base.TplName = "user/auth/forgot_passwd"
|
tplForgotPassword base.TplName = "user/auth/forgot_passwd"
|
||||||
tplResetPassword base.TplName = "user/auth/reset_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.
|
// AutoSignIn reads cookie and try to auto-login.
|
||||||
@ -69,15 +72,12 @@ func AutoSignIn(ctx *context.Context) (bool, error) {
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignIn render sign in page
|
func checkAutoLogin(ctx *context.Context) bool {
|
||||||
func SignIn(ctx *context.Context) {
|
|
||||||
ctx.Data["Title"] = ctx.Tr("sign_in")
|
|
||||||
|
|
||||||
// Check auto-login.
|
// Check auto-login.
|
||||||
isSucceed, err := AutoSignIn(ctx)
|
isSucceed, err := AutoSignIn(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Handle(500, "AutoSignIn", err)
|
ctx.Handle(500, "AutoSignIn", err)
|
||||||
return
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectTo := ctx.Query("redirect_to")
|
redirectTo := ctx.Query("redirect_to")
|
||||||
@ -94,6 +94,18 @@ func SignIn(ctx *context.Context) {
|
|||||||
} else {
|
} else {
|
||||||
ctx.Redirect(setting.AppSubURL + "/")
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,13 +131,158 @@ func SignInPost(ctx *context.Context, form auth.SignInForm) {
|
|||||||
return
|
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
|
days := 86400 * setting.LogInRememberDays
|
||||||
ctx.SetCookie(setting.CookieUserName, u.Name, days, setting.AppSubURL)
|
ctx.SetCookie(setting.CookieUserName, u.Name, days, setting.AppSubURL)
|
||||||
ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd),
|
ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd),
|
||||||
setting.CookieRememberName, u.Name, days, setting.AppSubURL)
|
setting.CookieRememberName, u.Name, days, setting.AppSubURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.Session.Delete("twofaUid")
|
||||||
|
ctx.Session.Delete("twofaRemember")
|
||||||
ctx.Session.Set("uid", u.ID)
|
ctx.Session.Set("uid", u.ID)
|
||||||
ctx.Session.Set("uname", u.Name)
|
ctx.Session.Set("uname", u.Name)
|
||||||
|
|
||||||
@ -141,12 +298,16 @@ func SignInPost(ctx *context.Context, form auth.SignInForm) {
|
|||||||
|
|
||||||
if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 {
|
if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 {
|
||||||
ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL)
|
ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL)
|
||||||
|
if obeyRedirect {
|
||||||
ctx.Redirect(redirectTo)
|
ctx.Redirect(redirectTo)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if obeyRedirect {
|
||||||
ctx.Redirect(setting.AppSubURL + "/")
|
ctx.Redirect(setting.AppSubURL + "/")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SignOut sign out from login status
|
// SignOut sign out from login status
|
||||||
func SignOut(ctx *context.Context) {
|
func SignOut(ctx *context.Context) {
|
||||||
|
@ -5,12 +5,19 @@
|
|||||||
package user
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Unknwon/com"
|
"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/models"
|
||||||
"code.gitea.io/gitea/modules/auth"
|
"code.gitea.io/gitea/modules/auth"
|
||||||
@ -28,6 +35,8 @@ const (
|
|||||||
tplSettingsSSHKeys base.TplName = "user/settings/sshkeys"
|
tplSettingsSSHKeys base.TplName = "user/settings/sshkeys"
|
||||||
tplSettingsSocial base.TplName = "user/settings/social"
|
tplSettingsSocial base.TplName = "user/settings/social"
|
||||||
tplSettingsApplications base.TplName = "user/settings/applications"
|
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"
|
tplSettingsDelete base.TplName = "user/settings/delete"
|
||||||
tplSecurity base.TplName = "user/security"
|
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
|
// SettingsDelete render user suicide page and response for delete user himself
|
||||||
func SettingsDelete(ctx *context.Context) {
|
func SettingsDelete(ctx *context.Context) {
|
||||||
ctx.Data["Title"] = ctx.Tr("settings")
|
ctx.Data["Title"] = ctx.Tr("settings")
|
||||||
|
27
templates/user/auth/twofa.tmpl
Normal file
27
templates/user/auth/twofa.tmpl
Normal file
@ -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
Normal file
26
templates/user/auth/twofa_scratch.tmpl
Normal file
@ -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" .}}
|
@ -19,6 +19,9 @@
|
|||||||
<a class="{{if .PageIsSettingsApplications}}active{{end}} item" href="{{AppSubUrl}}/user/settings/applications">
|
<a class="{{if .PageIsSettingsApplications}}active{{end}} item" href="{{AppSubUrl}}/user/settings/applications">
|
||||||
{{.i18n.Tr "settings.applications"}}
|
{{.i18n.Tr "settings.applications"}}
|
||||||
</a>
|
</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">
|
<a class="{{if .PageIsSettingsDelete}}active{{end}} item" href="{{AppSubUrl}}/user/settings/delete">
|
||||||
{{.i18n.Tr "settings.delete"}}
|
{{.i18n.Tr "settings.delete"}}
|
||||||
</a>
|
</a>
|
||||||
|
48
templates/user/settings/twofa.tmpl
Normal file
48
templates/user/settings/twofa.tmpl
Normal file
@ -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
Normal file
33
templates/user/settings/twofa_enroll.tmpl
Normal file
@ -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
generated
vendored
Normal file
21
vendor/github.com/boombuler/barcode/LICENSE
generated
vendored
Normal file
@ -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
generated
vendored
Normal file
18
vendor/github.com/boombuler/barcode/README.md
generated
vendored
Normal file
@ -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
generated
vendored
Normal file
21
vendor/github.com/boombuler/barcode/barcode.go
generated
vendored
Normal file
@ -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
generated
vendored
Normal file
66
vendor/github.com/boombuler/barcode/qr/alphanumeric.go
generated
vendored
Normal file
@ -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
generated
vendored
Normal file
23
vendor/github.com/boombuler/barcode/qr/automatic.go
generated
vendored
Normal file
@ -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
generated
vendored
Normal file
59
vendor/github.com/boombuler/barcode/qr/blocks.go
generated
vendored
Normal file
@ -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
generated
vendored
Normal file
416
vendor/github.com/boombuler/barcode/qr/encoder.go
generated
vendored
Normal file
@ -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},
|
||||||
|
19: []bool{false, true, false, false, true, true, false, true, false, true, false, false, true, true, false, false, true, false},
|
||||||
|
20: []bool{false, true, false, true, false, false, true, false, false, true, true, false, true, false, false, true, true, false},
|
||||||
|
21: []bool{false, true, false, true, false, true, false, true, true, false, true, false, false, false, false, false, true, true},
|
||||||
|
22: []bool{false, true, false, true, true, false, true, false, false, false, true, true, false, false, true, false, false, true},
|
||||||
|
23: []bool{false, true, false, true, true, true, false, true, true, true, true, true, true, false, true, true, false, false},
|
||||||
|
24: []bool{false, true, true, false, false, false, true, true, true, false, true, true, false, false, false, true, false, false},
|
||||||
|
25: []bool{false, true, true, false, false, true, false, false, false, true, true, true, true, false, false, false, false, true},
|
||||||
|
26: []bool{false, true, true, false, true, false, true, true, true, true, true, false, true, false, true, false, true, true},
|
||||||
|
27: []bool{false, true, true, false, true, true, false, false, false, false, true, false, false, false, true, true, true, false},
|
||||||
|
28: []bool{false, true, true, true, false, false, true, true, false, false, false, false, false, true, true, false, true, false},
|
||||||
|
29: []bool{false, true, true, true, false, true, false, false, true, true, false, false, true, true, true, true, true, true},
|
||||||
|
30: []bool{false, true, true, true, true, false, true, true, false, true, false, true, true, true, false, true, false, true},
|
||||||
|
31: []bool{false, true, true, true, true, true, false, false, true, false, false, true, false, true, false, false, false, false},
|
||||||
|
32: []bool{true, false, false, false, false, false, true, false, false, true, true, true, false, true, false, true, false, true},
|
||||||
|
33: []bool{true, false, false, false, false, true, false, true, true, false, true, true, true, true, false, false, false, false},
|
||||||
|
34: []bool{true, false, false, false, true, false, true, false, false, false, true, false, true, true, true, false, true, false},
|
||||||
|
35: []bool{true, false, false, false, true, true, false, true, true, true, true, false, false, true, true, true, true, true},
|
||||||
|
36: []bool{true, false, false, true, false, false, true, false, true, true, false, false, false, false, true, false, true, true},
|
||||||
|
37: []bool{true, false, false, true, false, true, false, true, false, false, false, false, true, false, true, true, true, false},
|
||||||
|
38: []bool{true, false, false, true, true, false, true, false, true, false, false, true, true, false, false, true, false, false},
|
||||||
|
39: []bool{true, false, false, true, true, true, false, true, false, true, false, true, false, false, false, false, false, true},
|
||||||
|
40: []bool{true, false, true, false, false, false, true, true, false, false, false, true, true, false, true, false, false, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawVersionInfo(vi *versionInfo, set func(int, int, bool)) {
|
||||||
|
versionInfoBits, ok := versionInfoBitsByVersion[vi.Version]
|
||||||
|
|
||||||
|
if ok && len(versionInfoBits) > 0 {
|
||||||
|
for i := 0; i < len(versionInfoBits); i++ {
|
||||||
|
x := (vi.modulWidth() - 11) + i%3
|
||||||
|
y := i / 3
|
||||||
|
set(x, y, versionInfoBits[len(versionInfoBits)-i-1])
|
||||||
|
set(y, x, versionInfoBits[len(versionInfoBits)-i-1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func addPaddingAndTerminator(bl *utils.BitList, vi *versionInfo) {
|
||||||
|
for i := 0; i < 4 && bl.Len() < vi.totalDataBytes()*8; i++ {
|
||||||
|
bl.AddBit(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
for bl.Len()%8 != 0 {
|
||||||
|
bl.AddBit(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; bl.Len() < vi.totalDataBytes()*8; i++ {
|
||||||
|
if i%2 == 0 {
|
||||||
|
bl.AddByte(236)
|
||||||
|
} else {
|
||||||
|
bl.AddByte(17)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
vendor/github.com/boombuler/barcode/qr/errorcorrection.go
generated
vendored
Normal file
29
vendor/github.com/boombuler/barcode/qr/errorcorrection.go
generated
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package qr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/boombuler/barcode/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type errorCorrection struct {
|
||||||
|
rs *utils.ReedSolomonEncoder
|
||||||
|
}
|
||||||
|
|
||||||
|
var ec = newErrorCorrection()
|
||||||
|
|
||||||
|
func newErrorCorrection() *errorCorrection {
|
||||||
|
fld := utils.NewGaloisField(285, 256, 0)
|
||||||
|
return &errorCorrection{utils.NewReedSolomonEncoder(fld)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *errorCorrection) calcECC(data []byte, eccCount byte) []byte {
|
||||||
|
dataInts := make([]int, len(data))
|
||||||
|
for i := 0; i < len(data); i++ {
|
||||||
|
dataInts[i] = int(data[i])
|
||||||
|
}
|
||||||
|
res := ec.rs.Encode(dataInts, int(eccCount))
|
||||||
|
result := make([]byte, len(res))
|
||||||
|
for i := 0; i < len(res); i++ {
|
||||||
|
result[i] = byte(res[i])
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
56
vendor/github.com/boombuler/barcode/qr/numeric.go
generated
vendored
Normal file
56
vendor/github.com/boombuler/barcode/qr/numeric.go
generated
vendored
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package qr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/boombuler/barcode/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func encodeNumeric(content string, ecl ErrorCorrectionLevel) (*utils.BitList, *versionInfo, error) {
|
||||||
|
contentBitCount := (len(content) / 3) * 10
|
||||||
|
switch len(content) % 3 {
|
||||||
|
case 1:
|
||||||
|
contentBitCount += 4
|
||||||
|
case 2:
|
||||||
|
contentBitCount += 7
|
||||||
|
}
|
||||||
|
vi := findSmallestVersionInfo(ecl, numericMode, contentBitCount)
|
||||||
|
if vi == nil {
|
||||||
|
return nil, nil, errors.New("To much data to encode")
|
||||||
|
}
|
||||||
|
res := new(utils.BitList)
|
||||||
|
res.AddBits(int(numericMode), 4)
|
||||||
|
res.AddBits(len(content), vi.charCountBits(numericMode))
|
||||||
|
|
||||||
|
for pos := 0; pos < len(content); pos += 3 {
|
||||||
|
var curStr string
|
||||||
|
if pos+3 <= len(content) {
|
||||||
|
curStr = content[pos : pos+3]
|
||||||
|
} else {
|
||||||
|
curStr = content[pos:]
|
||||||
|
}
|
||||||
|
|
||||||
|
i, err := strconv.Atoi(curStr)
|
||||||
|
if err != nil || i < 0 {
|
||||||
|
return nil, nil, fmt.Errorf("\"%s\" can not be encoded as %s", content, Numeric)
|
||||||
|
}
|
||||||
|
var bitCnt byte
|
||||||
|
switch len(curStr) % 3 {
|
||||||
|
case 0:
|
||||||
|
bitCnt = 10
|
||||||
|
case 1:
|
||||||
|
bitCnt = 4
|
||||||
|
break
|
||||||
|
case 2:
|
||||||
|
bitCnt = 7
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
res.AddBits(i, bitCnt)
|
||||||
|
}
|
||||||
|
|
||||||
|
addPaddingAndTerminator(res, vi)
|
||||||
|
return res, vi, nil
|
||||||
|
}
|
170
vendor/github.com/boombuler/barcode/qr/qrcode.go
generated
vendored
Normal file
170
vendor/github.com/boombuler/barcode/qr/qrcode.go
generated
vendored
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
package qr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/boombuler/barcode"
|
||||||
|
"github.com/boombuler/barcode/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type qrcode struct {
|
||||||
|
dimension int
|
||||||
|
data *utils.BitList
|
||||||
|
content string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qr *qrcode) Content() string {
|
||||||
|
return qr.content
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qr *qrcode) Metadata() barcode.Metadata {
|
||||||
|
return barcode.Metadata{"QR Code", 2}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qr *qrcode) ColorModel() color.Model {
|
||||||
|
return color.Gray16Model
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qr *qrcode) Bounds() image.Rectangle {
|
||||||
|
return image.Rect(0, 0, qr.dimension, qr.dimension)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qr *qrcode) At(x, y int) color.Color {
|
||||||
|
if qr.Get(x, y) {
|
||||||
|
return color.Black
|
||||||
|
}
|
||||||
|
return color.White
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qr *qrcode) Get(x, y int) bool {
|
||||||
|
return qr.data.GetBit(x*qr.dimension + y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qr *qrcode) Set(x, y int, val bool) {
|
||||||
|
qr.data.SetBit(x*qr.dimension+y, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qr *qrcode) CheckSum() int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qr *qrcode) calcPenalty() uint {
|
||||||
|
return qr.calcPenaltyRule1() + qr.calcPenaltyRule2() + qr.calcPenaltyRule3() + qr.calcPenaltyRule4()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qr *qrcode) calcPenaltyRule1() uint {
|
||||||
|
var result uint
|
||||||
|
for x := 0; x < qr.dimension; x++ {
|
||||||
|
checkForX := false
|
||||||
|
var cntX uint
|
||||||
|
checkForY := false
|
||||||
|
var cntY uint
|
||||||
|
|
||||||
|
for y := 0; y < qr.dimension; y++ {
|
||||||
|
if qr.Get(x, y) == checkForX {
|
||||||
|
cntX++
|
||||||
|
} else {
|
||||||
|
checkForX = !checkForX
|
||||||
|
if cntX >= 5 {
|
||||||
|
result += cntX - 2
|
||||||
|
}
|
||||||
|
cntX = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if qr.Get(y, x) == checkForY {
|
||||||
|
cntY++
|
||||||
|
} else {
|
||||||
|
checkForY = !checkForY
|
||||||
|
if cntY >= 5 {
|
||||||
|
result += cntY - 2
|
||||||
|
}
|
||||||
|
cntY = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cntX >= 5 {
|
||||||
|
result += cntX - 2
|
||||||
|
}
|
||||||
|
if cntY >= 5 {
|
||||||
|
result += cntY - 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qr *qrcode) calcPenaltyRule2() uint {
|
||||||
|
var result uint
|
||||||
|
for x := 0; x < qr.dimension-1; x++ {
|
||||||
|
for y := 0; y < qr.dimension-1; y++ {
|
||||||
|
check := qr.Get(x, y)
|
||||||
|
if qr.Get(x, y+1) == check && qr.Get(x+1, y) == check && qr.Get(x+1, y+1) == check {
|
||||||
|
result += 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qr *qrcode) calcPenaltyRule3() uint {
|
||||||
|
pattern1 := []bool{true, false, true, true, true, false, true, false, false, false, false}
|
||||||
|
pattern2 := []bool{false, false, false, false, true, false, true, true, true, false, true}
|
||||||
|
|
||||||
|
var result uint
|
||||||
|
for x := 0; x <= qr.dimension-len(pattern1); x++ {
|
||||||
|
for y := 0; y < qr.dimension; y++ {
|
||||||
|
pattern1XFound := true
|
||||||
|
pattern2XFound := true
|
||||||
|
pattern1YFound := true
|
||||||
|
pattern2YFound := true
|
||||||
|
|
||||||
|
for i := 0; i < len(pattern1); i++ {
|
||||||
|
iv := qr.Get(x+i, y)
|
||||||
|
if iv != pattern1[i] {
|
||||||
|
pattern1XFound = false
|
||||||
|
}
|
||||||
|
if iv != pattern2[i] {
|
||||||
|
pattern2XFound = false
|
||||||
|
}
|
||||||
|
iv = qr.Get(y, x+i)
|
||||||
|
if iv != pattern1[i] {
|
||||||
|
pattern1YFound = false
|
||||||
|
}
|
||||||
|
if iv != pattern2[i] {
|
||||||
|
pattern2YFound = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pattern1XFound || pattern2XFound {
|
||||||
|
result += 40
|
||||||
|
}
|
||||||
|
if pattern1YFound || pattern2YFound {
|
||||||
|
result += 40
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qr *qrcode) calcPenaltyRule4() uint {
|
||||||
|
totalNum := qr.data.Len()
|
||||||
|
trueCnt := 0
|
||||||
|
for i := 0; i < totalNum; i++ {
|
||||||
|
if qr.data.GetBit(i) {
|
||||||
|
trueCnt++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
percDark := float64(trueCnt) * 100 / float64(totalNum)
|
||||||
|
floor := math.Abs(math.Floor(percDark/5) - 10)
|
||||||
|
ceil := math.Abs(math.Ceil(percDark/5) - 10)
|
||||||
|
return uint(math.Min(floor, ceil) * 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBarcode(dim int) *qrcode {
|
||||||
|
res := new(qrcode)
|
||||||
|
res.dimension = dim
|
||||||
|
res.data = utils.NewBitList(dim * dim)
|
||||||
|
return res
|
||||||
|
}
|
27
vendor/github.com/boombuler/barcode/qr/unicode.go
generated
vendored
Normal file
27
vendor/github.com/boombuler/barcode/qr/unicode.go
generated
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package qr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/boombuler/barcode/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func encodeUnicode(content string, ecl ErrorCorrectionLevel) (*utils.BitList, *versionInfo, error) {
|
||||||
|
data := []byte(content)
|
||||||
|
|
||||||
|
vi := findSmallestVersionInfo(ecl, byteMode, len(data)*8)
|
||||||
|
if vi == nil {
|
||||||
|
return nil, nil, errors.New("To much data to encode")
|
||||||
|
}
|
||||||
|
|
||||||
|
// It's not correct to add the unicode bytes to the result directly but most readers can't handle the
|
||||||
|
// required ECI header...
|
||||||
|
res := new(utils.BitList)
|
||||||
|
res.AddBits(int(byteMode), 4)
|
||||||
|
res.AddBits(len(content), vi.charCountBits(byteMode))
|
||||||
|
for _, b := range data {
|
||||||
|
res.AddByte(b)
|
||||||
|
}
|
||||||
|
addPaddingAndTerminator(res, vi)
|
||||||
|
return res, vi, nil
|
||||||
|
}
|
310
vendor/github.com/boombuler/barcode/qr/versioninfo.go
generated
vendored
Normal file
310
vendor/github.com/boombuler/barcode/qr/versioninfo.go
generated
vendored
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
package qr
|
||||||
|
|
||||||
|
import "math"
|
||||||
|
|
||||||
|
// ErrorCorrectionLevel indicates the amount of "backup data" stored in the QR code
|
||||||
|
type ErrorCorrectionLevel byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
// L recovers 7% of data
|
||||||
|
L ErrorCorrectionLevel = iota
|
||||||
|
// M recovers 15% of data
|
||||||
|
M
|
||||||
|
// Q recovers 25% of data
|
||||||
|
Q
|
||||||
|
// H recovers 30% of data
|
||||||
|
H
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ecl ErrorCorrectionLevel) String() string {
|
||||||
|
switch ecl {
|
||||||
|
case L:
|
||||||
|
return "L"
|
||||||
|
case M:
|
||||||
|
return "M"
|
||||||
|
case Q:
|
||||||
|
return "Q"
|
||||||
|
case H:
|
||||||
|
return "H"
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
type encodingMode byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
numericMode encodingMode = 1
|
||||||
|
alphaNumericMode encodingMode = 2
|
||||||
|
byteMode encodingMode = 4
|
||||||
|
kanjiMode encodingMode = 8
|
||||||
|
)
|
||||||
|
|
||||||
|
type versionInfo struct {
|
||||||
|
Version byte
|
||||||
|
Level ErrorCorrectionLevel
|
||||||
|
ErrorCorrectionCodewordsPerBlock byte
|
||||||
|
NumberOfBlocksInGroup1 byte
|
||||||
|
DataCodeWordsPerBlockInGroup1 byte
|
||||||
|
NumberOfBlocksInGroup2 byte
|
||||||
|
DataCodeWordsPerBlockInGroup2 byte
|
||||||
|
}
|
||||||
|
|
||||||
|
var versionInfos = []*versionInfo{
|
||||||
|
&versionInfo{1, L, 7, 1, 19, 0, 0},
|
||||||
|
&versionInfo{1, M, 10, 1, 16, 0, 0},
|
||||||
|
&versionInfo{1, Q, 13, 1, 13, 0, 0},
|
||||||
|
&versionInfo{1, H, 17, 1, 9, 0, 0},
|
||||||
|
&versionInfo{2, L, 10, 1, 34, 0, 0},
|
||||||
|
&versionInfo{2, M, 16, 1, 28, 0, 0},
|
||||||
|
&versionInfo{2, Q, 22, 1, 22, 0, 0},
|
||||||
|
&versionInfo{2, H, 28, 1, 16, 0, 0},
|
||||||
|
&versionInfo{3, L, 15, 1, 55, 0, 0},
|
||||||
|
&versionInfo{3, M, 26, 1, 44, 0, 0},
|
||||||
|
&versionInfo{3, Q, 18, 2, 17, 0, 0},
|
||||||
|
&versionInfo{3, H, 22, 2, 13, 0, 0},
|
||||||
|
&versionInfo{4, L, 20, 1, 80, 0, 0},
|
||||||
|
&versionInfo{4, M, 18, 2, 32, 0, 0},
|
||||||
|
&versionInfo{4, Q, 26, 2, 24, 0, 0},
|
||||||
|
&versionInfo{4, H, 16, 4, 9, 0, 0},
|
||||||
|
&versionInfo{5, L, 26, 1, 108, 0, 0},
|
||||||
|
&versionInfo{5, M, 24, 2, 43, 0, 0},
|
||||||
|
&versionInfo{5, Q, 18, 2, 15, 2, 16},
|
||||||
|
&versionInfo{5, H, 22, 2, 11, 2, 12},
|
||||||
|
&versionInfo{6, L, 18, 2, 68, 0, 0},
|
||||||
|
&versionInfo{6, M, 16, 4, 27, 0, 0},
|
||||||
|
&versionInfo{6, Q, 24, 4, 19, 0, 0},
|
||||||
|
&versionInfo{6, H, 28, 4, 15, 0, 0},
|
||||||
|
&versionInfo{7, L, 20, 2, 78, 0, 0},
|
||||||
|
&versionInfo{7, M, 18, 4, 31, 0, 0},
|
||||||
|
&versionInfo{7, Q, 18, 2, 14, 4, 15},
|
||||||
|
&versionInfo{7, H, 26, 4, 13, 1, 14},
|
||||||
|
&versionInfo{8, L, 24, 2, 97, 0, 0},
|
||||||
|
&versionInfo{8, M, 22, 2, 38, 2, 39},
|
||||||
|
&versionInfo{8, Q, 22, 4, 18, 2, 19},
|
||||||
|
&versionInfo{8, H, 26, 4, 14, 2, 15},
|
||||||
|
&versionInfo{9, L, 30, 2, 116, 0, 0},
|
||||||
|
&versionInfo{9, M, 22, 3, 36, 2, 37},
|
||||||
|
&versionInfo{9, Q, 20, 4, 16, 4, 17},
|
||||||
|
&versionInfo{9, H, 24, 4, 12, 4, 13},
|
||||||
|
&versionInfo{10, L, 18, 2, 68, 2, 69},
|
||||||
|
&versionInfo{10, M, 26, 4, 43, 1, 44},
|
||||||
|
&versionInfo{10, Q, 24, 6, 19, 2, 20},
|
||||||
|
&versionInfo{10, H, 28, 6, 15, 2, 16},
|
||||||
|
&versionInfo{11, L, 20, 4, 81, 0, 0},
|
||||||
|
&versionInfo{11, M, 30, 1, 50, 4, 51},
|
||||||
|
&versionInfo{11, Q, 28, 4, 22, 4, 23},
|
||||||
|
&versionInfo{11, H, 24, 3, 12, 8, 13},
|
||||||
|
&versionInfo{12, L, 24, 2, 92, 2, 93},
|
||||||
|
&versionInfo{12, M, 22, 6, 36, 2, 37},
|
||||||
|
&versionInfo{12, Q, 26, 4, 20, 6, 21},
|
||||||
|
&versionInfo{12, H, 28, 7, 14, 4, 15},
|
||||||
|
&versionInfo{13, L, 26, 4, 107, 0, 0},
|
||||||
|
&versionInfo{13, M, 22, 8, 37, 1, 38},
|
||||||
|
&versionInfo{13, Q, 24, 8, 20, 4, 21},
|
||||||
|
&versionInfo{13, H, 22, 12, 11, 4, 12},
|
||||||
|
&versionInfo{14, L, 30, 3, 115, 1, 116},
|
||||||
|
&versionInfo{14, M, 24, 4, 40, 5, 41},
|
||||||
|
&versionInfo{14, Q, 20, 11, 16, 5, 17},
|
||||||
|
&versionInfo{14, H, 24, 11, 12, 5, 13},
|
||||||
|
&versionInfo{15, L, 22, 5, 87, 1, 88},
|
||||||
|
&versionInfo{15, M, 24, 5, 41, 5, 42},
|
||||||
|
&versionInfo{15, Q, 30, 5, 24, 7, 25},
|
||||||
|
&versionInfo{15, H, 24, 11, 12, 7, 13},
|
||||||
|
&versionInfo{16, L, 24, 5, 98, 1, 99},
|
||||||
|
&versionInfo{16, M, 28, 7, 45, 3, 46},
|
||||||
|
&versionInfo{16, Q, 24, 15, 19, 2, 20},
|
||||||
|
&versionInfo{16, H, 30, 3, 15, 13, 16},
|
||||||
|
&versionInfo{17, L, 28, 1, 107, 5, 108},
|
||||||
|
&versionInfo{17, M, 28, 10, 46, 1, 47},
|
||||||
|
&versionInfo{17, Q, 28, 1, 22, 15, 23},
|
||||||
|
&versionInfo{17, H, 28, 2, 14, 17, 15},
|
||||||
|
&versionInfo{18, L, 30, 5, 120, 1, 121},
|
||||||
|
&versionInfo{18, M, 26, 9, 43, 4, 44},
|
||||||
|
&versionInfo{18, Q, 28, 17, 22, 1, 23},
|
||||||
|
&versionInfo{18, H, 28, 2, 14, 19, 15},
|
||||||
|
&versionInfo{19, L, 28, 3, 113, 4, 114},
|
||||||
|
&versionInfo{19, M, 26, 3, 44, 11, 45},
|
||||||
|
&versionInfo{19, Q, 26, 17, 21, 4, 22},
|
||||||
|
&versionInfo{19, H, 26, 9, 13, 16, 14},
|
||||||
|
&versionInfo{20, L, 28, 3, 107, 5, 108},
|
||||||
|
&versionInfo{20, M, 26, 3, 41, 13, 42},
|
||||||
|
&versionInfo{20, Q, 30, 15, 24, 5, 25},
|
||||||
|
&versionInfo{20, H, 28, 15, 15, 10, 16},
|
||||||
|
&versionInfo{21, L, 28, 4, 116, 4, 117},
|
||||||
|
&versionInfo{21, M, 26, 17, 42, 0, 0},
|
||||||
|
&versionInfo{21, Q, 28, 17, 22, 6, 23},
|
||||||
|
&versionInfo{21, H, 30, 19, 16, 6, 17},
|
||||||
|
&versionInfo{22, L, 28, 2, 111, 7, 112},
|
||||||
|
&versionInfo{22, M, 28, 17, 46, 0, 0},
|
||||||
|
&versionInfo{22, Q, 30, 7, 24, 16, 25},
|
||||||
|
&versionInfo{22, H, 24, 34, 13, 0, 0},
|
||||||
|
&versionInfo{23, L, 30, 4, 121, 5, 122},
|
||||||
|
&versionInfo{23, M, 28, 4, 47, 14, 48},
|
||||||
|
&versionInfo{23, Q, 30, 11, 24, 14, 25},
|
||||||
|
&versionInfo{23, H, 30, 16, 15, 14, 16},
|
||||||
|
&versionInfo{24, L, 30, 6, 117, 4, 118},
|
||||||
|
&versionInfo{24, M, 28, 6, 45, 14, 46},
|
||||||
|
&versionInfo{24, Q, 30, 11, 24, 16, 25},
|
||||||
|
&versionInfo{24, H, 30, 30, 16, 2, 17},
|
||||||
|
&versionInfo{25, L, 26, 8, 106, 4, 107},
|
||||||
|
&versionInfo{25, M, 28, 8, 47, 13, 48},
|
||||||
|
&versionInfo{25, Q, 30, 7, 24, 22, 25},
|
||||||
|
&versionInfo{25, H, 30, 22, 15, 13, 16},
|
||||||
|
&versionInfo{26, L, 28, 10, 114, 2, 115},
|
||||||
|
&versionInfo{26, M, 28, 19, 46, 4, 47},
|
||||||
|
&versionInfo{26, Q, 28, 28, 22, 6, 23},
|
||||||
|
&versionInfo{26, H, 30, 33, 16, 4, 17},
|
||||||
|
&versionInfo{27, L, 30, 8, 122, 4, 123},
|
||||||
|
&versionInfo{27, M, 28, 22, 45, 3, 46},
|
||||||
|
&versionInfo{27, Q, 30, 8, 23, 26, 24},
|
||||||
|
&versionInfo{27, H, 30, 12, 15, 28, 16},
|
||||||
|
&versionInfo{28, L, 30, 3, 117, 10, 118},
|
||||||
|
&versionInfo{28, M, 28, 3, 45, 23, 46},
|
||||||
|
&versionInfo{28, Q, 30, 4, 24, 31, 25},
|
||||||
|
&versionInfo{28, H, 30, 11, 15, 31, 16},
|
||||||
|
&versionInfo{29, L, 30, 7, 116, 7, 117},
|
||||||
|
&versionInfo{29, M, 28, 21, 45, 7, 46},
|
||||||
|
&versionInfo{29, Q, 30, 1, 23, 37, 24},
|
||||||
|
&versionInfo{29, H, 30, 19, 15, 26, 16},
|
||||||
|
&versionInfo{30, L, 30, 5, 115, 10, 116},
|
||||||
|
&versionInfo{30, M, 28, 19, 47, 10, 48},
|
||||||
|
&versionInfo{30, Q, 30, 15, 24, 25, 25},
|
||||||
|
&versionInfo{30, H, 30, 23, 15, 25, 16},
|
||||||
|
&versionInfo{31, L, 30, 13, 115, 3, 116},
|
||||||
|
&versionInfo{31, M, 28, 2, 46, 29, 47},
|
||||||
|
&versionInfo{31, Q, 30, 42, 24, 1, 25},
|
||||||
|
&versionInfo{31, H, 30, 23, 15, 28, 16},
|
||||||
|
&versionInfo{32, L, 30, 17, 115, 0, 0},
|
||||||
|
&versionInfo{32, M, 28, 10, 46, 23, 47},
|
||||||
|
&versionInfo{32, Q, 30, 10, 24, 35, 25},
|
||||||
|
&versionInfo{32, H, 30, 19, 15, 35, 16},
|
||||||
|
&versionInfo{33, L, 30, 17, 115, 1, 116},
|
||||||
|
&versionInfo{33, M, 28, 14, 46, 21, 47},
|
||||||
|
&versionInfo{33, Q, 30, 29, 24, 19, 25},
|
||||||
|
&versionInfo{33, H, 30, 11, 15, 46, 16},
|
||||||
|
&versionInfo{34, L, 30, 13, 115, 6, 116},
|
||||||
|
&versionInfo{34, M, 28, 14, 46, 23, 47},
|
||||||
|
&versionInfo{34, Q, 30, 44, 24, 7, 25},
|
||||||
|
&versionInfo{34, H, 30, 59, 16, 1, 17},
|
||||||
|
&versionInfo{35, L, 30, 12, 121, 7, 122},
|
||||||
|
&versionInfo{35, M, 28, 12, 47, 26, 48},
|
||||||
|
&versionInfo{35, Q, 30, 39, 24, 14, 25},
|
||||||
|
&versionInfo{35, H, 30, 22, 15, 41, 16},
|
||||||
|
&versionInfo{36, L, 30, 6, 121, 14, 122},
|
||||||
|
&versionInfo{36, M, 28, 6, 47, 34, 48},
|
||||||
|
&versionInfo{36, Q, 30, 46, 24, 10, 25},
|
||||||
|
&versionInfo{36, H, 30, 2, 15, 64, 16},
|
||||||
|
&versionInfo{37, L, 30, 17, 122, 4, 123},
|
||||||
|
&versionInfo{37, M, 28, 29, 46, 14, 47},
|
||||||
|
&versionInfo{37, Q, 30, 49, 24, 10, 25},
|
||||||
|
&versionInfo{37, H, 30, 24, 15, 46, 16},
|
||||||
|
&versionInfo{38, L, 30, 4, 122, 18, 123},
|
||||||
|
&versionInfo{38, M, 28, 13, 46, 32, 47},
|
||||||
|
&versionInfo{38, Q, 30, 48, 24, 14, 25},
|
||||||
|
&versionInfo{38, H, 30, 42, 15, 32, 16},
|
||||||
|
&versionInfo{39, L, 30, 20, 117, 4, 118},
|
||||||
|
&versionInfo{39, M, 28, 40, 47, 7, 48},
|
||||||
|
&versionInfo{39, Q, 30, 43, 24, 22, 25},
|
||||||
|
&versionInfo{39, H, 30, 10, 15, 67, 16},
|
||||||
|
&versionInfo{40, L, 30, 19, 118, 6, 119},
|
||||||
|
&versionInfo{40, M, 28, 18, 47, 31, 48},
|
||||||
|
&versionInfo{40, Q, 30, 34, 24, 34, 25},
|
||||||
|
&versionInfo{40, H, 30, 20, 15, 61, 16},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vi *versionInfo) totalDataBytes() int {
|
||||||
|
g1Data := int(vi.NumberOfBlocksInGroup1) * int(vi.DataCodeWordsPerBlockInGroup1)
|
||||||
|
g2Data := int(vi.NumberOfBlocksInGroup2) * int(vi.DataCodeWordsPerBlockInGroup2)
|
||||||
|
return (g1Data + g2Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vi *versionInfo) charCountBits(m encodingMode) byte {
|
||||||
|
switch m {
|
||||||
|
case numericMode:
|
||||||
|
if vi.Version < 10 {
|
||||||
|
return 10
|
||||||
|
} else if vi.Version < 27 {
|
||||||
|
return 12
|
||||||
|
}
|
||||||
|
return 14
|
||||||
|
|
||||||
|
case alphaNumericMode:
|
||||||
|
if vi.Version < 10 {
|
||||||
|
return 9
|
||||||
|
} else if vi.Version < 27 {
|
||||||
|
return 11
|
||||||
|
}
|
||||||
|
return 13
|
||||||
|
|
||||||
|
case byteMode:
|
||||||
|
if vi.Version < 10 {
|
||||||
|
return 8
|
||||||
|
}
|
||||||
|
return 16
|
||||||
|
|
||||||
|
case kanjiMode:
|
||||||
|
if vi.Version < 10 {
|
||||||
|
return 8
|
||||||
|
} else if vi.Version < 27 {
|
||||||
|
return 10
|
||||||
|
}
|
||||||
|
return 12
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vi *versionInfo) modulWidth() int {
|
||||||
|
return ((int(vi.Version) - 1) * 4) + 21
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vi *versionInfo) alignmentPatternPlacements() []int {
|
||||||
|
if vi.Version == 1 {
|
||||||
|
return make([]int, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
first := 6
|
||||||
|
last := vi.modulWidth() - 7
|
||||||
|
space := float64(last - first)
|
||||||
|
count := int(math.Ceil(space/28)) + 1
|
||||||
|
|
||||||
|
result := make([]int, count)
|
||||||
|
result[0] = first
|
||||||
|
result[len(result)-1] = last
|
||||||
|
if count > 2 {
|
||||||
|
step := int(math.Ceil(float64(last-first) / float64(count-1)))
|
||||||
|
if step%2 == 1 {
|
||||||
|
frac := float64(last-first) / float64(count-1)
|
||||||
|
_, x := math.Modf(frac)
|
||||||
|
if x >= 0.5 {
|
||||||
|
frac = math.Ceil(frac)
|
||||||
|
} else {
|
||||||
|
frac = math.Floor(frac)
|
||||||
|
}
|
||||||
|
|
||||||
|
if int(frac)%2 == 0 {
|
||||||
|
step--
|
||||||
|
} else {
|
||||||
|
step++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 1; i <= count-2; i++ {
|
||||||
|
result[i] = last - (step * (count - 1 - i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func findSmallestVersionInfo(ecl ErrorCorrectionLevel, mode encodingMode, dataBits int) *versionInfo {
|
||||||
|
dataBits = dataBits + 4 // mode indicator
|
||||||
|
for _, vi := range versionInfos {
|
||||||
|
if vi.Level == ecl {
|
||||||
|
if (vi.totalDataBytes() * 8) >= (dataBits + int(vi.charCountBits(mode))) {
|
||||||
|
return vi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
115
vendor/github.com/boombuler/barcode/scaledbarcode.go
generated
vendored
Normal file
115
vendor/github.com/boombuler/barcode/scaledbarcode.go
generated
vendored
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
package barcode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
type wrapFunc func(x, y int) color.Color
|
||||||
|
|
||||||
|
type scaledBarcode struct {
|
||||||
|
wrapped Barcode
|
||||||
|
wrapperFunc wrapFunc
|
||||||
|
rect image.Rectangle
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *scaledBarcode) Content() string {
|
||||||
|
return bc.wrapped.Content()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *scaledBarcode) Metadata() Metadata {
|
||||||
|
return bc.wrapped.Metadata()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *scaledBarcode) ColorModel() color.Model {
|
||||||
|
return bc.wrapped.ColorModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *scaledBarcode) Bounds() image.Rectangle {
|
||||||
|
return bc.rect
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *scaledBarcode) At(x, y int) color.Color {
|
||||||
|
return bc.wrapperFunc(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *scaledBarcode) CheckSum() int {
|
||||||
|
return bc.wrapped.CheckSum()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale returns a resized barcode with the given width and height.
|
||||||
|
func Scale(bc Barcode, width, height int) (Barcode, error) {
|
||||||
|
switch bc.Metadata().Dimensions {
|
||||||
|
case 1:
|
||||||
|
return scale1DCode(bc, width, height)
|
||||||
|
case 2:
|
||||||
|
return scale2DCode(bc, width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("unsupported barcode format")
|
||||||
|
}
|
||||||
|
|
||||||
|
func scale2DCode(bc Barcode, width, height int) (Barcode, error) {
|
||||||
|
orgBounds := bc.Bounds()
|
||||||
|
orgWidth := orgBounds.Max.X - orgBounds.Min.X
|
||||||
|
orgHeight := orgBounds.Max.Y - orgBounds.Min.Y
|
||||||
|
|
||||||
|
factor := int(math.Min(float64(width)/float64(orgWidth), float64(height)/float64(orgHeight)))
|
||||||
|
if factor <= 0 {
|
||||||
|
return nil, fmt.Errorf("can not scale barcode to an image smaller then %dx%d", orgWidth, orgHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
offsetX := (width - (orgWidth * factor)) / 2
|
||||||
|
offsetY := (height - (orgHeight * factor)) / 2
|
||||||
|
|
||||||
|
wrap := func(x, y int) color.Color {
|
||||||
|
if x < offsetX || y < offsetY {
|
||||||
|
return color.White
|
||||||
|
}
|
||||||
|
x = (x - offsetX) / factor
|
||||||
|
y = (y - offsetY) / factor
|
||||||
|
if x >= orgWidth || y >= orgHeight {
|
||||||
|
return color.White
|
||||||
|
}
|
||||||
|
return bc.At(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &scaledBarcode{
|
||||||
|
bc,
|
||||||
|
wrap,
|
||||||
|
image.Rect(0, 0, width, height),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func scale1DCode(bc Barcode, width, height int) (Barcode, error) {
|
||||||
|
orgBounds := bc.Bounds()
|
||||||
|
orgWidth := orgBounds.Max.X - orgBounds.Min.X
|
||||||
|
factor := int(float64(width) / float64(orgWidth))
|
||||||
|
|
||||||
|
if factor <= 0 {
|
||||||
|
return nil, fmt.Errorf("can not scale barcode to an image smaller then %dx1", orgWidth)
|
||||||
|
}
|
||||||
|
offsetX := (width - (orgWidth * factor)) / 2
|
||||||
|
|
||||||
|
wrap := func(x, y int) color.Color {
|
||||||
|
if x < offsetX {
|
||||||
|
return color.White
|
||||||
|
}
|
||||||
|
x = (x - offsetX) / factor
|
||||||
|
|
||||||
|
if x >= orgWidth {
|
||||||
|
return color.White
|
||||||
|
}
|
||||||
|
return bc.At(x, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &scaledBarcode{
|
||||||
|
bc,
|
||||||
|
wrap,
|
||||||
|
image.Rect(0, 0, width, height),
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
}
|
48
vendor/github.com/boombuler/barcode/utils/base1dcode.go
generated
vendored
Normal file
48
vendor/github.com/boombuler/barcode/utils/base1dcode.go
generated
vendored
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// Package utils contain some utilities which are needed to create barcodes
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/boombuler/barcode"
|
||||||
|
)
|
||||||
|
|
||||||
|
type base1DCode struct {
|
||||||
|
*BitList
|
||||||
|
kind string
|
||||||
|
content string
|
||||||
|
checksum int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *base1DCode) Content() string {
|
||||||
|
return c.content
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *base1DCode) Metadata() barcode.Metadata {
|
||||||
|
return barcode.Metadata{c.kind, 1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *base1DCode) ColorModel() color.Model {
|
||||||
|
return color.Gray16Model
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *base1DCode) Bounds() image.Rectangle {
|
||||||
|
return image.Rect(0, 0, c.Len(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *base1DCode) At(x, y int) color.Color {
|
||||||
|
if c.GetBit(x) {
|
||||||
|
return color.Black
|
||||||
|
}
|
||||||
|
return color.White
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *base1DCode) CheckSum() int {
|
||||||
|
return c.checksum
|
||||||
|
}
|
||||||
|
|
||||||
|
// New1DCode creates a new 1D barcode where the bars are represented by the bits in the bars BitList
|
||||||
|
func New1DCode(codeKind, content string, bars *BitList, checksum int) barcode.Barcode {
|
||||||
|
return &base1DCode{bars, codeKind, content, checksum}
|
||||||
|
}
|
119
vendor/github.com/boombuler/barcode/utils/bitlist.go
generated
vendored
Normal file
119
vendor/github.com/boombuler/barcode/utils/bitlist.go
generated
vendored
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
// BitList is a list that contains bits
|
||||||
|
type BitList struct {
|
||||||
|
count int
|
||||||
|
data []int32
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBitList returns a new BitList with the given length
|
||||||
|
// all bits are initialize with false
|
||||||
|
func NewBitList(capacity int) *BitList {
|
||||||
|
bl := new(BitList)
|
||||||
|
bl.count = capacity
|
||||||
|
x := 0
|
||||||
|
if capacity%32 != 0 {
|
||||||
|
x = 1
|
||||||
|
}
|
||||||
|
bl.data = make([]int32, capacity/32+x)
|
||||||
|
return bl
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of contained bits
|
||||||
|
func (bl *BitList) Len() int {
|
||||||
|
return bl.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bl *BitList) grow() {
|
||||||
|
growBy := len(bl.data)
|
||||||
|
if growBy < 128 {
|
||||||
|
growBy = 128
|
||||||
|
} else if growBy >= 1024 {
|
||||||
|
growBy = 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
nd := make([]int32, len(bl.data)+growBy)
|
||||||
|
copy(nd, bl.data)
|
||||||
|
bl.data = nd
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddBit appends the given bits to the end of the list
|
||||||
|
func (bl *BitList) AddBit(bits ...bool) {
|
||||||
|
for _, bit := range bits {
|
||||||
|
itmIndex := bl.count / 32
|
||||||
|
for itmIndex >= len(bl.data) {
|
||||||
|
bl.grow()
|
||||||
|
}
|
||||||
|
bl.SetBit(bl.count, bit)
|
||||||
|
bl.count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBit sets the bit at the given index to the given value
|
||||||
|
func (bl *BitList) SetBit(index int, value bool) {
|
||||||
|
itmIndex := index / 32
|
||||||
|
itmBitShift := 31 - (index % 32)
|
||||||
|
if value {
|
||||||
|
bl.data[itmIndex] = bl.data[itmIndex] | 1<<uint(itmBitShift)
|
||||||
|
} else {
|
||||||
|
bl.data[itmIndex] = bl.data[itmIndex] & ^(1 << uint(itmBitShift))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBit returns the bit at the given index
|
||||||
|
func (bl *BitList) GetBit(index int) bool {
|
||||||
|
itmIndex := index / 32
|
||||||
|
itmBitShift := 31 - (index % 32)
|
||||||
|
return ((bl.data[itmIndex] >> uint(itmBitShift)) & 1) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddByte appends all 8 bits of the given byte to the end of the list
|
||||||
|
func (bl *BitList) AddByte(b byte) {
|
||||||
|
for i := 7; i >= 0; i-- {
|
||||||
|
bl.AddBit(((b >> uint(i)) & 1) == 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddBits appends the last (LSB) 'count' bits of 'b' the the end of the list
|
||||||
|
func (bl *BitList) AddBits(b int, count byte) {
|
||||||
|
for i := int(count) - 1; i >= 0; i-- {
|
||||||
|
bl.AddBit(((b >> uint(i)) & 1) == 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBytes returns all bits of the BitList as a []byte
|
||||||
|
func (bl *BitList) GetBytes() []byte {
|
||||||
|
len := bl.count >> 3
|
||||||
|
if (bl.count % 8) != 0 {
|
||||||
|
len++
|
||||||
|
}
|
||||||
|
result := make([]byte, len)
|
||||||
|
for i := 0; i < len; i++ {
|
||||||
|
shift := (3 - (i % 4)) * 8
|
||||||
|
result[i] = (byte)((bl.data[i/4] >> uint(shift)) & 0xFF)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// IterateBytes iterates through all bytes contained in the BitList
|
||||||
|
func (bl *BitList) IterateBytes() <-chan byte {
|
||||||
|
res := make(chan byte)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
c := bl.count
|
||||||
|
shift := 24
|
||||||
|
i := 0
|
||||||
|
for c > 0 {
|
||||||
|
res <- byte((bl.data[i] >> uint(shift)) & 0xFF)
|
||||||
|
shift -= 8
|
||||||
|
if shift < 0 {
|
||||||
|
shift = 24
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
c -= 8
|
||||||
|
}
|
||||||
|
close(res)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
65
vendor/github.com/boombuler/barcode/utils/galoisfield.go
generated
vendored
Normal file
65
vendor/github.com/boombuler/barcode/utils/galoisfield.go
generated
vendored
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
// GaloisField encapsulates galois field arithmetics
|
||||||
|
type GaloisField struct {
|
||||||
|
Size int
|
||||||
|
Base int
|
||||||
|
ALogTbl []int
|
||||||
|
LogTbl []int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGaloisField creates a new galois field
|
||||||
|
func NewGaloisField(pp, fieldSize, b int) *GaloisField {
|
||||||
|
result := new(GaloisField)
|
||||||
|
|
||||||
|
result.Size = fieldSize
|
||||||
|
result.Base = b
|
||||||
|
result.ALogTbl = make([]int, fieldSize)
|
||||||
|
result.LogTbl = make([]int, fieldSize)
|
||||||
|
|
||||||
|
x := 1
|
||||||
|
for i := 0; i < fieldSize; i++ {
|
||||||
|
result.ALogTbl[i] = x
|
||||||
|
x = x * 2
|
||||||
|
if x >= fieldSize {
|
||||||
|
x = (x ^ pp) & (fieldSize - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < fieldSize; i++ {
|
||||||
|
result.LogTbl[result.ALogTbl[i]] = int(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gf *GaloisField) Zero() *GFPoly {
|
||||||
|
return NewGFPoly(gf, []int{0})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddOrSub add or substract two numbers
|
||||||
|
func (gf *GaloisField) AddOrSub(a, b int) int {
|
||||||
|
return a ^ b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiply multiplys two numbers
|
||||||
|
func (gf *GaloisField) Multiply(a, b int) int {
|
||||||
|
if a == 0 || b == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return gf.ALogTbl[(gf.LogTbl[a]+gf.LogTbl[b])%(gf.Size-1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Divide divides two numbers
|
||||||
|
func (gf *GaloisField) Divide(a, b int) int {
|
||||||
|
if b == 0 {
|
||||||
|
panic("divide by zero")
|
||||||
|
} else if a == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return gf.ALogTbl[(gf.LogTbl[a]-gf.LogTbl[b])%(gf.Size-1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gf *GaloisField) Invers(num int) int {
|
||||||
|
return gf.ALogTbl[(gf.Size-1)-gf.LogTbl[num]]
|
||||||
|
}
|
103
vendor/github.com/boombuler/barcode/utils/gfpoly.go
generated
vendored
Normal file
103
vendor/github.com/boombuler/barcode/utils/gfpoly.go
generated
vendored
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
type GFPoly struct {
|
||||||
|
gf *GaloisField
|
||||||
|
Coefficients []int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gp *GFPoly) Degree() int {
|
||||||
|
return len(gp.Coefficients) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gp *GFPoly) Zero() bool {
|
||||||
|
return gp.Coefficients[0] == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCoefficient returns the coefficient of x ^ degree
|
||||||
|
func (gp *GFPoly) GetCoefficient(degree int) int {
|
||||||
|
return gp.Coefficients[gp.Degree()-degree]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gp *GFPoly) AddOrSubstract(other *GFPoly) *GFPoly {
|
||||||
|
if gp.Zero() {
|
||||||
|
return other
|
||||||
|
} else if other.Zero() {
|
||||||
|
return gp
|
||||||
|
}
|
||||||
|
smallCoeff := gp.Coefficients
|
||||||
|
largeCoeff := other.Coefficients
|
||||||
|
if len(smallCoeff) > len(largeCoeff) {
|
||||||
|
largeCoeff, smallCoeff = smallCoeff, largeCoeff
|
||||||
|
}
|
||||||
|
sumDiff := make([]int, len(largeCoeff))
|
||||||
|
lenDiff := len(largeCoeff) - len(smallCoeff)
|
||||||
|
copy(sumDiff, largeCoeff[:lenDiff])
|
||||||
|
for i := lenDiff; i < len(largeCoeff); i++ {
|
||||||
|
sumDiff[i] = int(gp.gf.AddOrSub(int(smallCoeff[i-lenDiff]), int(largeCoeff[i])))
|
||||||
|
}
|
||||||
|
return NewGFPoly(gp.gf, sumDiff)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gp *GFPoly) MultByMonominal(degree int, coeff int) *GFPoly {
|
||||||
|
if coeff == 0 {
|
||||||
|
return gp.gf.Zero()
|
||||||
|
}
|
||||||
|
size := len(gp.Coefficients)
|
||||||
|
result := make([]int, size+degree)
|
||||||
|
for i := 0; i < size; i++ {
|
||||||
|
result[i] = int(gp.gf.Multiply(int(gp.Coefficients[i]), int(coeff)))
|
||||||
|
}
|
||||||
|
return NewGFPoly(gp.gf, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gp *GFPoly) Multiply(other *GFPoly) *GFPoly {
|
||||||
|
if gp.Zero() || other.Zero() {
|
||||||
|
return gp.gf.Zero()
|
||||||
|
}
|
||||||
|
aCoeff := gp.Coefficients
|
||||||
|
aLen := len(aCoeff)
|
||||||
|
bCoeff := other.Coefficients
|
||||||
|
bLen := len(bCoeff)
|
||||||
|
product := make([]int, aLen+bLen-1)
|
||||||
|
for i := 0; i < aLen; i++ {
|
||||||
|
ac := int(aCoeff[i])
|
||||||
|
for j := 0; j < bLen; j++ {
|
||||||
|
bc := int(bCoeff[j])
|
||||||
|
product[i+j] = int(gp.gf.AddOrSub(int(product[i+j]), gp.gf.Multiply(ac, bc)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NewGFPoly(gp.gf, product)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gp *GFPoly) Divide(other *GFPoly) (quotient *GFPoly, remainder *GFPoly) {
|
||||||
|
quotient = gp.gf.Zero()
|
||||||
|
remainder = gp
|
||||||
|
fld := gp.gf
|
||||||
|
denomLeadTerm := other.GetCoefficient(other.Degree())
|
||||||
|
inversDenomLeadTerm := fld.Invers(int(denomLeadTerm))
|
||||||
|
for remainder.Degree() >= other.Degree() && !remainder.Zero() {
|
||||||
|
degreeDiff := remainder.Degree() - other.Degree()
|
||||||
|
scale := int(fld.Multiply(int(remainder.GetCoefficient(remainder.Degree())), inversDenomLeadTerm))
|
||||||
|
term := other.MultByMonominal(degreeDiff, scale)
|
||||||
|
itQuot := NewMonominalPoly(fld, degreeDiff, scale)
|
||||||
|
quotient = quotient.AddOrSubstract(itQuot)
|
||||||
|
remainder = remainder.AddOrSubstract(term)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMonominalPoly(field *GaloisField, degree int, coeff int) *GFPoly {
|
||||||
|
if coeff == 0 {
|
||||||
|
return field.Zero()
|
||||||
|
}
|
||||||
|
result := make([]int, degree+1)
|
||||||
|
result[0] = coeff
|
||||||
|
return NewGFPoly(field, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGFPoly(field *GaloisField, coefficients []int) *GFPoly {
|
||||||
|
for len(coefficients) > 1 && coefficients[0] == 0 {
|
||||||
|
coefficients = coefficients[1:]
|
||||||
|
}
|
||||||
|
return &GFPoly{field, coefficients}
|
||||||
|
}
|
44
vendor/github.com/boombuler/barcode/utils/reedsolomon.go
generated
vendored
Normal file
44
vendor/github.com/boombuler/barcode/utils/reedsolomon.go
generated
vendored
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ReedSolomonEncoder struct {
|
||||||
|
gf *GaloisField
|
||||||
|
polynomes []*GFPoly
|
||||||
|
m *sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewReedSolomonEncoder(gf *GaloisField) *ReedSolomonEncoder {
|
||||||
|
return &ReedSolomonEncoder{
|
||||||
|
gf, []*GFPoly{NewGFPoly(gf, []int{1})}, new(sync.Mutex),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs *ReedSolomonEncoder) getPolynomial(degree int) *GFPoly {
|
||||||
|
rs.m.Lock()
|
||||||
|
defer rs.m.Unlock()
|
||||||
|
|
||||||
|
if degree >= len(rs.polynomes) {
|
||||||
|
last := rs.polynomes[len(rs.polynomes)-1]
|
||||||
|
for d := len(rs.polynomes); d <= degree; d++ {
|
||||||
|
next := last.Multiply(NewGFPoly(rs.gf, []int{1, rs.gf.ALogTbl[d-1+rs.gf.Base]}))
|
||||||
|
rs.polynomes = append(rs.polynomes, next)
|
||||||
|
last = next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rs.polynomes[degree]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs *ReedSolomonEncoder) Encode(data []int, eccCount int) []int {
|
||||||
|
generator := rs.getPolynomial(eccCount)
|
||||||
|
info := NewGFPoly(rs.gf, data)
|
||||||
|
info = info.MultByMonominal(eccCount, 1)
|
||||||
|
_, remainder := info.Divide(generator)
|
||||||
|
|
||||||
|
result := make([]int, eccCount)
|
||||||
|
numZero := int(eccCount) - len(remainder.Coefficients)
|
||||||
|
copy(result[numZero:], remainder.Coefficients)
|
||||||
|
return result
|
||||||
|
}
|
19
vendor/github.com/boombuler/barcode/utils/runeint.go
generated
vendored
Normal file
19
vendor/github.com/boombuler/barcode/utils/runeint.go
generated
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
// RuneToInt converts a rune between '0' and '9' to an integer between 0 and 9
|
||||||
|
// If the rune is outside of this range -1 is returned.
|
||||||
|
func RuneToInt(r rune) int {
|
||||||
|
if r >= '0' && r <= '9' {
|
||||||
|
return int(r - '0')
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntToRune converts a digit 0 - 9 to the rune '0' - '9'. If the given int is outside
|
||||||
|
// of this range 'F' is returned!
|
||||||
|
func IntToRune(i int) rune {
|
||||||
|
if i >= 0 && i <= 9 {
|
||||||
|
return rune(i + '0')
|
||||||
|
}
|
||||||
|
return 'F'
|
||||||
|
}
|
202
vendor/github.com/pquerna/otp/LICENSE
generated
vendored
Normal file
202
vendor/github.com/pquerna/otp/LICENSE
generated
vendored
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
5
vendor/github.com/pquerna/otp/NOTICE
generated
vendored
Normal file
5
vendor/github.com/pquerna/otp/NOTICE
generated
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
otp
|
||||||
|
Copyright (c) 2014, Paul Querna
|
||||||
|
|
||||||
|
This product includes software developed by
|
||||||
|
Paul Querna (http://paul.querna.org/).
|
60
vendor/github.com/pquerna/otp/README.md
generated
vendored
Normal file
60
vendor/github.com/pquerna/otp/README.md
generated
vendored
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# otp: One Time Password utilities Go / Golang
|
||||||
|
|
||||||
|
[![GoDoc](https://godoc.org/github.com/pquerna/otp?status.svg)](https://godoc.org/github.com/pquerna/otp) [![Build Status](https://travis-ci.org/pquerna/otp.svg?branch=master)](https://travis-ci.org/pquerna/otp)
|
||||||
|
|
||||||
|
# Why One Time Passwords?
|
||||||
|
|
||||||
|
One Time Passwords (OTPs) are an mechanism to improve security over passwords alone. When a Time-based OTP (TOTP) is stored on a user's phone, and combined with something the user knows (Password), you have an easy on-ramp to [Multi-factor authentication](http://en.wikipedia.org/wiki/Multi-factor_authentication) without adding a dependency on a SMS provider. This Password and TOTP combination is used by many popular websites including Google, Github, Facebook, Salesforce and many others.
|
||||||
|
|
||||||
|
The `otp` library enables you to easily add TOTPs to your own application, increasing your user's security against mass-password breaches and malware.
|
||||||
|
|
||||||
|
Because TOTP is standardized and widely deployed, there are many [mobile clients and software implementations](http://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm#Client_implementations).
|
||||||
|
|
||||||
|
## `otp` Supports:
|
||||||
|
|
||||||
|
* Generating QR Code images for easy user enrollment.
|
||||||
|
* Time-based One-time Password Algorithm (TOTP) (RFC 6238): Time based OTP, the most commonly used method.
|
||||||
|
* HMAC-based One-time Password Algorithm (HOTP) (RFC 4226): Counter based OTP, which TOTP is based upon.
|
||||||
|
* Generation and Validation of codes for either algorithm.
|
||||||
|
|
||||||
|
## Implementing TOTP in your application:
|
||||||
|
|
||||||
|
### User Enrollment
|
||||||
|
|
||||||
|
For an example of a working enrollment work flow, [Github has documented theirs](https://help.github.com/articles/configuring-two-factor-authentication-via-a-totp-mobile-app/
|
||||||
|
), but the basics are:
|
||||||
|
|
||||||
|
1. Generate new TOTP Key for a User. `key,_ := totp.Generate(...)`.
|
||||||
|
1. Display the Key's Secret and QR-Code for the User. `key.Secret()` and `key.Image(...)`.
|
||||||
|
1. Test that the user can successfully use their TOTP. `totp.Validate(...)`.
|
||||||
|
1. Store TOTP Secret for the User in your backend. `key.Secret()`
|
||||||
|
1. Provide the user with "recovery codes". (See Recovery Codes bellow)
|
||||||
|
|
||||||
|
### Code Generation
|
||||||
|
|
||||||
|
* In either TOTP or HOTP cases, use the `GenerateCode` function and a counter or
|
||||||
|
`time.Time` struct to generate a valid code compatible with most implementations.
|
||||||
|
* For uncommon or custom settings, or to catch unlikely errors, use `GenerateCodeCustom`
|
||||||
|
in either module.
|
||||||
|
|
||||||
|
### Validation
|
||||||
|
|
||||||
|
1. Prompt and validate User's password as normal.
|
||||||
|
1. If the user has TOTP enabled, prompt for TOTP passcode.
|
||||||
|
1. Retrieve the User's TOTP Secret from your backend.
|
||||||
|
1. Validate the user's passcode. `totp.Validate(...)`
|
||||||
|
|
||||||
|
|
||||||
|
### Recovery Codes
|
||||||
|
|
||||||
|
When a user loses access to their TOTP device, they would no longer have access to their account. Because TOTPs are often configured on mobile devices that can be lost, stolen or damaged, this is a common problem. For this reason many providers give their users "backup codes" or "recovery codes". These are a set of one time use codes that can be used instead of the TOTP. These can simply be randomly generated strings that you store in your backend. [Github's documentation provides an overview of the user experience](
|
||||||
|
https://help.github.com/articles/downloading-your-two-factor-authentication-recovery-codes/).
|
||||||
|
|
||||||
|
|
||||||
|
## Improvements, bugs, adding feature, etc:
|
||||||
|
|
||||||
|
Please [open issues in Github](https://github.com/pquerna/otp/issues) for ideas, bugs, and general thoughts. Pull requests are of course preferred :)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
`otp` is licensed under the [Apache License, Version 2.0](./LICENSE)
|
70
vendor/github.com/pquerna/otp/doc.go
generated
vendored
Normal file
70
vendor/github.com/pquerna/otp/doc.go
generated
vendored
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2014 Paul Querna
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package otp implements both HOTP and TOTP based
|
||||||
|
// one time passcodes in a Google Authenticator compatible manner.
|
||||||
|
//
|
||||||
|
// When adding a TOTP for a user, you must store the "secret" value
|
||||||
|
// persistently. It is recommend to store the secret in an encrypted field in your
|
||||||
|
// datastore. Due to how TOTP works, it is not possible to store a hash
|
||||||
|
// for the secret value like you would a password.
|
||||||
|
//
|
||||||
|
// To enroll a user, you must first generate an OTP for them. Google
|
||||||
|
// Authenticator supports using a QR code as an enrollment method:
|
||||||
|
//
|
||||||
|
// import (
|
||||||
|
// "github.com/pquerna/otp/totp"
|
||||||
|
//
|
||||||
|
// "bytes"
|
||||||
|
// "image/png"
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// key, err := totp.Generate(totp.GenerateOpts{
|
||||||
|
// Issuer: "Example.com",
|
||||||
|
// AccountName: "alice@example.com",
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// // Convert TOTP key into a QR code encoded as a PNG image.
|
||||||
|
// var buf bytes.Buffer
|
||||||
|
// img, err := key.Image(200, 200)
|
||||||
|
// png.Encode(&buf, img)
|
||||||
|
//
|
||||||
|
// // display the QR code to the user.
|
||||||
|
// display(buf.Bytes())
|
||||||
|
//
|
||||||
|
// // Now Validate that the user's successfully added the passcode.
|
||||||
|
// passcode := promptForPasscode()
|
||||||
|
// valid := totp.Validate(passcode, key.Secret())
|
||||||
|
//
|
||||||
|
// if valid {
|
||||||
|
// // User successfully used their TOTP, save it to your backend!
|
||||||
|
// storeSecret("alice@example.com", key.Secret())
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Validating a TOTP passcode is very easy, just prompt the user for a passcode
|
||||||
|
// and retrieve the associated user's previously stored secret.
|
||||||
|
// import "github.com/pquerna/otp/totp"
|
||||||
|
//
|
||||||
|
// passcode := promptForPasscode()
|
||||||
|
// secret := getSecret("alice@example.com")
|
||||||
|
//
|
||||||
|
// valid := totp.Validate(passcode, secret)
|
||||||
|
//
|
||||||
|
// if valid {
|
||||||
|
// // Success! continue login process.
|
||||||
|
// }
|
||||||
|
package otp
|
181
vendor/github.com/pquerna/otp/hotp/hotp.go
generated
vendored
Normal file
181
vendor/github.com/pquerna/otp/hotp/hotp.go
generated
vendored
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2014 Paul Querna
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package hotp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pquerna/otp"
|
||||||
|
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/base32"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const debug = false
|
||||||
|
|
||||||
|
// Validate a HOTP passcode given a counter and secret.
|
||||||
|
// This is a shortcut for ValidateCustom, with parameters that
|
||||||
|
// are compataible with Google-Authenticator.
|
||||||
|
func Validate(passcode string, counter uint64, secret string) bool {
|
||||||
|
rv, _ := ValidateCustom(
|
||||||
|
passcode,
|
||||||
|
counter,
|
||||||
|
secret,
|
||||||
|
ValidateOpts{
|
||||||
|
Digits: otp.DigitsSix,
|
||||||
|
Algorithm: otp.AlgorithmSHA1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateOpts provides options for ValidateCustom().
|
||||||
|
type ValidateOpts struct {
|
||||||
|
// Digits as part of the input. Defaults to 6.
|
||||||
|
Digits otp.Digits
|
||||||
|
// Algorithm to use for HMAC. Defaults to SHA1.
|
||||||
|
Algorithm otp.Algorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateCode creates a HOTP passcode given a counter and secret.
|
||||||
|
// This is a shortcut for GenerateCodeCustom, with parameters that
|
||||||
|
// are compataible with Google-Authenticator.
|
||||||
|
func GenerateCode(secret string, counter uint64) (string, error) {
|
||||||
|
return GenerateCodeCustom(secret, counter, ValidateOpts{
|
||||||
|
Digits: otp.DigitsSix,
|
||||||
|
Algorithm: otp.AlgorithmSHA1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateCodeCustom uses a counter and secret value and options struct to
|
||||||
|
// create a passcode.
|
||||||
|
func GenerateCodeCustom(secret string, counter uint64, opts ValidateOpts) (passcode string, err error) {
|
||||||
|
secretBytes, err := base32.StdEncoding.DecodeString(secret)
|
||||||
|
if err != nil {
|
||||||
|
return "", otp.ErrValidateSecretInvalidBase32
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, 8)
|
||||||
|
mac := hmac.New(opts.Algorithm.Hash, secretBytes)
|
||||||
|
binary.BigEndian.PutUint64(buf, counter)
|
||||||
|
if debug {
|
||||||
|
fmt.Printf("counter=%v\n", counter)
|
||||||
|
fmt.Printf("buf=%v\n", buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
mac.Write(buf)
|
||||||
|
sum := mac.Sum(nil)
|
||||||
|
|
||||||
|
// "Dynamic truncation" in RFC 4226
|
||||||
|
// http://tools.ietf.org/html/rfc4226#section-5.4
|
||||||
|
offset := sum[len(sum)-1] & 0xf
|
||||||
|
value := int64(((int(sum[offset]) & 0x7f) << 24) |
|
||||||
|
((int(sum[offset+1] & 0xff)) << 16) |
|
||||||
|
((int(sum[offset+2] & 0xff)) << 8) |
|
||||||
|
(int(sum[offset+3]) & 0xff))
|
||||||
|
|
||||||
|
l := opts.Digits.Length()
|
||||||
|
mod := int32(value % int64(math.Pow10(l)))
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
fmt.Printf("offset=%v\n", offset)
|
||||||
|
fmt.Printf("value=%v\n", value)
|
||||||
|
fmt.Printf("mod'ed=%v\n", mod)
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts.Digits.Format(mod), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateCustom validates an HOTP with customizable options. Most users should
|
||||||
|
// use Validate().
|
||||||
|
func ValidateCustom(passcode string, counter uint64, secret string, opts ValidateOpts) (bool, error) {
|
||||||
|
passcode = strings.TrimSpace(passcode)
|
||||||
|
|
||||||
|
if len(passcode) != opts.Digits.Length() {
|
||||||
|
return false, otp.ErrValidateInputInvalidLength
|
||||||
|
}
|
||||||
|
|
||||||
|
otpstr, err := GenerateCodeCustom(secret, counter, opts)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if subtle.ConstantTimeCompare([]byte(otpstr), []byte(passcode)) == 1 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateOpts provides options for .Generate()
|
||||||
|
type GenerateOpts struct {
|
||||||
|
// Name of the issuing Organization/Company.
|
||||||
|
Issuer string
|
||||||
|
// Name of the User's Account (eg, email address)
|
||||||
|
AccountName string
|
||||||
|
// Size in size of the generated Secret. Defaults to 10 bytes.
|
||||||
|
SecretSize uint
|
||||||
|
// Digits to request. Defaults to 6.
|
||||||
|
Digits otp.Digits
|
||||||
|
// Algorithm to use for HMAC. Defaults to SHA1.
|
||||||
|
Algorithm otp.Algorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate creates a new HOTP Key.
|
||||||
|
func Generate(opts GenerateOpts) (*otp.Key, error) {
|
||||||
|
// url encode the Issuer/AccountName
|
||||||
|
if opts.Issuer == "" {
|
||||||
|
return nil, otp.ErrGenerateMissingIssuer
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.AccountName == "" {
|
||||||
|
return nil, otp.ErrGenerateMissingAccountName
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.SecretSize == 0 {
|
||||||
|
opts.SecretSize = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example
|
||||||
|
|
||||||
|
v := url.Values{}
|
||||||
|
secret := make([]byte, opts.SecretSize)
|
||||||
|
_, err := rand.Read(secret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
v.Set("secret", base32.StdEncoding.EncodeToString(secret))
|
||||||
|
v.Set("issuer", opts.Issuer)
|
||||||
|
v.Set("algorithm", opts.Algorithm.String())
|
||||||
|
v.Set("digits", opts.Digits.String())
|
||||||
|
|
||||||
|
u := url.URL{
|
||||||
|
Scheme: "otpauth",
|
||||||
|
Host: "hotp",
|
||||||
|
Path: "/" + opts.Issuer + ":" + opts.AccountName,
|
||||||
|
RawQuery: v.Encode(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return otp.NewKeyFromURL(u.String())
|
||||||
|
}
|
200
vendor/github.com/pquerna/otp/otp.go
generated
vendored
Normal file
200
vendor/github.com/pquerna/otp/otp.go
generated
vendored
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2014 Paul Querna
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package otp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/boombuler/barcode"
|
||||||
|
"github.com/boombuler/barcode/qr"
|
||||||
|
|
||||||
|
"crypto/md5"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/sha512"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"hash"
|
||||||
|
"image"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error when attempting to convert the secret from base32 to raw bytes.
|
||||||
|
var ErrValidateSecretInvalidBase32 = errors.New("Decoding of secret as base32 failed.")
|
||||||
|
|
||||||
|
// The user provided passcode length was not expected.
|
||||||
|
var ErrValidateInputInvalidLength = errors.New("Input length unexpected")
|
||||||
|
|
||||||
|
// When generating a Key, the Issuer must be set.
|
||||||
|
var ErrGenerateMissingIssuer = errors.New("Issuer must be set")
|
||||||
|
|
||||||
|
// When generating a Key, the Account Name must be set.
|
||||||
|
var ErrGenerateMissingAccountName = errors.New("AccountName must be set")
|
||||||
|
|
||||||
|
// Key represents an TOTP or HTOP key.
|
||||||
|
type Key struct {
|
||||||
|
orig string
|
||||||
|
url *url.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewKeyFromURL creates a new Key from an TOTP or HOTP url.
|
||||||
|
//
|
||||||
|
// The URL format is documented here:
|
||||||
|
// https://code.google.com/p/google-authenticator/wiki/KeyUriFormat
|
||||||
|
//
|
||||||
|
func NewKeyFromURL(orig string) (*Key, error) {
|
||||||
|
u, err := url.Parse(orig)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Key{
|
||||||
|
orig: orig,
|
||||||
|
url: u,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Key) String() string {
|
||||||
|
return k.orig
|
||||||
|
}
|
||||||
|
|
||||||
|
// Image returns an QR-Code image of the specified width and height,
|
||||||
|
// suitable for use by many clients like Google-Authenricator
|
||||||
|
// to enroll a user's TOTP/HOTP key.
|
||||||
|
func (k *Key) Image(width int, height int) (image.Image, error) {
|
||||||
|
b, err := qr.Encode(k.orig, qr.M, qr.Auto)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err = barcode.Scale(b, width, height)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns "hotp" or "totp".
|
||||||
|
func (k *Key) Type() string {
|
||||||
|
return k.url.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
// Issuer returns the name of the issuing organization.
|
||||||
|
func (k *Key) Issuer() string {
|
||||||
|
q := k.url.Query()
|
||||||
|
|
||||||
|
issuer := q.Get("issuer")
|
||||||
|
|
||||||
|
if issuer != "" {
|
||||||
|
return issuer
|
||||||
|
}
|
||||||
|
|
||||||
|
p := strings.TrimPrefix(k.url.Path, "/")
|
||||||
|
i := strings.Index(p, ":")
|
||||||
|
|
||||||
|
if i == -1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return p[:i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccountName returns the name of the user's account.
|
||||||
|
func (k *Key) AccountName() string {
|
||||||
|
p := strings.TrimPrefix(k.url.Path, "/")
|
||||||
|
i := strings.Index(p, ":")
|
||||||
|
|
||||||
|
if i == -1 {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
return p[i+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Secret returns the opaque secret for this Key.
|
||||||
|
func (k *Key) Secret() string {
|
||||||
|
q := k.url.Query()
|
||||||
|
|
||||||
|
return q.Get("secret")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Algorithm represents the hashing function to use in the HMAC
|
||||||
|
// operation needed for OTPs.
|
||||||
|
type Algorithm int
|
||||||
|
|
||||||
|
const (
|
||||||
|
AlgorithmSHA1 Algorithm = iota
|
||||||
|
AlgorithmSHA256
|
||||||
|
AlgorithmSHA512
|
||||||
|
AlgorithmMD5
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a Algorithm) String() string {
|
||||||
|
switch a {
|
||||||
|
case AlgorithmSHA1:
|
||||||
|
return "SHA1"
|
||||||
|
case AlgorithmSHA256:
|
||||||
|
return "SHA256"
|
||||||
|
case AlgorithmSHA512:
|
||||||
|
return "SHA512"
|
||||||
|
case AlgorithmMD5:
|
||||||
|
return "MD5"
|
||||||
|
}
|
||||||
|
panic("unreached")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Algorithm) Hash() hash.Hash {
|
||||||
|
switch a {
|
||||||
|
case AlgorithmSHA1:
|
||||||
|
return sha1.New()
|
||||||
|
case AlgorithmSHA256:
|
||||||
|
return sha256.New()
|
||||||
|
case AlgorithmSHA512:
|
||||||
|
return sha512.New()
|
||||||
|
case AlgorithmMD5:
|
||||||
|
return md5.New()
|
||||||
|
}
|
||||||
|
panic("unreached")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Digits represents the number of digits present in the
|
||||||
|
// user's OTP passcode. Six and Eight are the most common values.
|
||||||
|
type Digits int
|
||||||
|
|
||||||
|
const (
|
||||||
|
DigitsSix Digits = 6
|
||||||
|
DigitsEight Digits = 8
|
||||||
|
)
|
||||||
|
|
||||||
|
// Format converts an integer into the zero-filled size for this Digits.
|
||||||
|
func (d Digits) Format(in int32) string {
|
||||||
|
f := fmt.Sprintf("%%0%dd", d)
|
||||||
|
return fmt.Sprintf(f, in)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Length returns the number of characters for this Digits.
|
||||||
|
func (d Digits) Length() int {
|
||||||
|
return int(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Digits) String() string {
|
||||||
|
return fmt.Sprintf("%d", d)
|
||||||
|
}
|
191
vendor/github.com/pquerna/otp/totp/totp.go
generated
vendored
Normal file
191
vendor/github.com/pquerna/otp/totp/totp.go
generated
vendored
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2014 Paul Querna
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package totp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pquerna/otp"
|
||||||
|
"github.com/pquerna/otp/hotp"
|
||||||
|
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base32"
|
||||||
|
"math"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validate a TOTP using the current time.
|
||||||
|
// A shortcut for ValidateCustom, Validate uses a configuration
|
||||||
|
// that is compatible with Google-Authenticator and most clients.
|
||||||
|
func Validate(passcode string, secret string) bool {
|
||||||
|
rv, _ := ValidateCustom(
|
||||||
|
passcode,
|
||||||
|
secret,
|
||||||
|
time.Now().UTC(),
|
||||||
|
ValidateOpts{
|
||||||
|
Period: 30,
|
||||||
|
Skew: 1,
|
||||||
|
Digits: otp.DigitsSix,
|
||||||
|
Algorithm: otp.AlgorithmSHA1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateCode creates a TOTP token using the current time.
|
||||||
|
// A shortcut for GenerateCodeCustom, GenerateCode uses a configuration
|
||||||
|
// that is compatible with Google-Authenticator and most clients.
|
||||||
|
func GenerateCode(secret string, t time.Time) (string, error) {
|
||||||
|
return GenerateCodeCustom(secret, t, ValidateOpts{
|
||||||
|
Period: 30,
|
||||||
|
Skew: 1,
|
||||||
|
Digits: otp.DigitsSix,
|
||||||
|
Algorithm: otp.AlgorithmSHA1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateOpts provides options for ValidateCustom().
|
||||||
|
type ValidateOpts struct {
|
||||||
|
// Number of seconds a TOTP hash is valid for. Defaults to 30 seconds.
|
||||||
|
Period uint
|
||||||
|
// Periods before or after the current time to allow. Value of 1 allows up to Period
|
||||||
|
// of either side of the specified time. Defaults to 0 allowed skews. Values greater
|
||||||
|
// than 1 are likely sketchy.
|
||||||
|
Skew uint
|
||||||
|
// Digits as part of the input. Defaults to 6.
|
||||||
|
Digits otp.Digits
|
||||||
|
// Algorithm to use for HMAC. Defaults to SHA1.
|
||||||
|
Algorithm otp.Algorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateCodeCustom takes a timepoint and produces a passcode using a
|
||||||
|
// secret and the provided opts. (Under the hood, this is making an adapted
|
||||||
|
// call to hotp.GenerateCodeCustom)
|
||||||
|
func GenerateCodeCustom(secret string, t time.Time, opts ValidateOpts) (passcode string, err error) {
|
||||||
|
if opts.Period == 0 {
|
||||||
|
opts.Period = 30
|
||||||
|
}
|
||||||
|
counter := uint64(math.Floor(float64(t.Unix()) / float64(opts.Period)))
|
||||||
|
passcode, err = hotp.GenerateCodeCustom(secret, counter, hotp.ValidateOpts{
|
||||||
|
Digits: opts.Digits,
|
||||||
|
Algorithm: opts.Algorithm,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return passcode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateCustom validates a TOTP given a user specified time and custom options.
|
||||||
|
// Most users should use Validate() to provide an interpolatable TOTP experience.
|
||||||
|
func ValidateCustom(passcode string, secret string, t time.Time, opts ValidateOpts) (bool, error) {
|
||||||
|
if opts.Period == 0 {
|
||||||
|
opts.Period = 30
|
||||||
|
}
|
||||||
|
|
||||||
|
counters := []uint64{}
|
||||||
|
counter := int64(math.Floor(float64(t.Unix()) / float64(opts.Period)))
|
||||||
|
|
||||||
|
counters = append(counters, uint64(counter))
|
||||||
|
for i := 1; i <= int(opts.Skew); i++ {
|
||||||
|
counters = append(counters, uint64(counter+int64(i)))
|
||||||
|
counters = append(counters, uint64(counter-int64(i)))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, counter := range counters {
|
||||||
|
rv, err := hotp.ValidateCustom(passcode, counter, secret, hotp.ValidateOpts{
|
||||||
|
Digits: opts.Digits,
|
||||||
|
Algorithm: opts.Algorithm,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rv == true {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateOpts provides options for Generate(). The default values
|
||||||
|
// are compatible with Google-Authenticator.
|
||||||
|
type GenerateOpts struct {
|
||||||
|
// Name of the issuing Organization/Company.
|
||||||
|
Issuer string
|
||||||
|
// Name of the User's Account (eg, email address)
|
||||||
|
AccountName string
|
||||||
|
// Number of seconds a TOTP hash is valid for. Defaults to 30 seconds.
|
||||||
|
Period uint
|
||||||
|
// Size in size of the generated Secret. Defaults to 10 bytes.
|
||||||
|
SecretSize uint
|
||||||
|
// Digits to request. Defaults to 6.
|
||||||
|
Digits otp.Digits
|
||||||
|
// Algorithm to use for HMAC. Defaults to SHA1.
|
||||||
|
Algorithm otp.Algorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a new TOTP Key.
|
||||||
|
func Generate(opts GenerateOpts) (*otp.Key, error) {
|
||||||
|
// url encode the Issuer/AccountName
|
||||||
|
if opts.Issuer == "" {
|
||||||
|
return nil, otp.ErrGenerateMissingIssuer
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.AccountName == "" {
|
||||||
|
return nil, otp.ErrGenerateMissingAccountName
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Period == 0 {
|
||||||
|
opts.Period = 30
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.SecretSize == 0 {
|
||||||
|
opts.SecretSize = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Digits == 0 {
|
||||||
|
opts.Digits = otp.DigitsSix
|
||||||
|
}
|
||||||
|
|
||||||
|
// otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example
|
||||||
|
|
||||||
|
v := url.Values{}
|
||||||
|
secret := make([]byte, opts.SecretSize)
|
||||||
|
_, err := rand.Read(secret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
v.Set("secret", base32.StdEncoding.EncodeToString(secret))
|
||||||
|
v.Set("issuer", opts.Issuer)
|
||||||
|
v.Set("period", strconv.FormatUint(uint64(opts.Period), 10))
|
||||||
|
v.Set("algorithm", opts.Algorithm.String())
|
||||||
|
v.Set("digits", opts.Digits.String())
|
||||||
|
|
||||||
|
u := url.URL{
|
||||||
|
Scheme: "otpauth",
|
||||||
|
Host: "totp",
|
||||||
|
Path: "/" + opts.Issuer + ":" + opts.AccountName,
|
||||||
|
RawQuery: v.Encode(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return otp.NewKeyFromURL(u.String())
|
||||||
|
}
|
36
vendor/vendor.json
vendored
36
vendor/vendor.json
vendored
@ -50,6 +50,24 @@
|
|||||||
"revision": "831b652a7f8dbefaf94da0eb66abd46c0c4bcf23",
|
"revision": "831b652a7f8dbefaf94da0eb66abd46c0c4bcf23",
|
||||||
"revisionTime": "2016-03-26T03:18:27Z"
|
"revisionTime": "2016-03-26T03:18:27Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "NClRfzxXDSt/g4lM5BIkKhYRVoQ=",
|
||||||
|
"path": "github.com/boombuler/barcode",
|
||||||
|
"revision": "fe0f26ff6d26693948ee8189aa064ee8c54141fa",
|
||||||
|
"revisionTime": "2016-12-26T21:19:16Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "ion36oiOlMelz8OcoyPCHzMQNYY=",
|
||||||
|
"path": "github.com/boombuler/barcode/qr",
|
||||||
|
"revision": "fe0f26ff6d26693948ee8189aa064ee8c54141fa",
|
||||||
|
"revisionTime": "2016-12-26T21:19:16Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "CxHIsJBPkKrTK1pfNM0VV0oow4o=",
|
||||||
|
"path": "github.com/boombuler/barcode/utils",
|
||||||
|
"revision": "fe0f26ff6d26693948ee8189aa064ee8c54141fa",
|
||||||
|
"revisionTime": "2016-12-26T21:19:16Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "fNAC4qgZDqF3kxq74/yyk3PWdy8=",
|
"checksumSHA1": "fNAC4qgZDqF3kxq74/yyk3PWdy8=",
|
||||||
"path": "github.com/bradfitz/gomemcache/memcache",
|
"path": "github.com/bradfitz/gomemcache/memcache",
|
||||||
@ -740,6 +758,24 @@
|
|||||||
"revision": "976c720a22c8eb4eb6a0b4348ad85ad12491a506",
|
"revision": "976c720a22c8eb4eb6a0b4348ad85ad12491a506",
|
||||||
"revisionTime": "2016-09-25T22:06:09Z"
|
"revisionTime": "2016-09-25T22:06:09Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "pcKYSF+UN342M6Y+GSL5QhqKVk0=",
|
||||||
|
"path": "github.com/pquerna/otp",
|
||||||
|
"revision": "54653902c20e47f3417541d35435cb6d6162e28a",
|
||||||
|
"revisionTime": "2016-09-12T16:18:15Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "UL3g+cyh3ufwt0tP0dvwbbjLX+c=",
|
||||||
|
"path": "github.com/pquerna/otp/hotp",
|
||||||
|
"revision": "54653902c20e47f3417541d35435cb6d6162e28a",
|
||||||
|
"revisionTime": "2016-09-12T16:18:15Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "Ie55pTQw1rnOZ8KDekSDXUWDT1I=",
|
||||||
|
"path": "github.com/pquerna/otp/totp",
|
||||||
|
"revision": "54653902c20e47f3417541d35435cb6d6162e28a",
|
||||||
|
"revisionTime": "2016-09-12T16:18:15Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "c7jHQZk5ZEsFR9EXsWJXkszPBZA=",
|
"checksumSHA1": "c7jHQZk5ZEsFR9EXsWJXkszPBZA=",
|
||||||
"path": "github.com/russross/blackfriday",
|
"path": "github.com/russross/blackfriday",
|
||||||
|
Loading…
Reference in New Issue
Block a user