Skip to content

Commit

Permalink
feat(security): encrypt login password (#1583)
Browse files Browse the repository at this point in the history
  • Loading branch information
baurine authored Aug 31, 2023
1 parent db0052c commit d010da5
Show file tree
Hide file tree
Showing 9 changed files with 787 additions and 526 deletions.
22 changes: 21 additions & 1 deletion pkg/apiserver/user/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package user

import (
"crypto/rsa"
"encoding/base64"
"encoding/json"
"errors"
Expand Down Expand Up @@ -35,6 +36,9 @@ type AuthService struct {

middleware *jwt.GinJWTMiddleware
authenticators map[utils.AuthType]Authenticator

rsaPublicKey *rsa.PublicKey
RsaPrivateKey *rsa.PrivateKey
}

type AuthenticateForm struct {
Expand Down Expand Up @@ -90,10 +94,17 @@ func NewAuthService(featureFlags *featureflag.Registry) *AuthService {
secret = cryptopasta.NewEncryptionKey()
}

privateKey, publicKey, err := GenerateKey()
if err != nil {
log.Fatal("Failed to generate rsa key pairs", zap.Error(err))
}

service := &AuthService{
FeatureFlagNonRootLogin: featureFlags.Register("nonRootLogin", ">= 5.3.0"),
middleware: nil,
authenticators: map[utils.AuthType]Authenticator{},
RsaPrivateKey: privateKey,
rsaPublicKey: publicKey,
}

middleware, err := jwt.New(&jwt.GinJWTMiddleware{
Expand Down Expand Up @@ -278,7 +289,8 @@ func (s *AuthService) RegisterAuthenticator(typeID utils.AuthType, a Authenticat
}

type GetLoginInfoResponse struct {
SupportedAuthTypes []int `json:"supported_auth_types"`
SupportedAuthTypes []int `json:"supported_auth_types"`
SQLAuthPublicKey string `json:"sql_auth_public_key"`
}

// @ID userGetLoginInfo
Expand All @@ -298,8 +310,16 @@ func (s *AuthService) GetLoginInfoHandler(c *gin.Context) {
}
}
sort.Ints(supportedAuth)
// both work
// publicKeyStr, err := ExportPublicKeyAsString(s.rsaPublicKey)
publicKeyStr, err := DumpPublicKeyBase64(s.rsaPublicKey)
if err != nil {
rest.Error(c, err)
return
}
resp := GetLoginInfoResponse{
SupportedAuthTypes: supportedAuth,
SQLAuthPublicKey: publicKeyStr,
}
c.JSON(http.StatusOK, resp)
}
Expand Down
96 changes: 96 additions & 0 deletions pkg/apiserver/user/rsa_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright 2023 PingCAP, Inc. Licensed under Apache-2.0.

package user

import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
)

// Generate RSA private/public key.
func GenerateKey() (*rsa.PrivateKey, *rsa.PublicKey, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, err
}

publicKey := &privateKey.PublicKey
return privateKey, publicKey, nil
}

// Export public key to string
// Output format:
// -----BEGIN PUBLIC KEY-----
// MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA67F1RPMUO4SjARRe4UfX
// J7ZOCbcysna0jx2Av14KteGo6AWFHhuIxZwgp83GDqFv0Dhc/be7n+9V5vfq0Ob4
// fUtdjBio5ciF4pcqzVGbddfJ0R2e52DF6TI2pDgUFdN+1bmGDwZOCyrwBvVh0wW2
// jAI+QfQyRimZOMqFeX97XjW32vGk7cxNYMys9ExyJcfzfLanbzOwp6kdNbPXnYtU
// Y2nmp+evlPKrRzBPnmO0bpZhYHklrRxLo/u/mThysMEttLkgzCare+JPQyb3z3Si
// Q2E7WG4yz6+6L/wB4etHDfRljMOtqEwv9z4inUfh5716Mg23Div/AbwqGPiKPZf7
// cQIDAQAB
// -----END PUBLIC KEY-----.
func ExportPublicKeyAsString(publicKey *rsa.PublicKey) (string, error) {
publicKeyBytes, err := x509.MarshalPKIXPublicKey(publicKey)
if err != nil {
return "", err
}

publicKeyPEM := &pem.Block{
Type: "PUBLIC KEY",
Bytes: publicKeyBytes,
}

publicKeyString := string(pem.EncodeToMemory(publicKeyPEM))

return publicKeyString, nil
}

// Dump public key to base64 string
// 1. Have no header/tailer line
// 2. Key content is merged into one-line format
//
// The output is:
//
// MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2y8mEdCRE8siiI7udpge......2QIDAQAB
func DumpPublicKeyBase64(publicKey *rsa.PublicKey) (string, error) {
keyBytes, err := x509.MarshalPKIXPublicKey(publicKey)
if err != nil {
return "", err
}

keyBase64 := base64.StdEncoding.EncodeToString(keyBytes)
return keyBase64, nil
}

// Dump private key to base64 string
// 1. Have no header/tailer line
// 2. Key content is merged into one-line format
//
// The output is:
//
// MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2y8mEdCRE8siiI7udpge......2QIDAQAB
func DumpPrivateKeyBase64(privatekey *rsa.PrivateKey) (string, error) {
keyBytes := x509.MarshalPKCS1PrivateKey(privatekey)

keyBase64 := base64.StdEncoding.EncodeToString(keyBytes)
return keyBase64, nil
}

// Decrypt by private key.
func Decrypt(cipherText string, privateKey *rsa.PrivateKey) (string, error) {
// the cipherText is encoded by base64 in the frontend by jsEncrypt
decodedText, err := base64.StdEncoding.DecodeString(cipherText)
if err != nil {
return "", err
}

decryptedText, err := rsa.DecryptPKCS1v15(rand.Reader, privateKey, decodedText)
if err != nil {
return "", err
}

return string(decryptedText), nil
}
15 changes: 12 additions & 3 deletions pkg/apiserver/user/sqlauth/sqlauth.go
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
package sqlauth

import (
"crypto/rsa"

"github.com/joomcode/errorx"
"go.uber.org/fx"

Expand All @@ -15,7 +17,8 @@ const typeID utils.AuthType = 0

type Authenticator struct {
user.BaseAuthenticator
tidbClient *tidb.Client
tidbClient *tidb.Client
rsaPrivateKey *rsa.PrivateKey
}

func NewAuthenticator(tidbClient *tidb.Client) *Authenticator {
Expand All @@ -26,6 +29,7 @@ func NewAuthenticator(tidbClient *tidb.Client) *Authenticator {

func registerAuthenticator(a *Authenticator, authService *user.AuthService) {
authService.RegisterAuthenticator(typeID, a)
a.rsaPrivateKey = authService.RsaPrivateKey
}

var Module = fx.Options(
Expand All @@ -34,7 +38,12 @@ var Module = fx.Options(
)

func (a *Authenticator) Authenticate(f user.AuthenticateForm) (*utils.SessionUser, error) {
writeable, err := user.VerifySQLUser(a.tidbClient, f.Username, f.Password)
plainPwd, err := user.Decrypt(f.Password, a.rsaPrivateKey)
if err != nil {
return nil, user.ErrSignInOther.WrapWithNoMessage(err)
}

writeable, err := user.VerifySQLUser(a.tidbClient, f.Username, plainPwd)
if err != nil {
if errorx.Cast(err) == nil {
return nil, user.ErrSignInOther.WrapWithNoMessage(err)
Expand All @@ -52,7 +61,7 @@ func (a *Authenticator) Authenticate(f user.AuthenticateForm) (*utils.SessionUse
Version: utils.SessionVersion,
HasTiDBAuth: true,
TiDBUsername: f.Username,
TiDBPassword: f.Password,
TiDBPassword: plainPwd,
DisplayName: f.Username,
IsShareable: true,
IsWriteable: writeable,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@
* @interface UserGetLoginInfoResponse
*/
export interface UserGetLoginInfoResponse {
/**
*
* @type {string}
* @memberof UserGetLoginInfoResponse
*/
'sql_auth_public_key'?: string;
/**
*
* @type {Array<number>}
Expand Down
3 changes: 3 additions & 0 deletions ui/packages/tidb-dashboard-client/swagger/spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -5776,6 +5776,9 @@
"user.GetLoginInfoResponse": {
"type": "object",
"properties": {
"sql_auth_public_key": {
"type": "string"
},
"supported_auth_types": {
"type": "array",
"items": {
Expand Down
1 change: 1 addition & 0 deletions ui/packages/tidb-dashboard-for-op/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"compare-versions": "^5.0.1",
"eventemitter2": "^6.4.5",
"i18next": "^23.2.9",
"jsencrypt": "^3.3.2",
"nprogress": "^0.2.0",
"rc-animate": "^3.1.0",
"react": "^17.0.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { useTranslation } from 'react-i18next'
import { useMount } from 'react-use'
import Flexbox from '@g07cha/flexbox-react'
import { useMemoizedFn } from 'ahooks'
import JSEncrypt from 'jsencrypt'

import {
// distro
Expand Down Expand Up @@ -228,7 +229,7 @@ function useSignInSubmit(

const LAST_LOGIN_USERNAME_KEY = 'dashboard_last_login_username'

function TiDBSignInForm({ successRoute, onClickAlternative }) {
function TiDBSignInForm({ successRoute, onClickAlternative, publicKey }) {
const supportNonRootLogin = useIsFeatureSupport('nonRootLogin')

const { t } = useTranslation()
Expand All @@ -238,11 +239,29 @@ function TiDBSignInForm({ successRoute, onClickAlternative }) {

const { handleSubmit, loading, errorMsg, clearErrorMsg } = useSignInSubmit(
successRoute,
(form) => ({
username: form.username,
password: form.password,
type: auth.AuthTypes.SQLUser
}),
(form) => {
let password = form.password ?? ''
if (!!publicKey) {
const jsEncrypt = new JSEncrypt()
if (publicKey.startsWith('-----BEGIN PUBLIC KEY-----')) {
// if publicKey is generated by `ExportPublicKeyAsString(s.rsaPublicKey)`, it has header and footer, so we use it directly
jsEncrypt.setPublicKey(publicKey)
} else {
// if publicKey is generated by `DumpPublicKeyBase64(s.rsaPublicKey)`, it has no header and footer, so we need to add them
jsEncrypt.setPublicKey(
'-----BEGIN PUBLIC KEY-----' +
publicKey +
'-----END PUBLIC KEY-----'
)
}
password = jsEncrypt.encrypt(password)
}
return {
username: form.username,
password,
type: auth.AuthTypes.SQLUser
}
},
(form) => {
localStorage.setItem(LAST_LOGIN_USERNAME_KEY, form.username)
},
Expand Down Expand Up @@ -449,6 +468,7 @@ function App({ registry }) {
const [supportedAuthTypes, setSupportedAuthTypes] = useState<Array<number>>([
0
])
const [publicKey, setPublicKey] = useState('')

const handleClickAlternative = useCallback(() => {
setAlternativeVisible(true)
Expand Down Expand Up @@ -479,6 +499,9 @@ function App({ registry }) {
setFormType(DisplayFormType.tidbCredential)
}
setSupportedAuthTypes(loginInfo.supported_auth_types ?? [])
if (!!loginInfo.sql_auth_public_key) {
setPublicKey(loginInfo.sql_auth_public_key)
}
} catch (e) {
if ((e as any).response?.status === 404) {
setFormType(DisplayFormType.tidbCredential)
Expand Down Expand Up @@ -517,6 +540,7 @@ function App({ registry }) {
<TiDBSignInForm
successRoute={successRoute}
onClickAlternative={handleClickAlternative}
publicKey={publicKey}
/>
)}
{formType === DisplayFormType.shareCode && (
Expand Down
6 changes: 6 additions & 0 deletions ui/packages/tidb-dashboard-lib/src/client/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3809,6 +3809,12 @@ export interface UserAuthenticateForm {
* @interface UserGetLoginInfoResponse
*/
export interface UserGetLoginInfoResponse {
/**
*
* @type {string}
* @memberof UserGetLoginInfoResponse
*/
'sql_auth_public_key'?: string;
/**
*
* @type {Array<number>}
Expand Down
Loading

0 comments on commit d010da5

Please sign in to comment.