Store resetSecret in encrypted form
This commit is contained in:
parent
f766484b2e
commit
50c3a7a8e5
8 changed files with 61 additions and 65 deletions
|
@ -138,7 +138,7 @@ func main() {
|
|||
|
||||
twoFactorRepo := &repo.TwoFactorRepository{DB: db, SecretEncryptionKey: secretEncryptionKeyBytes}
|
||||
userAuthRepo := &repo.UserAuthRepository{DB: db}
|
||||
twoFactorRecoveryRepo := &two_factor_recovery.Repository{Db: db}
|
||||
twoFactorRecoveryRepo := &two_factor_recovery.Repository{Db: db, SecretEncryptionKey: secretEncryptionKeyBytes}
|
||||
billingRepo := &repo.BillingRepository{DB: db}
|
||||
userEntityRepo := &userEntityRepo.Repository{DB: db}
|
||||
locationTagRepository := &locationtagRepo.Repository{DB: db}
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
package ente
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// EncData is a struct that holds an encrypted data and related nonce.
|
||||
type EncData struct {
|
||||
Data string `json:"data"`
|
||||
Nonce string `json:"nonce"`
|
||||
}
|
||||
|
||||
// Value implements the driver.Valuer interface, allowing EncString to be used as a SQL value.
|
||||
func (e EncData) Value() (driver.Value, error) {
|
||||
return json.Marshal(e)
|
||||
}
|
||||
|
||||
// Scan implements the sql.Scanner interface, allowing EncString to be scanned from SQL queries.
|
||||
func (e *EncData) Scan(value interface{}) error {
|
||||
// Convert to bytes if necessary (depends on the driver, pq returns []byte for JSONB)
|
||||
b, ok := value.([]byte)
|
||||
if !ok {
|
||||
return fmt.Errorf("type assertion to []byte failed")
|
||||
}
|
||||
return json.Unmarshal(b, &e)
|
||||
}
|
|
@ -13,23 +13,29 @@ type Passkey struct {
|
|||
|
||||
var MaxPasskeys = 10
|
||||
|
||||
type ConfigurePassKeySkipRequest struct {
|
||||
PassKeySkipSecret string `json:"passKeySkipSecret" binding:"required"`
|
||||
EncPassKeySkipSecret EncData `json:"encPassKeySkipSecret" binding:"required"`
|
||||
type ConfigurePassKeyRecoveryRequest struct {
|
||||
SkipSecret string `json:"resetSecret" binding:"required"`
|
||||
// The UserSecretCipher has SkipSecret encrypted with the user's recoveryKey
|
||||
// If the user sends the correct UserSecretCipher, we can be sure that the user has the recoveryKey,
|
||||
// and we can allow the user to recover their MFA.
|
||||
UserSecretCipher string `json:"userSecretCipher" binding:"required"`
|
||||
UserSecretNonce string `json:"userSecretNonce" binding:"required"`
|
||||
}
|
||||
|
||||
type TwoFactorRecoveryStatus struct {
|
||||
// AllowAdminReset is a boolean that determines if the admin can reset the user's MFA.
|
||||
// If true, in the event that the user loses their MFA device, the admin can reset the user's MFA.
|
||||
AllowAdminReset bool `json:"allowAdminReset" binding:"required"`
|
||||
IsPassKeySkipEnabled bool `json:"isPassKeySkipEnabled" binding:"required"`
|
||||
IsPassKeySkipEnabled bool `json:"isPassKeyResetEnabled" binding:"required"`
|
||||
}
|
||||
|
||||
type PasseKeySkipChallengeResponse struct {
|
||||
EncData
|
||||
// The PassKeyEncSecret has SkipSecret encrypted with the user's recoveryKey
|
||||
UserSecretCipher string `json:"userSecretCipher" binding:"required"`
|
||||
UserSecretNonce string `json:"userSecretNonce" binding:"required"`
|
||||
}
|
||||
|
||||
type SkipPassKeyRequest struct {
|
||||
SessionID string `json:"sessionID" binding:"required"`
|
||||
PassKeySkipSecret string `json:"passKeySkipSecret" binding:"required"`
|
||||
SessionID string `json:"sessionID" binding:"required"`
|
||||
SkipSecret string `json:"resetSecret" binding:"required"`
|
||||
}
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
DROP TABLE IF NOT EXISTS two_factor_recovery;
|
||||
DROP TABLE IF EXISTS two_factor_recovery;
|
||||
DROP TRIGGER IF EXISTS update_two_factor_recovery_updated_at ON two_factor_recovery;
|
|
@ -2,10 +2,10 @@ CREATE TABLE IF NOT EXISTS two_factor_recovery (
|
|||
user_id bigint NOT NULL,
|
||||
-- if false, the support team team will not be able to reset the MFA for the user
|
||||
enable_admin_mfa_reset boolean NOT NULL DEFAULT true,
|
||||
pass_key_reset_key uuid,
|
||||
pass_key_reset_enc_data jsonb,
|
||||
twofa_key_reset_key uuid,
|
||||
twofa_key_reset_enc_data jsonb,
|
||||
server_passkey_secret_data bytea,
|
||||
server_passkey_secret_nonce bytea,
|
||||
user_passkey_secret_data text,
|
||||
user_passkey_secret_nonce text,
|
||||
created_at bigint NOT NULL DEFAULT now_utc_micro_seconds(),
|
||||
updated_at bigint NOT NULL DEFAULT now_utc_micro_seconds()
|
||||
);
|
||||
|
|
|
@ -256,7 +256,7 @@ func (h *UserHandler) GetAccountRecoveryStatus(c *gin.Context) {
|
|||
// ConfigurePassKeySkipChallenge configures the passkey skip challenge for a user. In case the user does not
|
||||
// have access to passkey, the user can bypass the passkey by providing the recovery key
|
||||
func (h *UserHandler) ConfigurePassKeySkipChallenge(c *gin.Context) {
|
||||
var request ente.ConfigurePassKeySkipRequest
|
||||
var request ente.ConfigurePassKeyRecoveryRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
handler.Error(c, stacktrace.Propagate(err, ""))
|
||||
return
|
||||
|
|
|
@ -13,12 +13,12 @@ func (c *UserController) GetAccountRecoveryStatus(ctx *gin.Context) (*ente.TwoFa
|
|||
return c.TwoFactorRecoveryRepo.GetStatus(userID)
|
||||
}
|
||||
|
||||
func (c *UserController) ConfigurePassKeySkip(ctx *gin.Context, req *ente.ConfigurePassKeySkipRequest) error {
|
||||
func (c *UserController) ConfigurePassKeySkip(ctx *gin.Context, req *ente.ConfigurePassKeyRecoveryRequest) error {
|
||||
userID := auth.GetUserID(ctx.Request.Header)
|
||||
return c.TwoFactorRecoveryRepo.ConfigurePassKeyRecovery(ctx, userID, req)
|
||||
return c.TwoFactorRecoveryRepo.ConfigurePassKeySkipChallenge(ctx, userID, req)
|
||||
}
|
||||
|
||||
func (c *UserController) GetPasskeySkipChallenge(ctx *gin.Context, passKeySessionID string) (*ente.EncData, error) {
|
||||
func (c *UserController) GetPasskeySkipChallenge(ctx *gin.Context, passKeySessionID string) (*ente.PasseKeySkipChallengeResponse, error) {
|
||||
userID, err := c.PasskeyRepo.GetUserIDWithPasskeyTwoFactorSession(passKeySessionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -31,7 +31,7 @@ func (c *UserController) GetPasskeySkipChallenge(ctx *gin.Context, passKeySessio
|
|||
return nil, ente.NewBadRequestWithMessage("Passkey reset is not configured")
|
||||
}
|
||||
|
||||
result, err := c.TwoFactorRecoveryRepo.GetPasskeyResetChallenge(ctx, userID)
|
||||
result, err := c.TwoFactorRecoveryRepo.GetPasskeySkipChallenge(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ func (c *UserController) SkipPassKey(context *gin.Context, req *ente.SkipPassKey
|
|||
if err != nil {
|
||||
return nil, stacktrace.Propagate(err, "")
|
||||
}
|
||||
exists, err := c.TwoFactorRecoveryRepo.VerifyRecoveryKeyForPassKey(userID, req.PassKeySkipSecret)
|
||||
exists, err := c.TwoFactorRecoveryRepo.VerifyPasskeySkipSecret(userID, req.SkipSecret)
|
||||
if err != nil {
|
||||
return nil, stacktrace.Propagate(err, "")
|
||||
}
|
||||
|
|
|
@ -4,18 +4,21 @@ import (
|
|||
"context"
|
||||
"database/sql"
|
||||
"github.com/ente-io/museum/ente"
|
||||
"github.com/ente-io/museum/pkg/utils/crypto"
|
||||
"github.com/ente-io/stacktrace"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
Db *sql.DB
|
||||
Db *sql.DB
|
||||
SecretEncryptionKey []byte
|
||||
}
|
||||
|
||||
// GetStatus returns `ente.TwoFactorRecoveryStatus` for a user
|
||||
func (r *Repository) GetStatus(userID int64) (*ente.TwoFactorRecoveryStatus, error) {
|
||||
var isAdminResetEnabled bool
|
||||
var resetKey sql.NullString
|
||||
row := r.Db.QueryRow("SELECT enable_admin_mfa_reset, pass_key_reset_key FROM two_factor_recovery WHERE user_id = $1", userID)
|
||||
var resetKey sql.NullByte
|
||||
row := r.Db.QueryRow(`SELECT enable_admin_mfa_reset, server_passkey_secret_data FROM two_factor_recovery WHERE user_id = $1`, userID)
|
||||
err := row.Scan(&isAdminResetEnabled, &resetKey)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
|
@ -30,30 +33,45 @@ func (r *Repository) GetStatus(userID int64) (*ente.TwoFactorRecoveryStatus, err
|
|||
return &ente.TwoFactorRecoveryStatus{AllowAdminReset: isAdminResetEnabled, IsPassKeySkipEnabled: resetKey.Valid}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) ConfigurePassKeyRecovery(ctx context.Context, userID int64, req *ente.ConfigurePassKeySkipRequest) error {
|
||||
_, err := r.Db.ExecContext(ctx, `INSERT INTO two_factor_recovery (user_id, pass_key_reset_key, pass_key_reset_enc_data)
|
||||
VALUES ($1, $2,$3) ON CONFLICT (user_id)
|
||||
DO UPDATE SET pass_key_reset_key = $2, pass_key_reset_enc_data = $3`, userID, req.PassKeySkipSecret,
|
||||
req.EncPassKeySkipSecret)
|
||||
func (r *Repository) ConfigurePassKeySkipChallenge(ctx context.Context, userID int64, req *ente.ConfigurePassKeyRecoveryRequest) error {
|
||||
serveEncPassKey, encRrr := crypto.Encrypt(req.SkipSecret, r.SecretEncryptionKey)
|
||||
if encRrr != nil {
|
||||
return stacktrace.Propagate(encRrr, "failed to encrypt passkey secret")
|
||||
}
|
||||
_, err := r.Db.ExecContext(ctx, `INSERT INTO two_factor_recovery
|
||||
(user_id, server_passkey_secret_data, server_passkey_secret_nonce, user_passkey_secret_data, user_passkey_secret_nonce))
|
||||
VALUES ($1, $2,$3,$4,$5) ON CONFLICT (user_id)
|
||||
DO UPDATE SET server_passkey_secret_data = $2, server_passkey_secret_nonce = $3, user_passkey_secret_data=$4,user_passkey_secret_nonce=$5`,
|
||||
userID, serveEncPassKey.Cipher, serveEncPassKey.Nonce, req.UserSecretCipher, req.UserSecretNonce)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Repository) GetPasskeyResetChallenge(ctx context.Context, userID int64) (*ente.EncData, error) {
|
||||
var encData *ente.EncData
|
||||
err := r.Db.QueryRowContext(ctx, "SELECT pass_key_reset_enc_data FROM two_factor_recovery WHERE user_id= $1", userID).Scan(encData)
|
||||
func (r *Repository) GetPasskeySkipChallenge(ctx context.Context, userID int64) (*ente.PasseKeySkipChallengeResponse, error) {
|
||||
var result *ente.PasseKeySkipChallengeResponse
|
||||
err := r.Db.QueryRowContext(ctx, "SELECT user_passkey_secret_data, user_passkey_secret_nonce FROM two_factor_recovery WHERE user_id= $1", userID).Scan(result.UserSecretCipher, result.UserSecretNonce)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return encData, nil
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// VerifyRecoveryKeyForPassKey checks if the passkey reset key is valid for a user
|
||||
func (r *Repository) VerifyRecoveryKeyForPassKey(userID int64, passKeyResetKey string) (bool, error) {
|
||||
var exists bool
|
||||
row := r.Db.QueryRow(`SELECT EXISTS( SELECT 1 FROM two_factor_recovery WHERE user_id = $1 AND pass_key_reset_key = $2)`, userID, passKeyResetKey)
|
||||
err := row.Scan(&exists)
|
||||
// VerifyPasskeySkipSecret checks if the passkey skip secret is valid for a user
|
||||
func (r *Repository) VerifyPasskeySkipSecret(userID int64, skipSecret string) (bool, error) {
|
||||
// get server_passkey_secret_data and server_passkey_secret_nonce for given user id
|
||||
var severSecreteData, serverSecretNonce []byte
|
||||
row := r.Db.QueryRow(`SELECT server_passkey_secret_data, server_passkey_secret_nonce FROM two_factor_recovery WHERE user_id = $1`, userID)
|
||||
err := row.Scan(&severSecreteData, &serverSecretNonce)
|
||||
if err != nil {
|
||||
return false, stacktrace.Propagate(err, "")
|
||||
}
|
||||
return exists, nil
|
||||
// decrypt server_passkey_secret_data
|
||||
serverSkipSecretKey, decErr := crypto.Decrypt(severSecreteData, serverSecretNonce, r.SecretEncryptionKey)
|
||||
if decErr != nil {
|
||||
return false, stacktrace.Propagate(decErr, "failed to decrypt passkey reset key")
|
||||
}
|
||||
if skipSecret != serverSkipSecretKey {
|
||||
logrus.Warn("invalid passkey skip secret")
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue