Get native auth working

Former-commit-id: e5345f6717
This commit is contained in:
Owen
2026-05-30 11:53:31 -07:00
parent e3fe89b431
commit b35dd025bd
8 changed files with 119 additions and 235 deletions

View File

@@ -96,6 +96,22 @@ func (g *Gateway) isAllowed(targetType, host string, port int, authToken string)
return false
}
// isTokenValid reports whether the given authToken matches any registered
// target of the specified type. Used for native SSH mode where there is no
// external destination to match against.
func (g *Gateway) isTokenValid(targetType, authToken string) bool {
g.mu.RLock()
defer g.mu.RUnlock()
for _, t := range g.targets {
if t.Type == targetType {
if subtle.ConstantTimeCompare([]byte(authToken), []byte(t.AuthToken)) == 1 {
return true
}
}
}
return false
}
// Start serves the browser gateway HTTP server on the provided listener.
// It returns nil when the listener is closed (normal shutdown).
func (g *Gateway) Start(ln net.Listener) error {

View File

@@ -2,7 +2,6 @@ package browsergateway
import (
"context"
"crypto/subtle"
"encoding/json"
"fmt"
"log"
@@ -12,6 +11,7 @@ import (
"time"
"github.com/coder/websocket"
"github.com/fosrl/newt/logger"
"golang.org/x/crypto/ssh"
)
@@ -36,11 +36,14 @@ type sshServerMsg struct {
// HandleSSH is an http.HandlerFunc for SSH-over-WebSocket connections.
func (g *Gateway) HandleSSH(w http.ResponseWriter, r *http.Request) {
logger.Debug("SSH connection request from %s", r.RemoteAddr)
ctx := r.Context()
token := r.URL.Query().Get("authToken")
var nativeSSH = false
// "mode=native" (default) connects to the local SSH daemon on this host.
// "mode=proxy" connects to an arbitrary host+port supplied in query params.
nativeSSH := r.URL.Query().Get("mode") != "proxy"
// In proxy mode we also need host + username from query params.
var target, username string
@@ -62,8 +65,8 @@ func (g *Gateway) HandleSSH(w http.ResponseWriter, r *http.Request) {
}
target = net.JoinHostPort(host, port)
} else {
// Native SSH mode: validate the gateway token then read the target username.
if subtle.ConstantTimeCompare([]byte(token), []byte(g.authToken)) != 1 {
// Native SSH mode: validate the token against any registered ssh target.
if !g.isTokenValid("ssh", token) {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}

5
go.mod
View File

@@ -23,7 +23,7 @@ require (
golang.org/x/crypto v0.50.0
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6
golang.org/x/net v0.53.0
golang.org/x/sys v0.43.0
golang.org/x/sys v0.45.0
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
golang.zx2c4.com/wireguard/windows v0.5.3
@@ -44,11 +44,14 @@ require (
github.com/docker/go-connections v0.7.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-crypt/crypt v0.14.15 // indirect
github.com/go-crypt/x v0.4.16 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/jsimonetti/pwscheme v0.0.0-20220922140336-67a4d090f150 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect

22
go.sum
View File

@@ -26,6 +26,10 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gaissmai/bart v0.26.1 h1:+w4rnLGNlA2GDVn382Tfe3jOsK5vOr5n4KmigJ9lbTo=
github.com/gaissmai/bart v0.26.1/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
github.com/go-crypt/crypt v0.14.15 h1:q1i5OMpL05r935IxWmXgpDAVF0nvi4SMoHhGXLBQUEQ=
github.com/go-crypt/crypt v0.14.15/go.mod h1:0n/to1VqIZPENj2yEUa/sLLYYnmupma6cp+QMX4zfF0=
github.com/go-crypt/x v0.4.16 h1:WXdY28H/0MsXnH+gwerxuCcvBTJPkBG90u6oS4gIPZI=
github.com/go-crypt/x v0.4.16/go.mod h1:vmVFA/d/oLrEaCbqsLcjBMlTqF8u8pvH/c4+EJ/ped8=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -43,6 +47,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/jsimonetti/pwscheme v0.0.0-20220922140336-67a4d090f150 h1:ta6N7DaOQEACq28cLa0iRqXIbchByN9Lfll08CT2GBc=
github.com/jsimonetti/pwscheme v0.0.0-20220922140336-67a4d090f150/go.mod h1:SiNTKDgjKQORnazFVHXhpny7UtU0iJOqtxd7R7sCfDI=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -113,22 +119,38 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220921155015-db77216a4ee9/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220919170432-7a66f970e087/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=

View File

@@ -534,7 +534,7 @@ func runNewtMain(ctx context.Context) {
// Start auth daemon if enabled
if !disableSSH {
if err := startAuthDaemon(ctx); err != nil {
logger.Fatal("Failed to start auth daemon: %v", err)
logger.Warn("Did not start on site auth daemon: %v", err)
}
}

View File

@@ -3,6 +3,7 @@ package nativessh
import (
"bufio"
"fmt"
"log"
"os"
"os/user"
"path/filepath"
@@ -58,19 +59,31 @@ func SystemUserExists(username string) bool {
//
// Returns nil on the first method that succeeds, or an error if all fail.
func Authenticate(username, password, privateKeyPEM string) error {
log.Printf("nativessh: authenticating user %q (hasPassword=%v, hasPrivateKey=%v)", username, password != "", privateKeyPEM != "")
if !SystemUserExists(username) {
log.Printf("nativessh: user %q not found on system", username)
return fmt.Errorf("user %q does not exist", username)
}
if privateKeyPEM != "" {
signer, err := ssh.ParsePrivateKey([]byte(privateKeyPEM))
if err == nil && CheckAuthorizedKeys(username, signer.PublicKey()) {
if err != nil {
log.Printf("nativessh: failed to parse private key for %q: %v", username, err)
} else if CheckAuthorizedKeys(username, signer.PublicKey()) {
log.Printf("nativessh: private key auth succeeded for %q", username)
return nil
} else {
log.Printf("nativessh: private key not in authorized_keys for %q", username)
}
}
if password != "" {
if err := VerifySystemPassword(username, password); err == nil {
if err := VerifySystemPassword(username, password); err != nil {
log.Printf("nativessh: password auth failed for %q: %v", username, err)
} else {
log.Printf("nativessh: password auth succeeded for %q", username)
return nil
}
} else {
log.Printf("nativessh: no password provided for %q", username)
}
return fmt.Errorf("authentication failed for user %q", username)
}

View File

@@ -5,25 +5,71 @@ package nativessh
import (
"bufio"
"bytes"
"crypto/sha512"
"errors"
"fmt"
"log"
"os"
"strconv"
"strings"
"golang.org/x/crypto/bcrypt"
"github.com/go-crypt/crypt"
"github.com/go-crypt/x/yescrypt"
)
// VerifySystemPassword authenticates username/password by reading /etc/shadow
// and verifying the stored hash using pure-Go cryptography (no CGo required).
// Supported hash schemes: bcrypt ($2a$/$2b$/$2y$) and SHA-512 crypt ($6$).
// VerifySystemPassword authenticates username/password by reading /etc/shadow.
// Supports yescrypt ($y$), bcrypt ($2b$/$2a$/$2y$), SHA-512 ($6$), SHA-256
// ($5$), argon2, scrypt, and other schemes handled by go-crypt/crypt.
func VerifySystemPassword(username, password string) error {
hash, err := readShadowHash(username)
if err != nil {
log.Printf("nativessh/pam: readShadowHash for %q failed: %v", username, err)
return fmt.Errorf("shadow: %w", err)
}
return cryptVerify(password, hash)
// Log the scheme prefix only (never the full hash).
scheme := "unknown"
for _, prefix := range []string{"$y$", "$2a$", "$2b$", "$2y$", "$6$", "$5$", "$1$"} {
if strings.HasPrefix(hash, prefix) {
scheme = prefix
break
}
}
log.Printf("nativessh/pam: verifying password for %q using scheme %s", username, scheme)
// Yescrypt ($y$) is not in go-crypt/crypt's default decoder; handle it directly.
if strings.HasPrefix(hash, "$y$") {
computed, err := yescrypt.Hash([]byte(password), []byte(hash))
if err != nil {
log.Printf("nativessh/pam: yescrypt.Hash for %q failed: %v", username, err)
return fmt.Errorf("yescrypt: %w", err)
}
if !bytes.Equal(computed, []byte(hash)) {
log.Printf("nativessh/pam: yescrypt mismatch for %q", username)
return errors.New("authentication failed")
}
return nil
}
decoder, err := crypt.NewDefaultDecoder()
if err != nil {
return fmt.Errorf("crypt decoder: %w", err)
}
digest, err := decoder.Decode(hash)
if err != nil {
log.Printf("nativessh/pam: failed to decode hash for %q: %v", username, err)
return fmt.Errorf("unsupported password hash scheme %q: %w", scheme, err)
}
match, err := digest.MatchAdvanced(password)
if err != nil {
log.Printf("nativessh/pam: MatchAdvanced for %q failed: %v", username, err)
return err
}
if !match {
log.Printf("nativessh/pam: password mismatch for %q", username)
return errors.New("authentication failed")
}
return nil
}
// readShadowHash reads /etc/shadow and returns the password hash for username.
@@ -51,223 +97,3 @@ func readShadowHash(username string) (string, error) {
}
return "", errors.New("user not found in shadow database")
}
// cryptVerify verifies password against a crypt(3) hash string.
func cryptVerify(password, hash string) error {
switch {
case strings.HasPrefix(hash, "$2a$"), strings.HasPrefix(hash, "$2b$"), strings.HasPrefix(hash, "$2y$"):
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
case strings.HasPrefix(hash, "$6$"):
computed, err := sha512CryptHash([]byte(password), hash)
if err != nil {
return err
}
if computed != hash {
return errors.New("authentication failed")
}
return nil
default:
return fmt.Errorf("unsupported password hash scheme")
}
}
// --- SHA-512 crypt ($6$) ---
// Specification: https://www.akkadia.org/docs/sha-crypt.txt
const (
sha512CryptMagic = "$6$"
sha512CryptRoundsDefault = 5000
sha512CryptRoundsMin = 1000
sha512CryptRoundsMax = 999999999
sha512CryptSaltLenMax = 16
sha512CryptAlphabet = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
)
var sha512CryptRoundsPrefix = []byte("rounds=")
// sha512CryptHash computes a SHA-512 crypt hash for key. saltStr may be a
// full stored hash string (for verification) or just the salt parameters.
func sha512CryptHash(key []byte, saltStr string) (string, error) {
salt := []byte(saltStr)
if !bytes.HasPrefix(salt, []byte(sha512CryptMagic)) {
return "", errors.New("sha512crypt: invalid prefix")
}
salt = salt[len(sha512CryptMagic):]
rounds := sha512CryptRoundsDefault
isRoundsDef := false
if bytes.HasPrefix(salt, sha512CryptRoundsPrefix) {
salt = salt[len(sha512CryptRoundsPrefix):]
i := bytes.IndexByte(salt, '$')
if i < 0 {
return "", errors.New("sha512crypt: malformed rounds field")
}
r, err := strconv.Atoi(string(salt[:i]))
if err != nil {
return "", fmt.Errorf("sha512crypt: invalid rounds: %w", err)
}
salt = salt[i+1:]
isRoundsDef = true
rounds = r
if rounds < sha512CryptRoundsMin {
rounds = sha512CryptRoundsMin
} else if rounds > sha512CryptRoundsMax {
rounds = sha512CryptRoundsMax
}
}
// When saltStr is a full hash, strip the stored hash after the last '$'.
if i := bytes.IndexByte(salt, '$'); i >= 0 {
salt = salt[:i]
}
if len(salt) > sha512CryptSaltLenMax {
salt = salt[:sha512CryptSaltLenMax]
}
// Alternate = SHA512(key + salt + key)
altH := sha512.New()
altH.Write(key)
altH.Write(salt)
altH.Write(key)
altSum := altH.Sum(nil)
// Digest A = SHA512(key + salt + altSum-cycling-to-len(key) + bit-pattern)
aH := sha512.New()
aH.Write(key)
aH.Write(salt)
for i := len(key); i > 0; i -= 64 {
if i > 64 {
aH.Write(altSum)
} else {
aH.Write(altSum[:i])
}
}
for i := len(key); i > 0; i >>= 1 {
if i&1 != 0 {
aH.Write(altSum)
} else {
aH.Write(key)
}
}
aSum := aH.Sum(nil)
// P-sequence: SHA512(key×len(key)) cycled to len(key) bytes
pH := sha512.New()
for i := 0; i < len(key); i++ {
pH.Write(key)
}
pSeq := sha512CryptCycle(pH.Sum(nil), len(key))
// S-sequence: SHA512(salt×(16+aSum[0])) cycled to len(salt) bytes
sH := sha512.New()
for i := 0; i < 16+int(aSum[0]); i++ {
sH.Write(salt)
}
sSeq := sha512CryptCycle(sH.Sum(nil), len(salt))
// Iterative hashing rounds
cSum := aSum
for i := 0; i < rounds; i++ {
c := sha512.New()
if i&1 != 0 {
c.Write(pSeq)
} else {
c.Write(cSum)
}
if i%3 != 0 {
c.Write(sSeq)
}
if i%7 != 0 {
c.Write(pSeq)
}
if i&1 != 0 {
c.Write(cSum)
} else {
c.Write(pSeq)
}
cSum = c.Sum(nil)
}
// Build the output string
out := []byte(sha512CryptMagic)
if isRoundsDef {
out = append(out, fmt.Sprintf("rounds=%d$", rounds)...)
}
out = append(out, salt...)
out = append(out, '$')
out = append(out, sha512CryptEncode(cSum)...)
return string(out), nil
}
// sha512CryptCycle returns exactly n bytes by cycling the 64-byte src slice.
func sha512CryptCycle(src []byte, n int) []byte {
dst := make([]byte, 0, n)
for i := n; i > 64; i -= 64 {
dst = append(dst, src...)
}
if rem := n % 64; rem == 0 && n > 0 {
dst = append(dst, src...)
} else if rem > 0 {
dst = append(dst, src[:rem]...)
}
return dst
}
// sha512CryptEncode applies the sha512crypt byte permutation and encodes the
// result with the crypt(3) base-64 alphabet (86 output characters for 64 input bytes).
func sha512CryptEncode(sum []byte) []byte {
perm := []byte{
sum[42], sum[21], sum[0],
sum[1], sum[43], sum[22],
sum[23], sum[2], sum[44],
sum[45], sum[24], sum[3],
sum[4], sum[46], sum[25],
sum[26], sum[5], sum[47],
sum[48], sum[27], sum[6],
sum[7], sum[49], sum[28],
sum[29], sum[8], sum[50],
sum[51], sum[30], sum[9],
sum[10], sum[52], sum[31],
sum[32], sum[11], sum[53],
sum[54], sum[33], sum[12],
sum[13], sum[55], sum[34],
sum[35], sum[14], sum[56],
sum[57], sum[36], sum[15],
sum[16], sum[58], sum[37],
sum[38], sum[17], sum[59],
sum[60], sum[39], sum[18],
sum[19], sum[61], sum[40],
sum[41], sum[20], sum[62],
sum[63],
}
src := perm
out := make([]byte, 0, 86)
for len(src) > 0 {
switch len(src) {
default:
out = append(out,
sha512CryptAlphabet[src[0]&0x3f],
sha512CryptAlphabet[((src[0]>>6)|(src[1]<<2))&0x3f],
sha512CryptAlphabet[((src[1]>>4)|(src[2]<<4))&0x3f],
sha512CryptAlphabet[(src[2]>>2)&0x3f],
)
src = src[3:]
case 2:
out = append(out,
sha512CryptAlphabet[src[0]&0x3f],
sha512CryptAlphabet[((src[0]>>6)|(src[1]<<2))&0x3f],
sha512CryptAlphabet[(src[1]>>4)&0x3f],
)
src = src[2:]
case 1:
out = append(out,
sha512CryptAlphabet[src[0]&0x3f],
sha512CryptAlphabet[(src[0]>>6)&0x3f],
)
src = src[1:]
}
}
return out
}

1
newt.REMOVED.git-id Normal file
View File

@@ -0,0 +1 @@
34062da7bd1b98e4a7953d2f8b43860d2222add0