diff --git a/adapter/outbound/shadowsocks.go b/adapter/outbound/shadowsocks.go index 25e4cb62..08a2247c 100644 --- a/adapter/outbound/shadowsocks.go +++ b/adapter/outbound/shadowsocks.go @@ -450,6 +450,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { if err != nil { return nil, fmt.Errorf("ss %s initialize jls-plugin error: %w", addr, err) } + jlsConfig.ClientFingerprint = option.ClientFingerprint } else if option.Plugin == kcptun.Mode { obfsMode = kcptun.Mode kcptunOpt := &kcpTunOption{} diff --git a/docs/config.yaml b/docs/config.yaml index 04e967de..dfabe630 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -543,6 +543,7 @@ proxies: # socks5 cipher: chacha20-ietf-poly1305 password: "password" plugin: jls + client-fingerprint: chrome plugin-opts: host: "www.example.com" username: "jls-user" diff --git a/listener/inbound/shadowsocks_test.go b/listener/inbound/shadowsocks_test.go index 0d59746d..0bc1d7da 100644 --- a/listener/inbound/shadowsocks_test.go +++ b/listener/inbound/shadowsocks_test.go @@ -200,6 +200,19 @@ func TestInboundShadowSocks_Restls_tls13(t *testing.T) { testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherShortLists, false) } +func testInboundShadowSocksJLS(t *testing.T, inboundOptions inbound.ShadowSocksOption, outboundOptions outbound.ShadowSocksOption) { + t.Parallel() + t.Run("Conn", func(t *testing.T) { + inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value + testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherShortLists, false) + }) + t.Run("UConn", func(t *testing.T) { + inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value + outboundOptions.ClientFingerprint = "chrome" + testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherShortLists, false) + }) +} + func TestInboundShadowSocks_JLS(t *testing.T) { username := "jls-user" password := "jls-password" @@ -219,7 +232,7 @@ func TestInboundShadowSocks_JLS(t *testing.T) { "password": password, }, } - testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherShortLists, false) + testInboundShadowSocksJLS(t, inboundOptions, outboundOptions) } func TestInboundShadowSocks_SimpleObfs_Http(t *testing.T) { diff --git a/transport/jls/jls.go b/transport/jls/jls.go index de0a887f..75e94a7f 100644 --- a/transport/jls/jls.go +++ b/transport/jls/jls.go @@ -26,15 +26,17 @@ const ( var ( DefaultALPN = []string{"h2", "http/1.1"} + ErrJLSAuthFailed = tls.ErrJLSAuthFailed ErrFallbackCompleted = errors.New("jls: connection relayed to fallback") ) type User = tls.JLSUser type ClientConfig struct { - ServerName string - User User - ALPN []string + ServerName string + User User + ALPN []string + ClientFingerprint string } type ServerConfig struct { @@ -44,14 +46,6 @@ type ServerConfig struct { DialContext func(ctx context.Context, network, address string) (net.Conn, error) } -type Conn struct { - *tls.Conn -} - -func (c *Conn) Upstream() any { - return c.Conn.NetConn() -} - func NewClientConfig(serverName, username, password string, alpn []string) (*ClientConfig, error) { if serverName == "" { return nil, errors.New("jls: server name is required") @@ -62,23 +56,30 @@ func NewClientConfig(serverName, username, password string, alpn []string) (*Cli if password == "" { return nil, errors.New("jls: password is required") } - if alpn == nil { - alpn = DefaultALPN - } - return &ClientConfig{ + config := &ClientConfig{ ServerName: serverName, User: User{Username: username, Password: password}, - ALPN: append([]string(nil), alpn...), - }, nil + } + if alpn != nil { + config.ALPN = append([]string{}, alpn...) + } + return config, nil } func NewClient(ctx context.Context, conn net.Conn, config *ClientConfig) (net.Conn, error) { if config == nil { return nil, errors.New("jls: nil client config") } + if client, ok, err := newUTLSClient(ctx, conn, config); ok { + return client, err + } + alpn := config.ALPN + if alpn == nil { + alpn = DefaultALPN + } tlsConn := tls.Client(conn, &tls.Config{ ServerName: config.ServerName, - NextProtos: append([]string(nil), config.ALPN...), + NextProtos: append([]string(nil), alpn...), RootCAs: ca.GetCertPool(), Time: ntp.Now, JLSConfig: &tls.JLSConfig{ @@ -90,9 +91,9 @@ func NewClient(ctx context.Context, conn net.Conn, config *ClientConfig) (net.Co return nil, err } if !tlsConn.ConnectionState().JLS.Authenticated { - return nil, tls.ErrJLSAuthFailed + return nil, ErrJLSAuthFailed } - return &Conn{Conn: tlsConn}, nil + return tlsConn, nil } func NewServerConfig(sni, dest string, users []User, alpn []string, rateLimit uint64, dialContext func(context.Context, string, string) (net.Conn, error)) (*ServerConfig, error) { @@ -166,9 +167,9 @@ func Server(ctx context.Context, conn net.Conn, config *ServerConfig) (net.Conn, } recorder.discard() if !tlsConn.ConnectionState().JLS.Authenticated { - return nil, tls.ErrJLSAuthFailed + return nil, ErrJLSAuthFailed } - return &Conn{Conn: tlsConn}, nil + return tlsConn, nil } func relayFallback(ctx context.Context, inbound net.Conn, prefix []byte, config *ServerConfig) error { diff --git a/transport/jls/jls_test.go b/transport/jls/jls_test.go index 8a81334b..53c61509 100644 --- a/transport/jls/jls_test.go +++ b/transport/jls/jls_test.go @@ -11,10 +11,24 @@ import ( "github.com/metacubex/mihomo/component/ca" + "github.com/metacubex/http" + "github.com/metacubex/http/httptest" tls "github.com/metacubex/jls-tls" ) func TestJLSClientServer(t *testing.T) { + for _, clientFingerprint := range []string{"", "chrome"} { + name := "Go" + if clientFingerprint != "" { + name = "uTLS" + } + t.Run(name, func(t *testing.T) { + testJLSClientServer(t, clientFingerprint) + }) + } +} + +func testJLSClientServer(t *testing.T, clientFingerprint string) { user := User{Username: "test-user", Password: "test-password"} serverConfig, err := NewServerConfig("camouflage.example", "camouflage.example:443", []User{user}, nil, 0, func(context.Context, string, string) (net.Conn, error) { return nil, errors.New("authenticated JLS connection dialed fallback") @@ -26,8 +40,9 @@ func TestJLSClientServer(t *testing.T) { if err != nil { t.Fatal(err) } + clientConfig.ClientFingerprint = clientFingerprint - serverSide, clientSide := net.Pipe() + serverSide, clientSide := newLocalTCPPair(t) serverDone := make(chan error, 1) go func() { conn, err := Server(context.Background(), serverSide, serverConfig) @@ -36,7 +51,7 @@ func TestJLSClientServer(t *testing.T) { return } defer conn.Close() - state := conn.(*Conn).ConnectionState() + state := conn.(*tls.Conn).ConnectionState() if !state.JLS.Authenticated || state.JLS.User != user.Username { serverDone <- errors.New("server did not authenticate JLS user") return @@ -66,6 +81,111 @@ func TestJLSClientServer(t *testing.T) { } } +func TestJLSUTLSClientRejectsInvalidFallbackCertificate(t *testing.T) { + tlsConfig := newTestTLSServerConfig(t, tls.VersionTLS13) + serverSide, clientSide := newLocalTCPPair(t) + serverDone := make(chan error, 1) + go func() { + server := tls.Server(serverSide, tlsConfig) + serverDone <- server.Handshake() + _ = serverSide.Close() + }() + + config, err := NewClientConfig("camouflage.example", "user", "password", nil) + if err != nil { + t.Fatal(err) + } + config.ClientFingerprint = "chrome" + if conn, clientErr := NewClient(context.Background(), clientSide, config); clientErr == nil || errors.Is(clientErr, ErrJLSAuthFailed) { + if conn != nil { + _ = conn.Close() + } + t.Fatalf("client error = %v, want fallback certificate verification error", clientErr) + } + <-serverDone +} + +func TestJLSUTLSClientFallback(t *testing.T) { + for _, protocol := range []string{"http/1.1", "h2"} { + t.Run(protocol, func(t *testing.T) { + requestProtocol := make(chan string, 1) + server := httptest.NewUnstartedServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + requestProtocol <- request.Proto + writer.WriteHeader(http.StatusNoContent) + })) + server.EnableHTTP2 = protocol == "h2" + server.StartTLS() + defer func() { + server.CloseClientConnections() + server.Close() + }() + ca.GetCertPool().AddCert(server.Certificate()) + + clientSide, err := net.Dial("tcp", server.Listener.Addr().String()) + if err != nil { + t.Fatal(err) + } + config, err := NewClientConfig("example.com", "user", "password", []string{protocol}) + if err != nil { + t.Fatal(err) + } + config.ClientFingerprint = "chrome" + if conn, clientErr := NewClient(context.Background(), clientSide, config); !errors.Is(clientErr, ErrJLSAuthFailed) { + if conn != nil { + _ = conn.Close() + } + t.Fatalf("client error = %v, want %v", clientErr, ErrJLSAuthFailed) + } + + wantProtocol := "HTTP/1.1" + if protocol == "h2" { + wantProtocol = "HTTP/2.0" + } + select { + case got := <-requestProtocol: + if got != wantProtocol { + t.Fatalf("fallback protocol = %q, want %q", got, wantProtocol) + } + default: + t.Fatal("fallback HTTP request was not sent") + } + }) + } +} + +func TestJLSUTLSClientRejectsHelloRetryRequest(t *testing.T) { + user := User{Username: "user", Password: "password"} + serverConfig, err := NewServerConfig("camouflage.example", "camouflage.example:443", []User{user}, nil, 0, func(context.Context, string, string) (net.Conn, error) { + return nil, errors.New("HRR failure dialed fallback") + }) + if err != nil { + t.Fatal(err) + } + serverConfig.TLSConfig.CurvePreferences = []tls.CurveID{tls.CurveP256} + + serverSide, clientSide := newLocalTCPPair(t) + serverDone := make(chan error, 1) + go func() { + defer serverSide.Close() + _, serverErr := Server(context.Background(), serverSide, serverConfig) + serverDone <- serverErr + }() + + clientConfig, err := NewClientConfig("camouflage.example", user.Username, user.Password, nil) + if err != nil { + t.Fatal(err) + } + clientConfig.ClientFingerprint = "chrome" + if conn, clientErr := NewClient(context.Background(), clientSide, clientConfig); clientErr == nil { + _ = conn.Close() + t.Fatal("uTLS client unexpectedly completed a HelloRetryRequest handshake") + } + _ = clientSide.Close() + if err = <-serverDone; err == nil { + t.Fatal("JLS server unexpectedly completed a HelloRetryRequest handshake") + } +} + func TestNewServerConfigRequiresDialContext(t *testing.T) { _, err := NewServerConfig( "camouflage.example", @@ -263,3 +383,37 @@ func newTestTLSServerConfig(t *testing.T, version uint16) *tls.Config { MaxVersion: version, } } + +// newLocalTCPPair mirrors crypto/tls's test helper. A real TCP connection has +// enough buffering to avoid net.Pipe deadlocks when TLS handshake writes cross, +// such as a server ticket flight and the client's Finished message. +func newLocalTCPPair(t *testing.T) (server, client net.Conn) { + t.Helper() + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer listener.Close() + + accepted := make(chan struct { + conn net.Conn + err error + }, 1) + go func() { + conn, acceptErr := listener.Accept() + accepted <- struct { + conn net.Conn + err error + }{conn: conn, err: acceptErr} + }() + client, err = net.Dial("tcp", listener.Addr().String()) + if err != nil { + t.Fatal(err) + } + result := <-accepted + if result.err != nil { + _ = client.Close() + t.Fatal(result.err) + } + return result.conn, client +} diff --git a/transport/jls/utls.go b/transport/jls/utls.go new file mode 100644 index 00000000..88db4a52 --- /dev/null +++ b/transport/jls/utls.go @@ -0,0 +1,388 @@ +package jls + +import ( + "bytes" + "context" + "crypto/aes" + "crypto/cipher" + "crypto/sha256" + "crypto/x509" + "errors" + "net" + "strings" + + "github.com/metacubex/mihomo/component/ca" + tlsC "github.com/metacubex/mihomo/component/tls" + "github.com/metacubex/mihomo/ntp" + + "github.com/metacubex/http" + "github.com/metacubex/randv2" + utls "github.com/metacubex/utls" + "golang.org/x/crypto/cryptobyte" +) + +const ( + jlsClientHelloType = 1 + jlsServerHelloType = 2 + jlsHandshakeHeaderLen = 4 + jlsHelloLegacyVersionLen = 2 + jlsHelloRandomLen = 32 + jlsHelloRandomOffset = jlsHandshakeHeaderLen + jlsHelloLegacyVersionLen + jlsRandomSeedLen = jlsHelloRandomLen / 2 + jlsExtensionPreSharedKey = 41 + jlsExtensionSupportedVers = 43 + jlsExtensionKeyShare = 51 +) + +func newUTLSClient(ctx context.Context, conn net.Conn, config *ClientConfig) (net.Conn, bool, error) { + fingerprint, ok := tlsC.GetFingerprint(config.ClientFingerprint) + if !ok { + return nil, false, nil + } + + verifier := &utlsJLSVerifier{ + user: config.User, + serverName: config.ServerName, + } + alpn := config.ALPN + if alpn == nil { + alpn = DefaultALPN + } + // Resumption would require recalculating PSK binders after replacing the + // ClientHello random. uTLS does not expose a hook for that operation, so this + // client deliberately disables session tickets and 0-RTT. + uConn := utls.UClient(conn, &utls.Config{ + ServerName: config.ServerName, + NextProtos: append([]string(nil), alpn...), + // JLS authenticates the server in VerifyConnection. TLS still verifies + // CertificateVerify against the camouflage certificate's public key. + InsecureSkipVerify: true, + SessionTicketsDisabled: true, + Time: ntp.Now, + VerifyConnection: verifier.VerifyConnection, + }, fingerprint) + verifier.UConn = uConn + // uTLS has no hook for JLS, so first let it build the complete fingerprint. + // JLS authenticates the exact serialized ClientHello with random zeroed; + // changing the random before the fingerprint is finalized would authenticate + // bytes that can differ from the ClientHello sent on the wire. + if err := uConn.BuildHandshakeState(); err != nil { + return nil, true, err + } + if config.ALPN != nil { + overrideUTLSALPN(uConn, config.ALPN) + if err := uConn.BuildHandshakeState(); err != nil { + return nil, true, err + } + } + + hello := uConn.HandshakeState.Hello + if !utlsClientHelloSupportsTLS13(hello) { + return nil, true, errors.New("jls: uTLS fingerprint does not support TLS 1.3") + } + authData, err := jlsClientHelloAuthData(hello.Raw) + if err != nil { + return nil, true, err + } + if len(hello.Random) != jlsHelloRandomLen { + return nil, true, errors.New("jls: invalid uTLS client random") + } + fakeRandom, err := jlsBuildFakeRandom(config.User, hello.Random[:jlsRandomSeedLen], authData) + if err != nil { + return nil, true, err + } + if err = uConn.SetClientRandom(fakeRandom); err != nil { + return nil, true, err + } + if err = uConn.BuildHandshakeState(); err != nil { + return nil, true, err + } + sentClientHello := append([]byte(nil), uConn.HandshakeState.Hello.Raw...) + + if err = uConn.HandshakeContext(ctx); err != nil { + return nil, true, err + } + if !verifier.authenticated { + // The fallback certificate is valid, so finish with a plausible HTTP + // request before returning the JLS authentication error. This is + // synchronous because the Shadowsocks caller closes conn on return. + jlsClientHTTPFallback(ctx, uConn, config.ServerName, fingerprint) + return nil, true, ErrJLSAuthFailed + } + // A HelloRetryRequest makes uTLS generate another ClientHello. Its public API + // cannot recalculate the JLS random at that point, so reject it instead of + // accepting a connection whose second ClientHello was not authenticated. + if !bytes.Equal(sentClientHello, uConn.HandshakeState.Hello.Raw) { + _ = uConn.Close() + return nil, true, errors.New("jls: uTLS HelloRetryRequest is not supported") + } + if uConn.ConnectionState().Version != utls.VersionTLS13 { + _ = uConn.Close() + return nil, true, ErrJLSAuthFailed + } + return uConn, true, nil +} + +func jlsClientHTTPFallback(ctx context.Context, uConn net.Conn, serverName string, fingerprint utls.ClientHelloID) { + defer uConn.Close() + // The TLS layer is already established, so HTTP/2 must use h2c mode to avoid + // another TLS handshake. Otherwise use HTTP/1 as negotiated by the server. + protocols := new(http.Protocols) + if conn, ok := uConn.(interface{ ConnectionState() utls.ConnectionState }); ok && conn.ConnectionState().NegotiatedProtocol == "h2" { + protocols.SetUnencryptedHTTP2(true) + } else { + protocols.SetHTTP1(true) + } + client := http.Client{ + Transport: &http.Transport{ + DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return uConn, nil + }, + Protocols: protocols, + }, + } + request, err := http.NewRequestWithContext(ctx, "GET", "https://"+serverName, nil) + if err != nil { + return + } + request.Header.Set("User-Agent", fingerprint.Client) + request.AddCookie(&http.Cookie{Name: "padding", Value: strings.Repeat("0", randv2.IntN(32)+30)}) + response, err := client.Do(request) + if err != nil { + return + } + response.Body.Close() + client.CloseIdleConnections() +} + +func utlsClientHelloSupportsTLS13(hello *utls.PubClientHelloMsg) bool { + for _, version := range hello.SupportedVersions { + if version == utls.VersionTLS13 { + return true + } + } + return false +} + +type utlsJLSVerifier struct { + *utls.UConn + user User + serverName string + authenticated bool +} + +func (v *utlsJLSVerifier) VerifyConnection(state utls.ConnectionState) error { + serverHello := v.HandshakeState.ServerHello + if serverHello == nil { + return errors.New("jls: uTLS server hello is unavailable") + } + authData, err := jlsServerHelloAuthData(serverHello.Raw) + if err != nil { + return err + } + if !jlsCheckFakeRandom(v.user, serverHello.Random, authData) { + return verifyUTLSCertificate(state, v.serverName) + } + v.authenticated = true + return nil +} + +func verifyUTLSCertificate(state utls.ConnectionState, serverName string) error { + certificates := state.PeerCertificates + if len(certificates) == 0 { + return errors.New("jls: fallback server sent no certificates") + } + opts := x509.VerifyOptions{ + Roots: ca.GetCertPool(), + DNSName: serverName, + Intermediates: x509.NewCertPool(), + CurrentTime: ntp.Now(), + } + for _, certificate := range certificates[1:] { + opts.Intermediates.AddCert(certificate) + } + _, err := certificates[0].Verify(opts) + return err +} + +// overrideUTLSALPN keeps ALPS only when h2 remains advertised. +func overrideUTLSALPN(conn *utls.UConn, protocols []string) { + hasH2 := false + for _, protocol := range protocols { + if protocol == "h2" { + hasH2 = true + break + } + } + + hasALPN := false + extensions := conn.Extensions[:0] + for _, extension := range conn.Extensions { + switch extension := extension.(type) { + case *utls.ALPNExtension: + if len(protocols) == 0 { + continue + } + extension.AlpnProtocols = append([]string(nil), protocols...) + hasALPN = true + case *utls.ApplicationSettingsExtension, *utls.ApplicationSettingsExtensionNew: + if !hasH2 { + continue + } + } + extensions = append(extensions, extension) + } + if !hasALPN && len(protocols) > 0 { + extensions = append(extensions, &utls.ALPNExtension{ + AlpnProtocols: append([]string(nil), protocols...), + }) + } + conn.Extensions = extensions +} + +func jlsClientHelloAuthData(raw []byte) ([]byte, error) { + msg, err := cloneJLSHello(raw, jlsClientHelloType) + if err != nil { + return nil, err + } + zeroJLSBytes(msg[jlsHelloRandomOffset : jlsHelloRandomOffset+jlsHelloRandomLen]) + return msg, nil +} + +func jlsServerHelloAuthData(raw []byte) ([]byte, error) { + msg, err := cloneJLSHello(raw, jlsServerHelloType) + if err != nil { + return nil, err + } + + s := cryptobyte.String(msg) + var messageType, compressionMethod uint8 + var legacyVersion, cipherSuite uint16 + var body, sessionID, extensions cryptobyte.String + var random []byte + if !s.ReadUint8(&messageType) || + !s.ReadUint24LengthPrefixed(&body) || + !s.Empty() || + !body.ReadUint16(&legacyVersion) || + !body.ReadBytes(&random, jlsHelloRandomLen) || + !body.ReadUint8LengthPrefixed(&sessionID) || + !body.ReadUint16(&cipherSuite) || + !body.ReadUint8(&compressionMethod) || + !body.ReadUint16LengthPrefixed(&extensions) || + !body.Empty() { + return nil, errors.New("jls: invalid uTLS server hello") + } + + type extension struct { + typeID uint16 + wire []byte + } + extensionBytes := extensions + parsed := make([]extension, 0, 3) + for len(extensions) > 0 { + remaining := extensions + var typeID uint16 + var data cryptobyte.String + if !extensions.ReadUint16(&typeID) || !extensions.ReadUint16LengthPrefixed(&data) { + return nil, errors.New("jls: invalid uTLS server hello extensions") + } + wireLen := len(remaining) - len(extensions) + parsed = append(parsed, extension{typeID: typeID, wire: remaining[:wireLen]}) + } + + // rustls decodes ServerHello extensions into fields and serializes them in + // this order when it calculates the JLS random. The wire order is irrelevant + // to TLS but not to JLS, whose authentication input must match byte for byte. + canonicalTypes := [...]uint16{ + jlsExtensionKeyShare, + jlsExtensionPreSharedKey, + jlsExtensionSupportedVers, + } + canonical := make(map[uint16][]byte, len(canonicalTypes)) + for _, ext := range parsed { + for _, typeID := range canonicalTypes { + if ext.typeID == typeID { + canonical[typeID] = ext.wire + break + } + } + } + + // Reinsert the canonical extensions at their first original position. Their + // encoded bytes and all unrelated extensions remain untouched, and the total + // length does not change. + extensionOffset := len(msg) - len(extensionBytes) + result := append([]byte(nil), msg[:extensionOffset]...) + canonicalWritten := false + for _, ext := range parsed { + if _, ok := canonical[ext.typeID]; ok { + if !canonicalWritten { + for _, typeID := range canonicalTypes { + result = append(result, canonical[typeID]...) + } + canonicalWritten = true + } + continue + } + result = append(result, ext.wire...) + } + zeroJLSBytes(result[jlsHelloRandomOffset : jlsHelloRandomOffset+jlsHelloRandomLen]) + return result, nil +} + +func cloneJLSHello(raw []byte, messageType byte) ([]byte, error) { + if len(raw) < jlsHelloRandomOffset+jlsHelloRandomLen || + raw[0] != messageType || + int(raw[1])<<16|int(raw[2])<<8|int(raw[3]) != len(raw)-jlsHandshakeHeaderLen { + return nil, errors.New("jls: invalid uTLS hello") + } + return append([]byte(nil), raw...), nil +} + +func jlsBuildFakeRandom(user User, random16, authData []byte) ([]byte, error) { + if len(random16) != jlsRandomSeedLen { + return nil, errors.New("jls: random seed must be 16 bytes") + } + aead, nonce, err := newJLSAEAD(user, authData) + if err != nil { + return nil, err + } + return aead.Seal(nil, nonce[:], random16, nil), nil +} + +func jlsCheckFakeRandom(user User, fakeRandom, authData []byte) bool { + if len(fakeRandom) != jlsHelloRandomLen { + return false + } + aead, nonce, err := newJLSAEAD(user, authData) + if err != nil { + return false + } + plain, err := aead.Open(nil, nonce[:], fakeRandom, nil) + return err == nil && len(plain) == jlsRandomSeedLen +} + +func newJLSAEAD(user User, authData []byte) (cipher.AEAD, [sha256.Size]byte, error) { + nonce := jlsHash(user.Username, authData) + key := jlsHash(user.Password, authData) + block, err := aes.NewCipher(key[:]) + if err != nil { + return nil, nonce, err + } + aead, err := cipher.NewGCMWithNonceSize(block, len(nonce)) + return aead, nonce, err +} + +func jlsHash(value string, authData []byte) (sum [sha256.Size]byte) { + hash := sha256.New() + _, _ = hash.Write([]byte(value)) + _, _ = hash.Write(authData) + hash.Sum(sum[:0]) + return sum +} + +func zeroJLSBytes(data []byte) { + for i := range data { + data[i] = 0 + } +}