improve (plg_authenticate_htpasswd): support additional algorithm

This commit is contained in:
Mickael Kerjean 2022-11-12 00:50:45 +11:00
parent 3b5bc80565
commit 10daf893cd
33 changed files with 1572 additions and 1231 deletions

2
go.mod
View file

@ -3,7 +3,6 @@ module github.com/mickael-kerjean/filestash
go 1.16
require (
github.com/abbot/go-http-auth v0.4.1-0.20220112235402-e1cee1c72f2f
github.com/aws/aws-sdk-go v1.40.41
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 // indirect
github.com/creack/pty v1.1.18
@ -28,6 +27,7 @@ require (
github.com/stretchr/testify v1.7.0
github.com/tidwall/gjson v1.13.0
github.com/tidwall/sjson v1.0.4
github.com/tredoe/osutil v1.0.6
github.com/wayneashleyberry/terminal-dimensions v1.1.0 // indirect
golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed
golang.org/x/image v0.0.0-20210622092929-e6eecd499c2c

8
go.sum
View file

@ -3,8 +3,6 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/abbot/go-http-auth v0.4.1-0.20220112235402-e1cee1c72f2f h1:R2ZVGCZzU95oXFJxncosHS9LsX8N4/MYUdGGWOb2cFk=
github.com/abbot/go-http-auth v0.4.1-0.20220112235402-e1cee1c72f2f/go.mod h1:l2P3JyHa+fjy5Bxol6y1u2o4DV/mv3QMBdBu2cNR53w=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
@ -143,6 +141,10 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.0.4 h1:UcdIRXff12Lpnu3OLtZvnc03g4vH2suXDXhBwBqmzYg=
github.com/tidwall/sjson v1.0.4/go.mod h1:bURseu1nuBkFpIES5cz6zBtjmYeOQmEESshn7VpF15Y=
github.com/tredoe/fileutil v1.0.5/go.mod h1:HFzzpvg+3Q8LgmZgo1mVF5epHc/CVkWKEb3hja+/1Zo=
github.com/tredoe/goutil v1.0.0/go.mod h1:Qhf75QLcNEChimbl4wb8nROzw9PCFCPYTEUmTnoszXY=
github.com/tredoe/osutil v1.0.6 h1:KJvG9AFmUPLe3hsNKyPMIjNx77CkAJtMKVS4ugAT7vM=
github.com/tredoe/osutil v1.0.6/go.mod h1:zNq93p2DLHJWkHi2/+zi3xOjZl8xxiv3tiI2A6zcB3w=
github.com/wayneashleyberry/terminal-dimensions v1.1.0 h1:EB7cIzBdsOzAgmhTUtTTQXBByuPheP/Zv1zL2BRPY6g=
github.com/wayneashleyberry/terminal-dimensions v1.1.0/go.mod h1:2lc/0eWCObmhRczn2SdGSQtgBooLUzIotkkEGXqghyg=
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
@ -157,7 +159,6 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed h1:YoWVYYAfvQ4ddHv3OKmIvX7NCAhFGTj62VP2l2kfBbA=
golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -181,7 +182,6 @@ golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=

View file

@ -56,7 +56,10 @@ func (this Admin) EntryPoint(idpParams map[string]string, req *http.Request, res
</label>
<button>CONNECT</button>
` + getFlash() + `
<style>.flash{ color: #f26d6d; font-weight: bold; }</style>
<style>
.flash{ color: #f26d6d; font-weight: bold; }
form { padding-top: 10vh; }
</style>
</form>`)))
return nil
}

View file

@ -1,9 +1,16 @@
package plg_authenticate_passthrough
package plg_authenticate_htpasswd
import (
"crypto/sha1"
"crypto/subtle"
"encoding/base64"
"fmt"
auth "github.com/abbot/go-http-auth"
. "github.com/mickael-kerjean/filestash/server/common"
"github.com/tredoe/osutil/user/crypt"
"github.com/tredoe/osutil/user/crypt/apr1_crypt"
"github.com/tredoe/osutil/user/crypt/md5_crypt"
"github.com/tredoe/osutil/user/crypt/sha256_crypt"
"github.com/tredoe/osutil/user/crypt/sha512_crypt"
"net/http"
"strings"
)
@ -23,11 +30,18 @@ func (this Htpasswd) Setup() Form {
Value: "htpasswd",
},
{
Name: "users",
Type: "long_text",
Placeholder: "test:$apr1$nEDlyMK/$4jL0BUAuEifz2VajdjVnE.\ntest:{SHA}qUqP5cyxm6YcTAhz05Hph5gvu9M=",
Default: "",
Description: `The list of users who are granted access using the htpasswd file format.
Name: "users",
Type: "long_text",
Placeholder: `test1:$apr1$ZiAIyyhS$ovyMo9eJRgDF/luvmAigP0
test2:{SHA}EJ9LPFDXsN9ynSmbxvjp75Bmlx8=
test3:$6$ME6DxvSEUjW4Kx/j$vQ5Yh1utmNEr4EZnWH0ZQa6hrG5yu2siybFW10aAax4u611W9awI5V90YWqGs4NjTSHkCrhpdbJoNErW9/Pbh1:19306:0:99999:7:::
test4:$6$wTy86P73X/DsCiQy$El3JVUjepBUO.e.1OTuDt4yL9w2CnzY4jHaIbg1P7p508n8vjzCC8ZNsWa1IlbhciBM8.0LqqXWi3OuhGfPmP.
test5:$5$RkdUxGLHGhmrO0yj$K6bCqmB.OPR7KM4i5eiAG.mxFyhElLNdthSL.dreqN5
test6:$1$vuUKD.37$R6eCPFBa6lKIVfnkABveB1`,
Default: "",
Description: `The list of users who are granted access using either or both the htpasswd file format or the /etc/shadow file format. To generate a password:
'openssl passwd -6' or 'mkpasswd -m SHA-512' or the htpasswd cli tool.
This plugin exposes {{ .user }} and {{ .password }} for the attribute mapping section`,
},
},
@ -59,7 +73,10 @@ func (this Htpasswd) EntryPoint(idpParams map[string]string, req *http.Request,
</label>
<button>CONNECT</button>
` + getFlash() + `
<style>.flash{ color: #f26d6d; font-weight: bold; }</style>
<style>
.flash{ color: #f26d6d; font-weight: bold; }
form { padding-top: 10vh; }
</style>
</form>`)))
return nil
}
@ -76,7 +93,10 @@ func (this Htpasswd) Callback(formData map[string]string, idpParams map[string]s
continue
} else if formData["user"] != pair[0] {
continue
} else if auth.CheckSecret(formData["password"], pair[1]) == false {
} else if verifyPassword(
formData["password"],
strings.SplitN(pair[1], ":", 2)[0], // filter out unwanted fields from hash
) == false {
continue
}
return map[string]string{
@ -92,3 +112,46 @@ func (this Htpasswd) Callback(formData map[string]string, idpParams map[string]s
})
return nil, ErrAuthenticationFailed
}
func verifyPassword(password string, hash string) bool {
if strings.HasPrefix(hash, "{SHA}") {
d := sha1.New()
d.Write([]byte(password))
return subtle.ConstantTimeCompare(
[]byte(strings.TrimPrefix(hash, "{SHA}")),
[]byte(base64.StdEncoding.EncodeToString(d.Sum(nil))),
) == 1
}
var c crypt.Crypter
parts := strings.SplitN(hash, "$", 4)
if len(parts) != 4 {
return false
}
if strings.HasPrefix(hash, "$apr1$") {
c = apr1_crypt.New()
parts[2] = "$apr1$" + parts[2]
} else if strings.HasPrefix(hash, "$6$") {
c = sha512_crypt.New()
parts[2] = "$6$" + parts[2]
} else if strings.HasPrefix(hash, "$5$") {
c = sha256_crypt.New()
parts[2] = "$5$" + parts[2]
} else if strings.HasPrefix(hash, "$1$") {
c = md5_crypt.New()
parts[2] = "$1$" + parts[2]
} else {
// TODO: there are other algorithm available but that's another job
// for another day
return false
}
shadow, err := c.Generate(
[]byte(password),
[]byte(parts[2]),
)
if err != nil {
return false
} else if shadow != hash {
return false
}
return true
}

View file

@ -1,5 +0,0 @@
*~
*.a
*.6
*.out
_testmain.go

View file

@ -1,178 +0,0 @@
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

View file

@ -1,12 +0,0 @@
include $(GOROOT)/src/Make.inc
TARG=auth_digest
GOFILES=\
auth.go\
digest.go\
basic.go\
misc.go\
md5crypt.go\
users.go\
include $(GOROOT)/src/Make.pkg

View file

@ -1,71 +0,0 @@
HTTP Authentication implementation in Go
========================================
This is an implementation of HTTP Basic and HTTP Digest authentication
in Go language. It is designed as a simple wrapper for
http.RequestHandler functions.
Features
--------
* Supports HTTP Basic and HTTP Digest authentication.
* Supports htpasswd and htdigest formatted files.
* Automatic reloading of password files.
* Pluggable interface for user/password storage.
* Supports MD5, SHA1 and BCrypt for Basic authentication password storage.
* Configurable Digest nonce cache size with expiration.
* Wrapper for legacy http handlers (http.HandlerFunc interface)
Example usage
-------------
This is a complete working example for Basic auth:
package main
import (
"fmt"
"net/http"
auth "github.com/abbot/go-http-auth"
)
func Secret(user, realm string) string {
if user == "john" {
// password is "hello"
return "$1$dlPL2MqE$oQmn16q49SqdmhenQuNgs1"
}
return ""
}
func handle(w http.ResponseWriter, r *auth.AuthenticatedRequest) {
fmt.Fprintf(w, "<html><body><h1>Hello, %s!</h1></body></html>", r.Username)
}
func main() {
authenticator := auth.NewBasicAuthenticator("example.com", Secret)
http.HandleFunc("/", authenticator.Wrap(handle))
http.ListenAndServe(":8080", nil)
}
See more examples in the "examples" directory.
Legal
-----
This module is developed under Apache 2.0 license, and can be used for
open and proprietary projects.
Copyright 2012-2013 Lev Shamardin
Licensed under the Apache License, Version 2.0 (the "License"); you
may not use this file or any other part of this project 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.

View file

@ -1,110 +0,0 @@
// Package auth is an implementation of HTTP Basic and HTTP Digest authentication.
package auth
import (
"context"
"net/http"
)
// AuthenticatedRequest is passed to AuthenticatedHandlerFunc instead
// of *http.Request.
type AuthenticatedRequest struct {
http.Request
// Username is the authenticated user name. Current API implies that
// Username is never empty, which means that authentication is
// always done before calling the request handler.
Username string
}
// AuthenticatedHandlerFunc is like http.HandlerFunc, but takes
// AuthenticatedRequest instead of http.Request
type AuthenticatedHandlerFunc func(http.ResponseWriter, *AuthenticatedRequest)
// Authenticator wraps an AuthenticatedHandlerFunc with
// authentication-checking code.
//
// Typical Authenticator usage is something like:
//
// authenticator := SomeAuthenticator(...)
// http.HandleFunc("/", authenticator(my_handler))
//
// Authenticator wrapper checks the user authentication and calls the
// wrapped function only after authentication has succeeded. Otherwise,
// it returns a handler which initiates the authentication procedure.
type Authenticator func(AuthenticatedHandlerFunc) http.HandlerFunc
// Info contains authentication information for the request.
type Info struct {
// Authenticated is set to true when request was authenticated
// successfully, i.e. username and password passed in request did
// pass the check.
Authenticated bool
// Username contains a user name passed in the request when
// Authenticated is true. It's value is undefined if Authenticated
// is false.
Username string
// ResponseHeaders contains extra headers that must be set by server
// when sending back HTTP response.
ResponseHeaders http.Header
}
// UpdateHeaders updates headers with this Info's ResponseHeaders. It is
// safe to call this function on nil Info.
func (i *Info) UpdateHeaders(headers http.Header) {
if i == nil {
return
}
for k, values := range i.ResponseHeaders {
for _, v := range values {
headers.Add(k, v)
}
}
}
type key int // used for context keys
var infoKey key
// AuthenticatorInterface is the interface implemented by BasicAuth
// and DigestAuth authenticators.
//
// Deprecated: this interface is not coherent. New code should define
// and use your own interfaces with a required subset of authenticator
// methods.
type AuthenticatorInterface interface {
// NewContext returns a new context carrying authentication
// information extracted from the request.
NewContext(ctx context.Context, r *http.Request) context.Context
// Wrap returns an http.HandlerFunc which wraps
// AuthenticatedHandlerFunc with this authenticator's
// authentication checks.
Wrap(AuthenticatedHandlerFunc) http.HandlerFunc
}
// FromContext returns authentication information from the context or
// nil if no such information present.
func FromContext(ctx context.Context) *Info {
info, ok := ctx.Value(infoKey).(*Info)
if !ok {
return nil
}
return info
}
// AuthUsernameHeader is the header set by JustCheck functions. It
// contains an authenticated username (if authentication was
// successful).
const AuthUsernameHeader = "X-Authenticated-Username"
// JustCheck returns a new http.HandlerFunc, which requires
// authenticator to successfully authenticate a user before calling
// wrapped http.HandlerFunc.
func JustCheck(auth AuthenticatorInterface, wrapped http.HandlerFunc) http.HandlerFunc {
return auth.Wrap(func(w http.ResponseWriter, ar *AuthenticatedRequest) {
ar.Header.Set(AuthUsernameHeader, ar.Username)
wrapped(w, &ar.Request)
})
}

View file

@ -1,161 +0,0 @@
package auth
import (
"bytes"
"context"
"crypto/sha1"
"crypto/subtle"
"encoding/base64"
"errors"
"net/http"
"strings"
"golang.org/x/crypto/bcrypt"
)
type compareFunc func(hashedPassword, password []byte) error
var (
errMismatchedHashAndPassword = errors.New("mismatched hash and password")
compareFuncs = []struct {
prefix string
compare compareFunc
}{
{"", compareMD5HashAndPassword}, // default compareFunc
{"{SHA}", compareShaHashAndPassword},
// Bcrypt is complicated. According to crypt(3) from
// crypt_blowfish version 1.3 (fetched from
// http://www.openwall.com/crypt/crypt_blowfish-1.3.tar.gz), there
// are three different has prefixes: "$2a$", used by versions up
// to 1.0.4, and "$2x$" and "$2y$", used in all later
// versions. "$2a$" has a known bug, "$2x$" was added as a
// migration path for systems with "$2a$" prefix and still has a
// bug, and only "$2y$" should be used by modern systems. The bug
// has something to do with handling of 8-bit characters. Since
// both "$2a$" and "$2x$" are deprecated, we are handling them the
// same way as "$2y$", which will yield correct results for 7-bit
// character passwords, but is wrong for 8-bit character
// passwords. You have to upgrade to "$2y$" if you want sant 8-bit
// character password support with bcrypt. To add to the mess,
// OpenBSD 5.5. introduced "$2b$" prefix, which behaves exactly
// like "$2y$" according to the same source.
{"$2a$", bcrypt.CompareHashAndPassword},
{"$2b$", bcrypt.CompareHashAndPassword},
{"$2x$", bcrypt.CompareHashAndPassword},
{"$2y$", bcrypt.CompareHashAndPassword},
}
)
// BasicAuth is an authenticator implementation for 'Basic' HTTP
// Authentication scheme (RFC 7617).
type BasicAuth struct {
Realm string
Secrets SecretProvider
// Headers used by authenticator. Set to ProxyHeaders to use with
// proxy server. When nil, NormalHeaders are used.
Headers *Headers
}
// check that BasicAuth implements AuthenticatorInterface
var _ = (AuthenticatorInterface)((*BasicAuth)(nil))
// CheckAuth checks the username/password combination from the
// request. Returns either an empty string (authentication failed) or
// the name of the authenticated user.
func (a *BasicAuth) CheckAuth(r *http.Request) string {
user, password, ok := r.BasicAuth()
if !ok {
return ""
}
secret := a.Secrets(user, a.Realm)
if secret == "" {
return ""
}
if !CheckSecret(password, secret) {
return ""
}
return user
}
// CheckSecret returns true if the password matches the encrypted
// secret.
func CheckSecret(password, secret string) bool {
compare := compareFuncs[0].compare
for _, cmp := range compareFuncs[1:] {
if strings.HasPrefix(secret, cmp.prefix) {
compare = cmp.compare
break
}
}
return compare([]byte(secret), []byte(password)) == nil
}
func compareShaHashAndPassword(hashedPassword, password []byte) error {
d := sha1.New()
d.Write(password)
if subtle.ConstantTimeCompare(hashedPassword[5:], []byte(base64.StdEncoding.EncodeToString(d.Sum(nil)))) != 1 {
return errMismatchedHashAndPassword
}
return nil
}
func compareMD5HashAndPassword(hashedPassword, password []byte) error {
parts := bytes.SplitN(hashedPassword, []byte("$"), 4)
if len(parts) != 4 {
return errMismatchedHashAndPassword
}
magic := []byte("$" + string(parts[1]) + "$")
salt := parts[2]
if subtle.ConstantTimeCompare(hashedPassword, MD5Crypt(password, salt, magic)) != 1 {
return errMismatchedHashAndPassword
}
return nil
}
// RequireAuth is an http.HandlerFunc for BasicAuth which initiates
// the authentication process (or requires reauthentication).
func (a *BasicAuth) RequireAuth(w http.ResponseWriter, r *http.Request) {
w.Header().Set(contentType, a.Headers.V().UnauthContentType)
w.Header().Set(a.Headers.V().Authenticate, `Basic realm="`+a.Realm+`"`)
w.WriteHeader(a.Headers.V().UnauthCode)
w.Write([]byte(a.Headers.V().UnauthResponse))
}
// Wrap returns an http.HandlerFunc, which wraps
// AuthenticatedHandlerFunc with this BasicAuth authenticator's
// authentication checks. Once the request contains valid credentials,
// it calls wrapped AuthenticatedHandlerFunc.
//
// Deprecated: new code should use NewContext instead.
func (a *BasicAuth) Wrap(wrapped AuthenticatedHandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if username := a.CheckAuth(r); username == "" {
a.RequireAuth(w, r)
} else {
ar := &AuthenticatedRequest{Request: *r, Username: username}
wrapped(w, ar)
}
}
}
// NewContext returns a context carrying authentication information for the request.
func (a *BasicAuth) NewContext(ctx context.Context, r *http.Request) context.Context {
info := &Info{Username: a.CheckAuth(r), ResponseHeaders: make(http.Header)}
info.Authenticated = (info.Username != "")
if !info.Authenticated {
info.ResponseHeaders.Set(a.Headers.V().Authenticate, `Basic realm="`+a.Realm+`"`)
}
return context.WithValue(ctx, infoKey, info)
}
// NewBasicAuthenticator returns a BasicAuth initialized with provided
// realm and secrets.
//
// Deprecated: new code should construct BasicAuth values directly.
func NewBasicAuthenticator(realm string, secrets SecretProvider) *BasicAuth {
return &BasicAuth{Realm: realm, Secrets: secrets}
}

View file

@ -1,288 +0,0 @@
package auth
import (
"context"
"crypto/subtle"
"fmt"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"sync"
"time"
)
type digestClient struct {
nc uint64
lastSeen int64
}
// DigestAuth is an authenticator implementation for 'Digest' HTTP Authentication scheme (RFC 7616).
//
// Note: this implementation was written following now deprecated RFC
// 2617, and supports only MD5 algorithm.
//
// TODO: Add support for SHA-256 and SHA-512/256 algorithms.
type DigestAuth struct {
Realm string
Opaque string
Secrets SecretProvider
PlainTextSecrets bool
IgnoreNonceCount bool
// Headers used by authenticator. Set to ProxyHeaders to use with
// proxy server. When nil, NormalHeaders are used.
Headers *Headers
/*
Approximate size of Client's Cache. When actual number of
tracked client nonces exceeds
ClientCacheSize+ClientCacheTolerance, ClientCacheTolerance*2
older entries are purged.
*/
ClientCacheSize int
ClientCacheTolerance int
clients map[string]*digestClient
mutex sync.RWMutex
}
// check that DigestAuth implements AuthenticatorInterface
var _ = (AuthenticatorInterface)((*DigestAuth)(nil))
type digestCacheEntry struct {
nonce string
lastSeen int64
}
type digestCache []digestCacheEntry
func (c digestCache) Less(i, j int) bool {
return c[i].lastSeen < c[j].lastSeen
}
func (c digestCache) Len() int {
return len(c)
}
func (c digestCache) Swap(i, j int) {
c[i], c[j] = c[j], c[i]
}
// Purge removes count oldest entries from DigestAuth.clients
func (da *DigestAuth) Purge(count int) {
da.mutex.Lock()
da.purgeLocked(count)
da.mutex.Unlock()
}
func (da *DigestAuth) purgeLocked(count int) {
entries := make([]digestCacheEntry, 0, len(da.clients))
for nonce, client := range da.clients {
entries = append(entries, digestCacheEntry{nonce, client.lastSeen})
}
cache := digestCache(entries)
sort.Sort(cache)
for _, client := range cache[:count] {
delete(da.clients, client.nonce)
}
}
// RequireAuth is an http.HandlerFunc which initiates the
// authentication process (or requires reauthentication).
func (da *DigestAuth) RequireAuth(w http.ResponseWriter, r *http.Request) {
da.mutex.RLock()
clientsLen := len(da.clients)
da.mutex.RUnlock()
if clientsLen > da.ClientCacheSize+da.ClientCacheTolerance {
da.Purge(da.ClientCacheTolerance * 2)
}
nonce := RandomKey()
da.mutex.Lock()
da.clients[nonce] = &digestClient{nc: 0, lastSeen: time.Now().UnixNano()}
da.mutex.Unlock()
da.mutex.RLock()
w.Header().Set(contentType, da.Headers.V().UnauthContentType)
w.Header().Set(da.Headers.V().Authenticate,
fmt.Sprintf(`Digest realm="%s", nonce="%s", opaque="%s", algorithm=MD5, qop="auth"`,
da.Realm, nonce, da.Opaque))
w.WriteHeader(da.Headers.V().UnauthCode)
w.Write([]byte(da.Headers.V().UnauthResponse))
da.mutex.RUnlock()
}
// DigestAuthParams parses Authorization header from the
// http.Request. Returns a map of auth parameters or nil if the header
// is not a valid parsable Digest auth header.
func DigestAuthParams(authorization string) map[string]string {
s := strings.SplitN(authorization, " ", 2)
if len(s) != 2 || s[0] != "Digest" {
return nil
}
return ParsePairs(s[1])
}
// CheckAuth checks whether the request contains valid authentication
// data. Returns a pair of username, authinfo, where username is the
// name of the authenticated user or an empty string and authinfo is
// the contents for the optional Authentication-Info response header.
func (da *DigestAuth) CheckAuth(r *http.Request) (username string, authinfo *string) {
da.mutex.RLock()
defer da.mutex.RUnlock()
username = ""
authinfo = nil
auth := DigestAuthParams(r.Header.Get(da.Headers.V().Authorization))
if auth == nil {
return "", nil
}
// RFC2617 Section 3.2.1 specifies that unset value of algorithm in
// WWW-Authenticate Response header should be treated as
// "MD5". According to section 3.2.2 the "algorithm" value in
// subsequent Request Authorization header must be set to whatever
// was supplied in the WWW-Authenticate Response header. This
// implementation always returns an algorithm in WWW-Authenticate
// header, however there seems to be broken clients in the wild
// which do not set the algorithm. Assume the unset algorithm in
// Authorization header to be equal to MD5.
if _, ok := auth["algorithm"]; !ok {
auth["algorithm"] = "MD5"
}
if da.Opaque != auth["opaque"] || auth["algorithm"] != "MD5" || auth["qop"] != "auth" {
return "", nil
}
// Check if the requested URI matches auth header
if r.RequestURI != auth["uri"] {
// We allow auth["uri"] to be a full path prefix of request-uri
// for some reason lost in history, which is probably wrong, but
// used to be like that for quite some time
// (https://tools.ietf.org/html/rfc2617#section-3.2.2 explicitly
// says that auth["uri"] is the request-uri).
//
// TODO: make an option to allow only strict checking.
switch u, err := url.Parse(auth["uri"]); {
case err != nil:
return "", nil
case r.URL == nil:
return "", nil
case len(u.Path) > len(r.URL.Path):
return "", nil
case !strings.HasPrefix(r.URL.Path, u.Path):
return "", nil
}
}
HA1 := da.Secrets(auth["username"], da.Realm)
if da.PlainTextSecrets {
HA1 = H(auth["username"] + ":" + da.Realm + ":" + HA1)
}
HA2 := H(r.Method + ":" + auth["uri"])
KD := H(strings.Join([]string{HA1, auth["nonce"], auth["nc"], auth["cnonce"], auth["qop"], HA2}, ":"))
if subtle.ConstantTimeCompare([]byte(KD), []byte(auth["response"])) != 1 {
return "", nil
}
// At this point crypto checks are completed and validated.
// Now check if the session is valid.
nc, err := strconv.ParseUint(auth["nc"], 16, 64)
if err != nil {
return "", nil
}
client, ok := da.clients[auth["nonce"]]
if !ok {
return "", nil
}
if client.nc != 0 && client.nc >= nc && !da.IgnoreNonceCount {
return "", nil
}
client.nc = nc
client.lastSeen = time.Now().UnixNano()
respHA2 := H(":" + auth["uri"])
rspauth := H(strings.Join([]string{HA1, auth["nonce"], auth["nc"], auth["cnonce"], auth["qop"], respHA2}, ":"))
info := fmt.Sprintf(`qop="auth", rspauth="%s", cnonce="%s", nc="%s"`, rspauth, auth["cnonce"], auth["nc"])
return auth["username"], &info
}
// Default values for ClientCacheSize and ClientCacheTolerance for DigestAuth
const (
DefaultClientCacheSize = 1000
DefaultClientCacheTolerance = 100
)
// Wrap returns an http.HandlerFunc wraps AuthenticatedHandlerFunc
// with this DigestAuth authentication checks. Once the request
// contains valid credentials, it calls wrapped
// AuthenticatedHandlerFunc.
//
// Deprecated: new code should use NewContext instead.
func (da *DigestAuth) Wrap(wrapped AuthenticatedHandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if username, authinfo := da.CheckAuth(r); username == "" {
da.RequireAuth(w, r)
} else {
ar := &AuthenticatedRequest{Request: *r, Username: username}
if authinfo != nil {
w.Header().Set(da.Headers.V().AuthInfo, *authinfo)
}
wrapped(w, ar)
}
}
}
// JustCheck returns a new http.HandlerFunc, which requires
// DigestAuth to successfully authenticate a user before calling
// wrapped http.HandlerFunc.
//
// Authenticated Username is passed as an extra
// X-Authenticated-Username header to the wrapped HandlerFunc.
func (da *DigestAuth) JustCheck(wrapped http.HandlerFunc) http.HandlerFunc {
return da.Wrap(func(w http.ResponseWriter, ar *AuthenticatedRequest) {
ar.Header.Set(AuthUsernameHeader, ar.Username)
wrapped(w, &ar.Request)
})
}
// NewContext returns a context carrying authentication information for the request.
func (da *DigestAuth) NewContext(ctx context.Context, r *http.Request) context.Context {
username, authinfo := da.CheckAuth(r)
da.mutex.Lock()
defer da.mutex.Unlock()
info := &Info{Username: username, ResponseHeaders: make(http.Header)}
if username != "" {
info.Authenticated = true
info.ResponseHeaders.Set(da.Headers.V().AuthInfo, *authinfo)
} else {
// return back digest WWW-Authenticate header
if len(da.clients) > da.ClientCacheSize+da.ClientCacheTolerance {
da.purgeLocked(da.ClientCacheTolerance * 2)
}
nonce := RandomKey()
da.clients[nonce] = &digestClient{nc: 0, lastSeen: time.Now().UnixNano()}
info.ResponseHeaders.Set(da.Headers.V().Authenticate,
fmt.Sprintf(`Digest realm="%s", nonce="%s", opaque="%s", algorithm=MD5, qop="auth"`,
da.Realm, nonce, da.Opaque))
}
return context.WithValue(ctx, infoKey, info)
}
// NewDigestAuthenticator generates a new DigestAuth object
func NewDigestAuthenticator(realm string, secrets SecretProvider) *DigestAuth {
da := &DigestAuth{
Opaque: RandomKey(),
Realm: realm,
Secrets: secrets,
PlainTextSecrets: false,
ClientCacheSize: DefaultClientCacheSize,
ClientCacheTolerance: DefaultClientCacheTolerance,
clients: map[string]*digestClient{}}
return da
}

View file

@ -1,5 +0,0 @@
module github.com/abbot/go-http-auth
go 1.14
require golang.org/x/crypto v0.0.0-20210921155107-089bfa567519

View file

@ -1,8 +0,0 @@
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View file

@ -1,73 +0,0 @@
package auth
import "crypto/md5"
const itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
var md5CryptSwaps = [16]int{12, 6, 0, 13, 7, 1, 14, 8, 2, 15, 9, 3, 5, 10, 4, 11}
// MD5Crypt is the MD5 password crypt implementation.
func MD5Crypt(password, salt, magic []byte) []byte {
d := md5.New()
d.Write(password)
d.Write(magic)
d.Write(salt)
d2 := md5.New()
d2.Write(password)
d2.Write(salt)
d2.Write(password)
for i, mixin := 0, d2.Sum(nil); i < len(password); i++ {
d.Write([]byte{mixin[i%16]})
}
for i := len(password); i != 0; i >>= 1 {
if i&1 == 0 {
d.Write([]byte{password[0]})
} else {
d.Write([]byte{0})
}
}
final := d.Sum(nil)
for i := 0; i < 1000; i++ {
d2 := md5.New()
if i&1 == 0 {
d2.Write(final)
} else {
d2.Write(password)
}
if i%3 != 0 {
d2.Write(salt)
}
if i%7 != 0 {
d2.Write(password)
}
if i&1 == 0 {
d2.Write(password)
} else {
d2.Write(final)
}
final = d2.Sum(nil)
}
result := make([]byte, 0, 22)
v := uint(0)
bits := uint(0)
for _, i := range md5CryptSwaps {
v |= (uint(final[i]) << bits)
for bits = bits + 8; bits > 6; bits -= 6 {
result = append(result, itoa64[v&0x3f])
v >>= 6
}
}
result = append(result, itoa64[v&0x3f])
return append(append(append(magic, salt...), '$'), result...)
}

View file

@ -1,146 +0,0 @@
package auth
import (
"bytes"
"crypto/md5"
"crypto/rand"
"encoding/base64"
"fmt"
"net/http"
"strings"
)
// RandomKey returns a random 16-byte base64 alphabet string
func RandomKey() string {
k := make([]byte, 12)
for bytes := 0; bytes < len(k); {
n, err := rand.Read(k[bytes:])
if err != nil {
panic("rand.Read() failed")
}
bytes += n
}
return base64.StdEncoding.EncodeToString(k)
}
// H function for MD5 algorithm (returns a lower-case hex MD5 digest)
func H(data string) string {
digest := md5.New()
digest.Write([]byte(data))
return fmt.Sprintf("%x", digest.Sum(nil))
}
// ParseList parses a comma-separated list of values as described by
// RFC 2068 and returns list elements.
//
// Lifted from https://code.google.com/p/gorilla/source/browse/http/parser/parser.go
// which was ported from urllib2.parse_http_list, from the Python
// standard library.
func ParseList(value string) []string {
var list []string
var escape, quote bool
b := new(bytes.Buffer)
for _, r := range value {
switch {
case escape:
b.WriteRune(r)
escape = false
case quote:
if r == '\\' {
escape = true
} else {
if r == '"' {
quote = false
}
b.WriteRune(r)
}
case r == ',':
list = append(list, strings.TrimSpace(b.String()))
b.Reset()
case r == '"':
quote = true
b.WriteRune(r)
default:
b.WriteRune(r)
}
}
// Append last part.
if s := b.String(); s != "" {
list = append(list, strings.TrimSpace(s))
}
return list
}
// ParsePairs extracts key/value pairs from a comma-separated list of
// values as described by RFC 2068 and returns a map[key]value. The
// resulting values are unquoted. If a list element doesn't contain a
// "=", the key is the element itself and the value is an empty
// string.
//
// Lifted from https://code.google.com/p/gorilla/source/browse/http/parser/parser.go
func ParsePairs(value string) map[string]string {
m := make(map[string]string)
for _, pair := range ParseList(strings.TrimSpace(value)) {
switch i := strings.Index(pair, "="); {
case i < 0:
// No '=' in pair, treat whole string as a 'key'.
m[pair] = ""
case i == len(pair)-1:
// Malformed pair ('key=' with no value), keep key with empty value.
m[pair[:i]] = ""
default:
v := pair[i+1:]
if v[0] == '"' && v[len(v)-1] == '"' {
// Unquote it.
v = v[1 : len(v)-1]
}
m[pair[:i]] = v
}
}
return m
}
// Headers contains header and error codes used by authenticator.
type Headers struct {
Authenticate string // WWW-Authenticate
Authorization string // Authorization
AuthInfo string // Authentication-Info
UnauthCode int // 401
UnauthContentType string // text/plain
UnauthResponse string // Unauthorized.
}
// V returns NormalHeaders when h is nil, or h otherwise. Allows to
// use uninitialized *Headers values in structs.
func (h *Headers) V() *Headers {
if h == nil {
return NormalHeaders
}
return h
}
var (
// NormalHeaders are the regular Headers used by an HTTP Server for
// request authentication.
NormalHeaders = &Headers{
Authenticate: "WWW-Authenticate",
Authorization: "Authorization",
AuthInfo: "Authentication-Info",
UnauthCode: http.StatusUnauthorized,
UnauthContentType: "text/plain",
UnauthResponse: fmt.Sprintf("%d %s\n", http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized)),
}
// ProxyHeaders are Headers used by an HTTP Proxy server for proxy
// access authentication.
ProxyHeaders = &Headers{
Authenticate: "Proxy-Authenticate",
Authorization: "Proxy-Authorization",
AuthInfo: "Proxy-Authentication-Info",
UnauthCode: http.StatusProxyAuthRequired,
UnauthContentType: "text/plain",
UnauthResponse: fmt.Sprintf("%d %s\n", http.StatusProxyAuthRequired, http.StatusText(http.StatusProxyAuthRequired)),
}
)
const contentType = "Content-Type"

View file

@ -1 +0,0 @@
test:example.com:aa78524fceb0e50fd8ca96dd818b8cf9

View file

@ -1,4 +0,0 @@
test:{SHA}qvTGHdzF6KLavt4PO0gs2a6pQ00=
test2:$apr1$a0j62R97$mYqFkloXH0/UOaUnAiV2b0
test16:$apr1$JI4wh3am$AmhephVqLTUyAVpFQeHZC0
test3:$2y$05$ih3C91zUBSTFcAh2mQnZYuob0UOZVEf16wl/ukgjDhjvj.xgM1WwS

View file

@ -1,151 +0,0 @@
package auth
import (
"encoding/csv"
"os"
"sync"
)
// SecretProvider is used by authenticators. Takes user name and realm
// as an argument, returns secret required for authentication (HA1 for
// digest authentication, properly encrypted password for basic).
//
// Returning an empty string means failing the authentication.
type SecretProvider func(user, realm string) string
// File handles automatic file reloading on changes.
type File struct {
Path string
Info os.FileInfo
/* must be set in inherited types during initialization */
Reload func()
mu sync.Mutex
}
// ReloadIfNeeded checks file Stat and calls Reload() if any changes
// were detected. File mutex is Locked for the duration of Reload()
// call.
//
// This function will panic() if Stat fails.
func (f *File) ReloadIfNeeded() {
info, err := os.Stat(f.Path)
if err != nil {
panic(err)
}
f.mu.Lock()
defer f.mu.Unlock()
if f.Info == nil || f.Info.ModTime() != info.ModTime() {
f.Info = info
f.Reload()
}
}
// HtdigestFile is a File holding htdigest authentication data.
type HtdigestFile struct {
// File is used for automatic reloading of the authentication data.
File
// Users is a map of realms to users to HA1 digests.
Users map[string]map[string]string
mu sync.RWMutex
}
func reloadHTDigest(hf *HtdigestFile) {
r, err := os.Open(hf.Path)
if err != nil {
panic(err)
}
reader := csv.NewReader(r)
reader.Comma = ':'
reader.Comment = '#'
reader.TrimLeadingSpace = true
records, err := reader.ReadAll()
if err != nil {
panic(err)
}
hf.mu.Lock()
defer hf.mu.Unlock()
hf.Users = make(map[string]map[string]string)
for _, record := range records {
_, exists := hf.Users[record[1]]
if !exists {
hf.Users[record[1]] = make(map[string]string)
}
hf.Users[record[1]][record[0]] = record[2]
}
}
// HtdigestFileProvider is a SecretProvider implementation based on
// htdigest-formated files. It will automatically reload htdigest file
// on changes. It panics on syntax errors in htdigest files.
func HtdigestFileProvider(filename string) SecretProvider {
hf := &HtdigestFile{File: File{Path: filename}}
hf.Reload = func() { reloadHTDigest(hf) }
return func(user, realm string) string {
hf.ReloadIfNeeded()
hf.mu.RLock()
defer hf.mu.RUnlock()
_, exists := hf.Users[realm]
if !exists {
return ""
}
digest, exists := hf.Users[realm][user]
if !exists {
return ""
}
return digest
}
}
// HtpasswdFile is a File holding basic authentication data.
type HtpasswdFile struct {
// File is used for automatic reloading of the authentication data.
File
// Users is a map of users to their secrets (salted encrypted
// passwords).
Users map[string]string
mu sync.RWMutex
}
func reloadHTPasswd(h *HtpasswdFile) {
r, err := os.Open(h.Path)
if err != nil {
panic(err)
}
reader := csv.NewReader(r)
reader.Comma = ':'
reader.Comment = '#'
reader.TrimLeadingSpace = true
records, err := reader.ReadAll()
if err != nil {
panic(err)
}
h.mu.Lock()
defer h.mu.Unlock()
h.Users = make(map[string]string)
for _, record := range records {
h.Users[record[0]] = record[1]
}
}
// HtpasswdFileProvider is a SecretProvider implementation based on
// htpasswd-formated files. It will automatically reload htpasswd file
// on changes. It panics on syntax errors in htpasswd files. Realm
// argument of the SecretProvider is ignored.
func HtpasswdFileProvider(filename string) SecretProvider {
h := &HtpasswdFile{File: File{Path: filename}}
h.Reload = func() { reloadHTPasswd(h) }
return func(user, realm string) string {
h.ReloadIfNeeded()
h.mu.RLock()
password, exists := h.Users[user]
h.mu.RUnlock()
if !exists {
return ""
}
return password
}
}

16
vendor/github.com/tredoe/osutil/AUTHORS.md generated vendored Normal file
View file

@ -0,0 +1,16 @@
# Authors
This is the official list of authors for copyright purposes.
This file is distinct from the 'CONTRIBUTORS' file. See the latter for an explanation.
Names should be added to this file as:
Name or Organization <email address> / (url address)
(The email address is not required for organizations)
Please keep the list sorted.
## Code
* Jonas mg (https://github.com/tredoe)

22
vendor/github.com/tredoe/osutil/CONTRIBUTORS.md generated vendored Normal file
View file

@ -0,0 +1,22 @@
# Contributors
This is the official list of people who can contribute (and typically
have contributed) to the repository.
The 'AUTHORS' file lists the copyright holders; this file lists people. For
example, the employees of an organization are listed here but not in 'AUTHORS',
because the organization holds the copyright.
Names should be added to this file as:
Name <email address> / (url address)
Please keep the list sorted.
## Code
* Jonas mg (https://github.com/tredoe)
* DJ Gregor <dj@gregor.com>
* Jeramey Crawford <jeramey@antihe.ro>
* Misha Seltzer (https://github.com/mishas)

374
vendor/github.com/tredoe/osutil/LICENSE-MPL.txt generated vendored Normal file
View file

@ -0,0 +1,374 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

View file

@ -0,0 +1,8 @@
### Initial author
[Jeramey Crawford](https://github.com/jeramey)
### Other authors
[Jonas mg](https://github.com/tredoe)

27
vendor/github.com/tredoe/osutil/user/crypt/LICENSE generated vendored Normal file
View file

@ -0,0 +1,27 @@
Copyright (c) 2012, Jeramey Crawford <jeramey@antihe.ro>
Copyright (c) 2013, Jonas mg
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

25
vendor/github.com/tredoe/osutil/user/crypt/README.md generated vendored Normal file
View file

@ -0,0 +1,25 @@
crypt
=====
A password hashing library.
The goal of crypt is to bring a library of many common and popular password
hashing algorithms to Go and to provide a simple and consistent interface to
each of them. As every hashing method is implemented in pure Go, this library
should be as portable as Go itself.
All hashing methods come with a test suite which verifies their operation
against itself as well as the output of other password hashing implementations
to ensure compatibility with them.
I hope you find this library to be useful and easy to use!
Note: forked from <https://github.com/jeramey/go-pwhash>
## Installation
go get github.com/tredoe/osutil/user/crypt
## License
The source files are distributed under a BSD-style license that can be found
in the LICENSE file.

View file

@ -0,0 +1,62 @@
// Copyright 2012, Jeramey Crawford <jeramey@antihe.ro>
// Copyright 2013, Jonas mg
// All rights reserved.
//
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file.
// Package apr1_crypt implements the standard Unix MD5-crypt algorithm created
// by Poul-Henning Kamp for FreeBSD, and modified by the Apache project.
//
// The only change from MD5-crypt is the use of the magic constant "$apr1$"
// instead of "$1$". The algorithms are otherwise identical.
package apr1_crypt
import (
"github.com/tredoe/osutil/user/crypt"
"github.com/tredoe/osutil/user/crypt/common"
"github.com/tredoe/osutil/user/crypt/md5_crypt"
)
func init() {
crypt.RegisterCrypt(crypt.APR1, New, MagicPrefix)
}
const (
MagicPrefix = "$apr1$"
SaltLenMin = 1
SaltLenMax = 8
RoundsDefault = 1000
)
var md5Crypt = md5_crypt.New()
func init() {
md5Crypt.SetSalt(GetSalt())
}
type crypter struct{ Salt common.Salt }
// New returns a new crypt.Crypter computing the variant "apr1" of MD5-crypt
func New() crypt.Crypter { return &crypter{common.Salt{}} }
func (c *crypter) Generate(key, salt []byte) (string, error) {
return md5Crypt.Generate(key, salt)
}
func (c *crypter) Verify(hashedKey string, key []byte) error {
return md5Crypt.Verify(hashedKey, key)
}
func (c *crypter) Cost(hashedKey string) (int, error) { return RoundsDefault, nil }
func (c *crypter) SetSalt(salt common.Salt) {}
func GetSalt() common.Salt {
return common.Salt{
MagicPrefix: []byte(MagicPrefix),
SaltLenMin: SaltLenMin,
SaltLenMax: SaltLenMax,
RoundsDefault: RoundsDefault,
}
}

View file

@ -0,0 +1,60 @@
// Copyright 2012, Jeramey Crawford <jeramey@antihe.ro>
// Copyright 2013, Jonas mg
// All rights reserved.
//
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file.
package common
const alphabet = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
// Base64_24Bit is a variant of Base64 encoding, commonly used with password
// hashing algorithms to encode the result of their checksum output.
//
// The algorithm operates on up to 3 bytes at a time, encoding the following
// 6-bit sequences into up to 4 hash64 ASCII bytes.
//
// 1. Bottom 6 bits of the first byte
// 2. Top 2 bits of the first byte, and bottom 4 bits of the second byte.
// 3. Top 4 bits of the second byte, and bottom 2 bits of the third byte.
// 4. Top 6 bits of the third byte.
//
// This encoding method does not emit padding bytes as Base64 does.
func Base64_24Bit(src []byte) (hash []byte) {
if len(src) == 0 {
return []byte{} // TODO: return nil
}
hashSize := (len(src) * 8) / 6
if (len(src) % 6) != 0 {
hashSize += 1
}
hash = make([]byte, hashSize)
dst := hash
for len(src) > 0 {
switch len(src) {
default:
dst[0] = alphabet[src[0]&0x3f]
dst[1] = alphabet[((src[0]>>6)|(src[1]<<2))&0x3f]
dst[2] = alphabet[((src[1]>>4)|(src[2]<<4))&0x3f]
dst[3] = alphabet[(src[2]>>2)&0x3f]
src = src[3:]
dst = dst[4:]
case 2:
dst[0] = alphabet[src[0]&0x3f]
dst[1] = alphabet[((src[0]>>6)|(src[1]<<2))&0x3f]
dst[2] = alphabet[(src[1]>>4)&0x3f]
src = src[2:]
dst = dst[3:]
case 1:
dst[0] = alphabet[src[0]&0x3f]
dst[1] = alphabet[(src[0]>>6)&0x3f]
src = src[1:]
dst = dst[2:]
}
}
return
}

View file

@ -0,0 +1,13 @@
// Copyright 2012, Jeramey Crawford <jeramey@antihe.ro>
// Copyright 2013, Jonas mg
// All rights reserved.
//
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file.
// Package common contains routines used by multiple password hashing
// algorithms.
//
// Generally, you will never import this package directly. Many of the
// *_crypt packages will import this package if they require it.
package common

View file

@ -0,0 +1,105 @@
// Copyright 2012, Jeramey Crawford <jeramey@antihe.ro>
// Copyright 2013, Jonas mg
// All rights reserved.
//
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file.
package common
import (
"crypto/rand"
"errors"
"strconv"
)
var (
ErrSaltPrefix = errors.New("invalid magic prefix")
ErrSaltFormat = errors.New("invalid salt format")
ErrSaltRounds = errors.New("invalid rounds")
)
// Salt represents a salt.
type Salt struct {
MagicPrefix []byte
SaltLenMin int
SaltLenMax int
RoundsMin int
RoundsMax int
RoundsDefault int
}
// Generate generates a random salt of a given length.
//
// The length is set thus:
//
// length > SaltLenMax: length = SaltLenMax
// length < SaltLenMin: length = SaltLenMin
func (s *Salt) Generate(length int) []byte {
if length > s.SaltLenMax {
length = s.SaltLenMax
} else if length < s.SaltLenMin {
length = s.SaltLenMin
}
saltLen := (length * 6 / 8)
if (length*6)%8 != 0 {
saltLen++
}
salt := make([]byte, saltLen)
rand.Read(salt)
out := make([]byte, len(s.MagicPrefix)+length)
copy(out, s.MagicPrefix)
copy(out[len(s.MagicPrefix):], Base64_24Bit(salt))
return out
}
// GenerateWRounds creates a random salt with the random bytes being of the
// length provided, and the rounds parameter set as specified.
//
// The parameters are set thus:
//
// length > SaltLenMax: length = SaltLenMax
// length < SaltLenMin: length = SaltLenMin
//
// rounds < 0: rounds = RoundsDefault
// rounds < RoundsMin: rounds = RoundsMin
// rounds > RoundsMax: rounds = RoundsMax
//
// If rounds is equal to RoundsDefault, then the "rounds=" part of the salt is
// removed.
func (s *Salt) GenerateWRounds(length, rounds int) []byte {
if length > s.SaltLenMax {
length = s.SaltLenMax
} else if length < s.SaltLenMin {
length = s.SaltLenMin
}
if rounds < 0 {
rounds = s.RoundsDefault
} else if rounds < s.RoundsMin {
rounds = s.RoundsMin
} else if rounds > s.RoundsMax {
rounds = s.RoundsMax
}
saltLen := (length * 6 / 8)
if (length*6)%8 != 0 {
saltLen++
}
salt := make([]byte, saltLen)
rand.Read(salt)
roundsText := ""
if rounds != s.RoundsDefault {
roundsText = "rounds=" + strconv.Itoa(rounds) + "$"
}
out := make([]byte, len(s.MagicPrefix)+len(roundsText)+length)
copy(out, s.MagicPrefix)
copy(out[len(s.MagicPrefix):], []byte(roundsText))
copy(out[len(s.MagicPrefix)+len(roundsText):], Base64_24Bit(salt))
return out
}

108
vendor/github.com/tredoe/osutil/user/crypt/crypt.go generated vendored Normal file
View file

@ -0,0 +1,108 @@
// Copyright 2013, Jonas mg
// All rights reserved.
//
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file.
// Package crypt provides interface for password crypt functions and collects
// common constants.
package crypt
import (
"errors"
"strings"
"github.com/tredoe/osutil/user/crypt/common"
)
var ErrKeyMismatch = errors.New("hashed value is not the hash of the given password")
// Crypter is the common interface implemented by all crypt functions.
type Crypter interface {
// Generate performs the hashing algorithm, returning a full hash suitable
// for storage and later password verification.
//
// If the salt is empty, a randomly-generated salt will be generated with a
// length of SaltLenMax and number RoundsDefault of rounds.
//
// Any error only can be got when the salt argument is not empty.
Generate(key, salt []byte) (string, error)
// Verify compares a hashed key with its possible key equivalent.
// Returns nil on success, or an error on failure; if the hashed key is
// diffrent, the error is "ErrKeyMismatch".
Verify(hashedKey string, key []byte) error
// Cost returns the hashing cost (in rounds) used to create the given hashed
// key.
//
// When, in the future, the hashing cost of a key needs to be increased in
// order to adjust for greater computational power, this function allows one
// to establish which keys need to be updated.
//
// The algorithms based in MD5-crypt use a fixed value of rounds.
Cost(hashedKey string) (int, error)
// SetSalt sets a different salt. It is used to easily create derivated
// algorithms, i.e. "apr1_crypt" from "md5_crypt".
SetSalt(salt common.Salt)
}
// Crypt identifies a crypt function that is implemented in another package.
type Crypt uint
const (
APR1 Crypt = iota + 1 // import "github.com/tredoe/osutil/user/crypt/apr1_crypt"
MD5 // import "github.com/tredoe/osutil/user/crypt/md5_crypt"
SHA256 // import "github.com/tredoe/osutil/user/crypt/sha256_crypt"
SHA512 // import "github.com/tredoe/osutil/user/crypt/sha512_crypt"
maxCrypt
)
var cryptPrefixes = make([]string, maxCrypt)
var crypts = make([]func() Crypter, maxCrypt)
// RegisterCrypt registers a function that returns a new instance of the given
// crypt function. This is intended to be called from the init function in
// packages that implement crypt functions.
func RegisterCrypt(c Crypt, f func() Crypter, prefix string) {
if c >= maxCrypt {
panic("crypt: RegisterHash of unknown crypt function")
}
crypts[c] = f
cryptPrefixes[c] = prefix
}
// New returns a new crypter.
func New(c Crypt) Crypter {
f := crypts[c]
if f != nil {
return f()
}
panic("crypt: requested crypt function is unavailable")
}
// NewFromHash returns a new Crypter using the prefix in the given hashed key.
func NewFromHash(hashedKey string) Crypter {
var f func() Crypter
if strings.HasPrefix(hashedKey, cryptPrefixes[SHA512]) {
f = crypts[SHA512]
} else if strings.HasPrefix(hashedKey, cryptPrefixes[SHA256]) {
f = crypts[SHA256]
} else if strings.HasPrefix(hashedKey, cryptPrefixes[MD5]) {
f = crypts[MD5]
} else if strings.HasPrefix(hashedKey, cryptPrefixes[APR1]) {
f = crypts[APR1]
} else {
toks := strings.SplitN(hashedKey, "$", 3)
prefix := "$" + toks[1] + "$"
panic("crypt: unknown cryp function from prefix: " + prefix)
}
if f != nil {
return f()
}
panic("crypt: requested cryp function is unavailable")
}

View file

@ -0,0 +1,166 @@
// Copyright 2012, Jeramey Crawford <jeramey@antihe.ro>
// Copyright 2013, Jonas mg
// All rights reserved.
//
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file.
// Package md5_crypt implements the standard Unix MD5-crypt algorithm created by
// Poul-Henning Kamp for FreeBSD.
package md5_crypt
import (
"bytes"
"crypto/md5"
"github.com/tredoe/osutil/user/crypt"
"github.com/tredoe/osutil/user/crypt/common"
)
func init() {
crypt.RegisterCrypt(crypt.MD5, New, MagicPrefix)
}
// NOTE: Cisco IOS only allows salts of length 4.
const (
MagicPrefix = "$1$"
SaltLenMin = 1 // Real minimum is 0, but that isn't useful.
SaltLenMax = 8
RoundsDefault = 1000
)
type crypter struct{ Salt common.Salt }
// New returns a new crypt.Crypter computing the MD5-crypt password hashing.
func New() crypt.Crypter {
return &crypter{GetSalt()}
}
func (c *crypter) Generate(key, salt []byte) (string, error) {
if len(salt) == 0 {
salt = c.Salt.Generate(SaltLenMax)
}
if !bytes.HasPrefix(salt, c.Salt.MagicPrefix) {
return "", common.ErrSaltPrefix
}
saltToks := bytes.Split(salt, []byte{'$'})
if len(saltToks) < 3 {
return "", common.ErrSaltFormat
} else {
salt = saltToks[2]
}
if len(salt) > 8 {
salt = salt[0:8]
}
// Compute alternate MD5 sum with input KEY, SALT, and KEY.
Alternate := md5.New()
Alternate.Write(key)
Alternate.Write(salt)
Alternate.Write(key)
AlternateSum := Alternate.Sum(nil) // 16 bytes
A := md5.New()
A.Write(key)
A.Write(c.Salt.MagicPrefix)
A.Write(salt)
// Add for any character in the key one byte of the alternate sum.
i := len(key)
for ; i > 16; i -= 16 {
A.Write(AlternateSum)
}
A.Write(AlternateSum[0:i])
// The original implementation now does something weird:
// For every 1 bit in the key, the first 0 is added to the buffer
// For every 0 bit, the first character of the key
// This does not seem to be what was intended but we have to follow this to
// be compatible.
for i = len(key); i > 0; i >>= 1 {
if (i & 1) == 0 {
A.Write(key[0:1])
} else {
A.Write([]byte{0})
}
}
Csum := A.Sum(nil)
// In fear of password crackers here comes a quite long loop which just
// processes the output of the previous round again.
// We cannot ignore this here.
for i = 0; i < RoundsDefault; i++ {
C := md5.New()
// Add key or last result.
if (i & 1) != 0 {
C.Write(key)
} else {
C.Write(Csum)
}
// Add salt for numbers not divisible by 3.
if (i % 3) != 0 {
C.Write(salt)
}
// Add key for numbers not divisible by 7.
if (i % 7) != 0 {
C.Write(key)
}
// Add key or last result.
if (i & 1) == 0 {
C.Write(key)
} else {
C.Write(Csum)
}
Csum = C.Sum(nil)
}
out := make([]byte, 0, 23+len(c.Salt.MagicPrefix)+len(salt))
out = append(out, c.Salt.MagicPrefix...)
out = append(out, salt...)
out = append(out, '$')
out = append(out, common.Base64_24Bit([]byte{
Csum[12], Csum[6], Csum[0],
Csum[13], Csum[7], Csum[1],
Csum[14], Csum[8], Csum[2],
Csum[15], Csum[9], Csum[3],
Csum[5], Csum[10], Csum[4],
Csum[11],
})...)
// Clean sensitive data.
A.Reset()
Alternate.Reset()
for i = 0; i < len(AlternateSum); i++ {
AlternateSum[i] = 0
}
return string(out), nil
}
func (c *crypter) Verify(hashedKey string, key []byte) error {
newHash, err := c.Generate(key, []byte(hashedKey))
if err != nil {
return err
}
if newHash != hashedKey {
return crypt.ErrKeyMismatch
}
return nil
}
func (c *crypter) Cost(hashedKey string) (int, error) { return RoundsDefault, nil }
func (c *crypter) SetSalt(salt common.Salt) { c.Salt = salt }
func GetSalt() common.Salt {
return common.Salt{
MagicPrefix: []byte(MagicPrefix),
SaltLenMin: SaltLenMin,
SaltLenMax: SaltLenMax,
RoundsDefault: RoundsDefault,
}
}

View file

@ -0,0 +1,243 @@
// Copyright 2012, Jeramey Crawford <jeramey@antihe.ro>
// Copyright 2013, Jonas mg
// All rights reserved.
//
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file.
// Package sha256_crypt implements Ulrich Drepper's SHA256-crypt password
// hashing algorithm.
//
// The specification for this algorithm can be found here:
// http://www.akkadia.org/drepper/SHA-crypt.txt
package sha256_crypt
import (
"bytes"
"crypto/sha256"
"strconv"
"github.com/tredoe/osutil/user/crypt"
"github.com/tredoe/osutil/user/crypt/common"
)
func init() {
crypt.RegisterCrypt(crypt.SHA256, New, MagicPrefix)
}
const (
MagicPrefix = "$5$"
SaltLenMin = 1
SaltLenMax = 16
RoundsMin = 1000
RoundsMax = 999999999
RoundsDefault = 5000
)
var _rounds = []byte("rounds=")
type crypter struct{ Salt common.Salt }
// New returns a new crypt.Crypter computing the SHA256-crypt password hashing.
func New() crypt.Crypter {
return &crypter{GetSalt()}
}
func (c *crypter) Generate(key, salt []byte) (string, error) {
var rounds int
var isRoundsDef bool
if len(salt) == 0 {
salt = c.Salt.GenerateWRounds(SaltLenMax, RoundsDefault)
}
if !bytes.HasPrefix(salt, c.Salt.MagicPrefix) {
return "", common.ErrSaltPrefix
}
saltToks := bytes.Split(salt, []byte{'$'})
if len(saltToks) < 3 {
return "", common.ErrSaltFormat
}
if bytes.HasPrefix(saltToks[2], _rounds) {
isRoundsDef = true
pr, err := strconv.ParseInt(string(saltToks[2][7:]), 10, 32)
if err != nil {
return "", common.ErrSaltRounds
}
rounds = int(pr)
if rounds < RoundsMin {
rounds = RoundsMin
} else if rounds > RoundsMax {
rounds = RoundsMax
}
salt = saltToks[3]
} else {
rounds = RoundsDefault
salt = saltToks[2]
}
if len(salt) > 16 {
salt = salt[0:16]
}
// Compute alternate SHA256 sum with input KEY, SALT, and KEY.
Alternate := sha256.New()
Alternate.Write(key)
Alternate.Write(salt)
Alternate.Write(key)
AlternateSum := Alternate.Sum(nil) // 32 bytes
A := sha256.New()
A.Write(key)
A.Write(salt)
// Add for any character in the key one byte of the alternate sum.
i := len(key)
for ; i > 32; i -= 32 {
A.Write(AlternateSum)
}
A.Write(AlternateSum[0:i])
// Take the binary representation of the length of the key and for every add
// the alternate sum, for every 0 the key.
for i = len(key); i > 0; i >>= 1 {
if (i & 1) != 0 {
A.Write(AlternateSum)
} else {
A.Write(key)
}
}
Asum := A.Sum(nil)
// Start computation of P byte sequence.
P := sha256.New()
// For every character in the password add the entire password.
for i = 0; i < len(key); i++ {
P.Write(key)
}
Psum := P.Sum(nil)
// Create byte sequence P.
Pseq := make([]byte, 0, len(key))
for i = len(key); i > 32; i -= 32 {
Pseq = append(Pseq, Psum...)
}
Pseq = append(Pseq, Psum[0:i]...)
// Start computation of S byte sequence.
S := sha256.New()
for i = 0; i < (16 + int(Asum[0])); i++ {
S.Write(salt)
}
Ssum := S.Sum(nil)
// Create byte sequence S.
Sseq := make([]byte, 0, len(salt))
for i = len(salt); i > 32; i -= 32 {
Sseq = append(Sseq, Ssum...)
}
Sseq = append(Sseq, Ssum[0:i]...)
Csum := Asum
// Repeatedly run the collected hash value through SHA256 to burn CPU cycles.
for i = 0; i < rounds; i++ {
C := sha256.New()
// Add key or last result.
if (i & 1) != 0 {
C.Write(Pseq)
} else {
C.Write(Csum)
}
// Add salt for numbers not divisible by 3.
if (i % 3) != 0 {
C.Write(Sseq)
}
// Add key for numbers not divisible by 7.
if (i % 7) != 0 {
C.Write(Pseq)
}
// Add key or last result.
if (i & 1) != 0 {
C.Write(Csum)
} else {
C.Write(Pseq)
}
Csum = C.Sum(nil)
}
out := make([]byte, 0, 80)
out = append(out, c.Salt.MagicPrefix...)
if isRoundsDef {
out = append(out, []byte("rounds="+strconv.Itoa(rounds)+"$")...)
}
out = append(out, salt...)
out = append(out, '$')
out = append(out, common.Base64_24Bit([]byte{
Csum[20], Csum[10], Csum[0],
Csum[11], Csum[1], Csum[21],
Csum[2], Csum[22], Csum[12],
Csum[23], Csum[13], Csum[3],
Csum[14], Csum[4], Csum[24],
Csum[5], Csum[25], Csum[15],
Csum[26], Csum[16], Csum[6],
Csum[17], Csum[7], Csum[27],
Csum[8], Csum[28], Csum[18],
Csum[29], Csum[19], Csum[9],
Csum[30], Csum[31],
})...)
// Clean sensitive data.
A.Reset()
Alternate.Reset()
P.Reset()
for i = 0; i < len(Asum); i++ {
Asum[i] = 0
}
for i = 0; i < len(AlternateSum); i++ {
AlternateSum[i] = 0
}
for i = 0; i < len(Pseq); i++ {
Pseq[i] = 0
}
return string(out), nil
}
func (c *crypter) Verify(hashedKey string, key []byte) error {
newHash, err := c.Generate(key, []byte(hashedKey))
if err != nil {
return err
}
if newHash != hashedKey {
return crypt.ErrKeyMismatch
}
return nil
}
func (c *crypter) Cost(hashedKey string) (int, error) {
saltToks := bytes.Split([]byte(hashedKey), []byte{'$'})
if len(saltToks) < 3 {
return 0, common.ErrSaltFormat
}
if !bytes.HasPrefix(saltToks[2], _rounds) {
return RoundsDefault, nil
}
roundToks := bytes.Split(saltToks[2], []byte{'='})
cost, err := strconv.ParseInt(string(roundToks[1]), 10, 0)
return int(cost), err
}
func (c *crypter) SetSalt(salt common.Salt) { c.Salt = salt }
func GetSalt() common.Salt {
return common.Salt{
MagicPrefix: []byte(MagicPrefix),
SaltLenMin: SaltLenMin,
SaltLenMax: SaltLenMax,
RoundsDefault: RoundsDefault,
RoundsMin: RoundsMin,
RoundsMax: RoundsMax,
}
}

View file

@ -0,0 +1,254 @@
// Copyright 2012, Jeramey Crawford <jeramey@antihe.ro>
// Copyright 2013, Jonas mg
// All rights reserved.
//
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file.
// Package sha512_crypt implements Ulrich Drepper's SHA512-crypt password
// hashing algorithm.
//
// The specification for this algorithm can be found here:
// http://www.akkadia.org/drepper/SHA-crypt.txt
package sha512_crypt
import (
"bytes"
"crypto/sha512"
"strconv"
"github.com/tredoe/osutil/user/crypt"
"github.com/tredoe/osutil/user/crypt/common"
)
func init() {
crypt.RegisterCrypt(crypt.SHA512, New, MagicPrefix)
}
const (
MagicPrefix = "$6$"
SaltLenMin = 1
SaltLenMax = 16
RoundsMin = 1000
RoundsMax = 999999999
RoundsDefault = 5000
)
var _rounds = []byte("rounds=")
type crypter struct{ Salt common.Salt }
// New returns a new crypt.Crypter computing the SHA512-crypt password hashing.
func New() crypt.Crypter {
return &crypter{GetSalt()}
}
func (c *crypter) Generate(key, salt []byte) (string, error) {
var rounds int
var isRoundsDef bool
if len(salt) == 0 {
salt = c.Salt.GenerateWRounds(SaltLenMax, RoundsDefault)
}
if !bytes.HasPrefix(salt, c.Salt.MagicPrefix) {
return "", common.ErrSaltPrefix
}
saltToks := bytes.Split(salt, []byte{'$'})
if len(saltToks) < 3 {
return "", common.ErrSaltFormat
}
if bytes.HasPrefix(saltToks[2], _rounds) {
isRoundsDef = true
pr, err := strconv.ParseInt(string(saltToks[2][7:]), 10, 32)
if err != nil {
return "", common.ErrSaltRounds
}
rounds = int(pr)
if rounds < RoundsMin {
rounds = RoundsMin
} else if rounds > RoundsMax {
rounds = RoundsMax
}
salt = saltToks[3]
} else {
rounds = RoundsDefault
salt = saltToks[2]
}
if len(salt) > SaltLenMax {
salt = salt[0:SaltLenMax]
}
// Compute alternate SHA512 sum with input KEY, SALT, and KEY.
Alternate := sha512.New()
Alternate.Write(key)
Alternate.Write(salt)
Alternate.Write(key)
AlternateSum := Alternate.Sum(nil) // 64 bytes
A := sha512.New()
A.Write(key)
A.Write(salt)
// Add for any character in the key one byte of the alternate sum.
i := len(key)
for ; i > 64; i -= 64 {
A.Write(AlternateSum)
}
A.Write(AlternateSum[0:i])
// Take the binary representation of the length of the key and for every add
// the alternate sum, for every 0 the key.
for i = len(key); i > 0; i >>= 1 {
if (i & 1) != 0 {
A.Write(AlternateSum)
} else {
A.Write(key)
}
}
Asum := A.Sum(nil)
// Start computation of P byte sequence.
P := sha512.New()
// For every character in the password add the entire password.
for i = 0; i < len(key); i++ {
P.Write(key)
}
Psum := P.Sum(nil)
// Create byte sequence P.
Pseq := make([]byte, 0, len(key))
for i = len(key); i > 64; i -= 64 {
Pseq = append(Pseq, Psum...)
}
Pseq = append(Pseq, Psum[0:i]...)
// Start computation of S byte sequence.
S := sha512.New()
for i = 0; i < (16 + int(Asum[0])); i++ {
S.Write(salt)
}
Ssum := S.Sum(nil)
// Create byte sequence S.
Sseq := make([]byte, 0, len(salt))
for i = len(salt); i > 64; i -= 64 {
Sseq = append(Sseq, Ssum...)
}
Sseq = append(Sseq, Ssum[0:i]...)
Csum := Asum
// Repeatedly run the collected hash value through SHA512 to burn CPU cycles.
for i = 0; i < rounds; i++ {
C := sha512.New()
// Add key or last result.
if (i & 1) != 0 {
C.Write(Pseq)
} else {
C.Write(Csum)
}
// Add salt for numbers not divisible by 3.
if (i % 3) != 0 {
C.Write(Sseq)
}
// Add key for numbers not divisible by 7.
if (i % 7) != 0 {
C.Write(Pseq)
}
// Add key or last result.
if (i & 1) != 0 {
C.Write(Csum)
} else {
C.Write(Pseq)
}
Csum = C.Sum(nil)
}
out := make([]byte, 0, 123)
out = append(out, c.Salt.MagicPrefix...)
if isRoundsDef {
out = append(out, []byte("rounds="+strconv.Itoa(rounds)+"$")...)
}
out = append(out, salt...)
out = append(out, '$')
out = append(out, common.Base64_24Bit([]byte{
Csum[42], Csum[21], Csum[0],
Csum[1], Csum[43], Csum[22],
Csum[23], Csum[2], Csum[44],
Csum[45], Csum[24], Csum[3],
Csum[4], Csum[46], Csum[25],
Csum[26], Csum[5], Csum[47],
Csum[48], Csum[27], Csum[6],
Csum[7], Csum[49], Csum[28],
Csum[29], Csum[8], Csum[50],
Csum[51], Csum[30], Csum[9],
Csum[10], Csum[52], Csum[31],
Csum[32], Csum[11], Csum[53],
Csum[54], Csum[33], Csum[12],
Csum[13], Csum[55], Csum[34],
Csum[35], Csum[14], Csum[56],
Csum[57], Csum[36], Csum[15],
Csum[16], Csum[58], Csum[37],
Csum[38], Csum[17], Csum[59],
Csum[60], Csum[39], Csum[18],
Csum[19], Csum[61], Csum[40],
Csum[41], Csum[20], Csum[62],
Csum[63],
})...)
// Clean sensitive data.
A.Reset()
Alternate.Reset()
P.Reset()
for i = 0; i < len(Asum); i++ {
Asum[i] = 0
}
for i = 0; i < len(AlternateSum); i++ {
AlternateSum[i] = 0
}
for i = 0; i < len(Pseq); i++ {
Pseq[i] = 0
}
return string(out), nil
}
func (c *crypter) Verify(hashedKey string, key []byte) error {
newHash, err := c.Generate(key, []byte(hashedKey))
if err != nil {
return err
}
if newHash != hashedKey {
return crypt.ErrKeyMismatch
}
return nil
}
func (c *crypter) Cost(hashedKey string) (int, error) {
saltToks := bytes.Split([]byte(hashedKey), []byte{'$'})
if len(saltToks) < 3 {
return 0, common.ErrSaltFormat
}
if !bytes.HasPrefix(saltToks[2], _rounds) {
return RoundsDefault, nil
}
roundToks := bytes.Split(saltToks[2], []byte{'='})
cost, err := strconv.ParseInt(string(roundToks[1]), 10, 0)
return int(cost), err
}
func (c *crypter) SetSalt(salt common.Salt) { c.Salt = salt }
func GetSalt() common.Salt {
return common.Salt{
MagicPrefix: []byte(MagicPrefix),
SaltLenMin: SaltLenMin,
SaltLenMax: SaltLenMax,
RoundsDefault: RoundsDefault,
RoundsMin: RoundsMin,
RoundsMax: RoundsMax,
}
}

11
vendor/modules.txt vendored
View file

@ -1,8 +1,5 @@
# cloud.google.com/go v0.38.0
cloud.google.com/go/compute/metadata
# github.com/abbot/go-http-auth v0.4.1-0.20220112235402-e1cee1c72f2f
## explicit
github.com/abbot/go-http-auth
# github.com/aws/aws-sdk-go v1.40.41
## explicit
github.com/aws/aws-sdk-go/aws
@ -189,6 +186,14 @@ github.com/tidwall/pretty
# github.com/tidwall/sjson v1.0.4
## explicit
github.com/tidwall/sjson
# github.com/tredoe/osutil v1.0.6
## explicit
github.com/tredoe/osutil/user/crypt
github.com/tredoe/osutil/user/crypt/apr1_crypt
github.com/tredoe/osutil/user/crypt/common
github.com/tredoe/osutil/user/crypt/md5_crypt
github.com/tredoe/osutil/user/crypt/sha256_crypt
github.com/tredoe/osutil/user/crypt/sha512_crypt
# github.com/wayneashleyberry/terminal-dimensions v1.1.0
## explicit
github.com/wayneashleyberry/terminal-dimensions