perf: password 等使用 rsa 加密传输 (#8188)

* perf: 修改 model fields 路径

* stash it

* pref: 统一加密方式,密码字段采用 rsa 加密

* pref: 临时密码使用 rsa

* perf: 去掉 debug msg

* perf: 去掉 Debug

* perf: 去掉 debug

* perf: 抽出来

Co-authored-by: ibuler <ibuler@qq.com>
pull/8199/head
fit2bot 2022-05-07 16:20:12 +08:00 committed by GitHub
parent 3f856e68f0
commit 031077c298
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 291 additions and 245 deletions

View File

@ -1,6 +1,6 @@
# Generated by Django 2.1.7 on 2019-05-20 11:04
import common.fields.model
import common.db.fields
from django.db import migrations, models
import django.db.models.deletion
import uuid
@ -23,7 +23,7 @@ class Migration(migrations.Migration):
('name', models.CharField(max_length=128, verbose_name='Name')),
('type', models.CharField(choices=[('Browser', (('chrome', 'Chrome'),)), ('Database tools', (('mysql_workbench', 'MySQL Workbench'),)), ('Virtualization tools', (('vmware_client', 'vSphere Client'),)), ('custom', 'Custom')], default='chrome', max_length=128, verbose_name='App type')),
('path', models.CharField(max_length=128, verbose_name='App path')),
('params', common.fields.model.EncryptJsonDictTextField(blank=True, default={}, max_length=4096, null=True, verbose_name='Parameters')),
('params', common.db.fields.EncryptJsonDictTextField(blank=True, default={}, max_length=4096, null=True, verbose_name='Parameters')),
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),

View File

@ -1,7 +1,7 @@
# Generated by Django 3.1.12 on 2021-08-26 09:07
import assets.models.base
import common.fields.model
import common.db.fields
from django.conf import settings
import django.core.validators
from django.db import migrations, models
@ -26,9 +26,9 @@ class Migration(migrations.Migration):
('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
('password', common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('private_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
('public_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(blank=True, editable=False, verbose_name='Date created')),
('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')),
@ -56,9 +56,9 @@ class Migration(migrations.Migration):
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
('password', common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('private_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
('public_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),

View File

@ -4,7 +4,6 @@ from rest_framework.response import Response
from rest_framework.decorators import action
from common.utils import get_logger, get_object_or_none
from common.utils.crypto import get_aes_crypto
from common.permissions import IsValidUser
from common.mixins.api import SuggestionMixin
from orgs.mixins.api import OrgBulkModelViewSet
@ -102,27 +101,17 @@ class SystemUserTempAuthInfoApi(generics.CreateAPIView):
permission_classes = (IsValidUser,)
serializer_class = SystemUserTempAuthSerializer
def decrypt_data_if_need(self, data):
csrf_token = self.request.META.get('CSRF_COOKIE')
aes = get_aes_crypto(csrf_token, 'ECB')
password = data.get('password', '')
try:
data['password'] = aes.decrypt(password)
except:
pass
return data
def create(self, request, *args, **kwargs):
serializer = super().get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
pk = kwargs.get('pk')
data = self.decrypt_data_if_need(serializer.validated_data)
instance_id = data.get('instance_id')
data = serializer.validated_data
asset_or_app_id = data.get('instance_id')
with tmp_to_root_org():
instance = get_object_or_404(SystemUser, pk=pk)
instance.set_temp_auth(instance_id, self.request.user.id, data)
instance.set_temp_auth(asset_or_app_id, self.request.user.id, data)
return Response(serializer.data, status=201)

View File

@ -1,7 +1,7 @@
# Generated by Django 2.1.7 on 2019-06-24 13:08
import assets.models.utils
import common.fields.model
import common.db.fields
from django.db import migrations
@ -15,61 +15,61 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='adminuser',
name='_password',
field=common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
field=common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
),
migrations.AlterField(
model_name='adminuser',
name='_private_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
field=common.db.fields.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
),
migrations.AlterField(
model_name='adminuser',
name='_public_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
),
migrations.AlterField(
model_name='authbook',
name='_password',
field=common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
field=common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
),
migrations.AlterField(
model_name='authbook',
name='_private_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
field=common.db.fields.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
),
migrations.AlterField(
model_name='authbook',
name='_public_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
),
migrations.AlterField(
model_name='gateway',
name='_password',
field=common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
field=common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
),
migrations.AlterField(
model_name='gateway',
name='_private_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
field=common.db.fields.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
),
migrations.AlterField(
model_name='gateway',
name='_public_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
),
migrations.AlterField(
model_name='systemuser',
name='_password',
field=common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
field=common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
),
migrations.AlterField(
model_name='systemuser',
name='_private_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
field=common.db.fields.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
),
migrations.AlterField(
model_name='systemuser',
name='_public_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
),
]

View File

@ -1,6 +1,6 @@
# Generated by Django 2.1.7 on 2019-07-11 12:18
import common.fields.model
import common.db.fields
from django.db import migrations
@ -14,21 +14,21 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='adminuser',
name='private_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
),
migrations.AlterField(
model_name='authbook',
name='private_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
),
migrations.AlterField(
model_name='gateway',
name='private_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
),
migrations.AlterField(
model_name='systemuser',
name='private_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
),
]

View File

@ -1,6 +1,6 @@
# Generated by Django 2.2.7 on 2019-12-06 07:26
import common.fields.model
import common.db.fields
from django.db import migrations, models
@ -36,7 +36,7 @@ class Migration(migrations.Migration):
('name', models.SlugField(allow_unicode=True, unique=True, verbose_name='Name')),
('base', models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Other', 'Other')], default='Linux', max_length=16, verbose_name='Base')),
('charset', models.CharField(choices=[('utf8', 'UTF-8'), ('gbk', 'GBK')], default='utf8', max_length=8, verbose_name='Charset')),
('meta', common.fields.model.JsonDictTextField(blank=True, null=True, verbose_name='Meta')),
('meta', common.db.fields.JsonDictTextField(blank=True, null=True, verbose_name='Meta')),
('internal', models.BooleanField(default=False, verbose_name='Internal')),
('comment', models.TextField(blank=True, null=True, verbose_name='Comment')),
],

View File

@ -1,6 +1,6 @@
# Generated by Django 3.1.6 on 2021-06-05 16:10
import common.fields.model
import common.db.fields
from django.conf import settings
import django.core.validators
from django.db import migrations, models
@ -58,9 +58,9 @@ class Migration(migrations.Migration):
('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
('password', common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('private_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
('public_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(blank=True, editable=False, verbose_name='Date created')),
('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')),

View File

@ -11,7 +11,7 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _
from rest_framework.exceptions import ValidationError
from common.fields.model import JsonDictTextField
from common.db.fields import JsonDictTextField
from common.utils import lazyproperty
from orgs.mixins.models import OrgModelMixin, OrgManager

View File

@ -19,7 +19,7 @@ from common.utils import (
)
from common.utils.encode import ssh_pubkey_gen
from common.validators import alphanumeric
from common import fields
from common.db import fields
from orgs.mixins.models import OrgModelMixin

View File

@ -6,12 +6,13 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from common.utils import ssh_pubkey_gen, ssh_private_key_gen, validate_ssh_private_key
from common.drf.fields import EncryptedField
from assets.models import Type
class AuthSerializer(serializers.ModelSerializer):
password = serializers.CharField(required=False, allow_blank=True, allow_null=True, max_length=1024)
private_key = serializers.CharField(required=False, allow_blank=True, allow_null=True, max_length=4096)
password = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=1024)
private_key = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=4096)
def gen_keys(self, private_key=None, password=None):
if private_key is None:
@ -31,6 +32,8 @@ class AuthSerializer(serializers.ModelSerializer):
class AuthSerializerMixin(serializers.ModelSerializer):
password = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=1024)
private_key = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=4096)
passphrase = serializers.CharField(
allow_blank=True, allow_null=True, required=False, max_length=512,
write_only=True, label=_('Key password')

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.14 on 2022-05-05 11:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('audits', '0013_auto_20211130_1037'),
]
operations = [
migrations.AlterField(
model_name='operatelog',
name='action',
field=models.CharField(choices=[('create', 'Create'), ('view', 'View'), ('update', 'Update'), ('delete', 'Delete')], max_length=16, verbose_name='Action'),
),
]

View File

@ -27,8 +27,10 @@ class TokenCreateApi(AuthMixin, CreateAPIView):
def create(self, request, *args, **kwargs):
self.create_session_if_need()
# 如果认证没有过,检查账号密码
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
try:
user = self.check_user_auth_if_need()
user = self.get_user_or_auth(serializer.validated_data)
self.check_user_mfa_if_need(user)
self.check_user_login_confirm_if_need(user)
self.send_auth_signal(success=True, user=user)

View File

@ -1,15 +1,25 @@
# -*- coding: utf-8 -*-
#
from django import forms
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from captcha.fields import CaptchaField, CaptchaTextInput
from common.utils import get_logger, rsa_decrypt_by_session_pkey
logger = get_logger(__name__)
class EncryptedField(forms.CharField):
def to_python(self, value):
value = super().to_python(value)
return rsa_decrypt_by_session_pkey(value)
class UserLoginForm(forms.Form):
days_auto_login = int(settings.SESSION_COOKIE_AGE / 3600 / 24)
disable_days_auto_login = settings.SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE or days_auto_login < 1
disable_days_auto_login = settings.SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE \
or days_auto_login < 1
username = forms.CharField(
label=_('Username'), max_length=100,
@ -18,7 +28,7 @@ class UserLoginForm(forms.Form):
'autofocus': 'autofocus'
})
)
password = forms.CharField(
password = EncryptedField(
label=_('Password'), widget=forms.PasswordInput,
max_length=1024, strip=False
)

View File

@ -1,8 +1,12 @@
import base64
from django.shortcuts import redirect, reverse
from django.utils.deprecation import MiddlewareMixin
from django.http import HttpResponse
from django.conf import settings
from common.utils import gen_key_pair
class MFAMiddleware:
"""
@ -48,3 +52,28 @@ class SessionCookieMiddleware(MiddlewareMixin):
return response
response.set_cookie(key, value)
return response
class EncryptedMiddleware:
def __init__(self, get_response):
self.get_response = get_response
@staticmethod
def check_key_pair(request, response):
pub_key_name = settings.SESSION_RSA_PUBLIC_KEY_NAME
public_key = request.session.get(pub_key_name)
cookie_key = request.COOKIES.get(pub_key_name)
if public_key and public_key == cookie_key:
return
pri_key_name = settings.SESSION_RSA_PRIVATE_KEY_NAME
private_key, public_key = gen_key_pair()
public_key_decode = base64.b64encode(public_key.encode()).decode()
request.session[pub_key_name] = public_key_decode
request.session[pri_key_name] = private_key
response.set_cookie(pub_key_name, public_key_decode)
def __call__(self, request):
response = self.get_response(request)
self.check_key_pair(request, response)
return response

View File

@ -23,9 +23,7 @@ from acls.models import LoginACL
from users.models import User
from users.utils import LoginBlockUtil, MFABlockUtils, LoginIpBlockUtil
from . import errors
from .utils import rsa_decrypt, gen_key_pair
from .signals import post_auth_success, post_auth_failed
from .const import RSA_PRIVATE_KEY, RSA_PUBLIC_KEY
logger = get_logger(__name__)
@ -91,46 +89,8 @@ def authenticate(request=None, **credentials):
auth.authenticate = authenticate
class PasswordEncryptionViewMixin:
request = None
def get_decrypted_password(self, password=None, username=None):
request = self.request
if hasattr(request, 'data'):
data = request.data
else:
data = request.POST
username = username or data.get('username')
password = password or data.get('password')
password = self.decrypt_passwd(password)
if not password:
self.raise_password_decrypt_failed(username=username)
return password
def raise_password_decrypt_failed(self, username):
ip = self.get_request_ip()
raise errors.CredentialError(
error=errors.reason_password_decrypt_failed,
username=username, ip=ip, request=self.request
)
def decrypt_passwd(self, raw_passwd):
# 获取解密密钥,对密码进行解密
rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
if rsa_private_key is None:
return raw_passwd
try:
return rsa_decrypt(raw_passwd, rsa_private_key)
except Exception as e:
logger.error(e, exc_info=True)
logger.error(
f'Decrypt password failed: password[{raw_passwd}] '
f'rsa_private_key[{rsa_private_key}]'
)
return None
class CommonMixin:
request: Request
def get_request_ip(self):
ip = ''
@ -139,26 +99,6 @@ class PasswordEncryptionViewMixin:
ip = ip or get_request_ip(self.request)
return ip
def get_context_data(self, **kwargs):
# 生成加解密密钥对public_key传递给前端private_key存入session中供解密使用
rsa_public_key = self.request.session.get(RSA_PUBLIC_KEY)
rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
if not all([rsa_private_key, rsa_public_key]):
rsa_private_key, rsa_public_key = gen_key_pair()
rsa_public_key = rsa_public_key.replace('\n', '\\n')
self.request.session[RSA_PRIVATE_KEY] = rsa_private_key
self.request.session[RSA_PUBLIC_KEY] = rsa_public_key
kwargs.update({
'rsa_public_key': rsa_public_key,
})
return super().get_context_data(**kwargs)
class CommonMixin(PasswordEncryptionViewMixin):
request: Request
get_request_ip: Callable
def raise_credential_error(self, error):
raise self.partial_credential_error(error=error)
@ -193,20 +133,13 @@ class CommonMixin(PasswordEncryptionViewMixin):
user.backend = self.request.session.get("auth_backend")
return user
def get_auth_data(self, decrypt_passwd=False):
def get_auth_data(self, data):
request = self.request
if hasattr(request, 'data'):
data = request.data
else:
data = request.POST
items = ['username', 'password', 'challenge', 'public_key', 'auto_login']
username, password, challenge, public_key, auto_login = bulk_get(data, items, default='')
ip = self.get_request_ip()
self._set_partial_credential_error(username=username, ip=ip, request=request)
if decrypt_passwd:
password = self.get_decrypted_password()
password = password + challenge.strip()
return username, password, public_key, ip, auto_login
@ -482,10 +415,10 @@ class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPost
need = cache.get(self.key_prefix_captcha.format(ip))
return need
def check_user_auth(self, decrypt_passwd=False):
def check_user_auth(self, valid_data=None):
# pre check
self.check_is_block()
username, password, public_key, ip, auto_login = self.get_auth_data(decrypt_passwd)
username, password, public_key, ip, auto_login = self.get_auth_data(valid_data)
self._check_only_allow_exists_user_auth(username)
# check auth
@ -537,11 +470,12 @@ class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPost
self.mark_password_ok(user, False)
return user
def check_user_auth_if_need(self, decrypt_passwd=False):
def get_user_or_auth(self, valid_data):
request = self.request
if not request.session.get('auth_password'):
return self.check_user_auth(decrypt_passwd=decrypt_passwd)
return self.get_user_from_session()
if request.session.get('auth_password'):
return self.get_user_from_session()
else:
return self.check_user_auth(valid_data)
def clear_auth_mark(self):
keys = ['auth_password', 'user_id', 'auth_confirm', 'auth_ticket_id']

View File

@ -2,6 +2,8 @@
#
from rest_framework import serializers
from common.drf.fields import EncryptedField
__all__ = [
'OtpVerifySerializer', 'MFAChallengeSerializer', 'MFASelectTypeSerializer',
@ -10,7 +12,7 @@ __all__ = [
class PasswordVerifySerializer(serializers.Serializer):
password = serializers.CharField()
password = EncryptedField()
class MFASelectTypeSerializer(serializers.Serializer):

View File

@ -161,6 +161,7 @@
<span style="font-size: 21px;font-weight:400;color: #151515;letter-spacing: 0;">{{ JMS_TITLE }}</span>
</div>
<div class="contact-form col-md-10 col-md-offset-1">
<form id="login-form" action="" method="post" role="form" novalidate="novalidate">
{% csrf_token %}
<div style="line-height: 17px;margin-bottom: 20px;color: #999999;">
@ -241,20 +242,10 @@
{% include '_foot_js.html' %}
<script type="text/javascript" src="/static/js/plugins/jsencrypt/jsencrypt.min.js"></script>
<script>
function encryptLoginPassword(password, rsaPublicKey) {
if (!password) {
return ''
}
var jsencrypt = new JSEncrypt(); //加密对象
jsencrypt.setPublicKey(rsaPublicKey); // 设置密钥
return jsencrypt.encrypt(password); //加密
}
function doLogin() {
//公钥加密
var rsaPublicKey = "{{ rsa_public_key }}"
var password = $('#password').val(); //明文密码
var passwordEncrypted = encryptLoginPassword(password, rsaPublicKey)
var passwordEncrypted = encryptPassword(password)
$('#password-hidden').val(passwordEncrypted); //返回给密码输入input
$('#login-form').submit(); //post提交
}

View File

@ -1,62 +1,22 @@
# -*- coding: utf-8 -*-
#
import base64
from Cryptodome.PublicKey import RSA
from Cryptodome.Cipher import PKCS1_v1_5
from Cryptodome import Random
from django.conf import settings
from .notifications import DifferentCityLoginMessage
from audits.models import UserLoginLog
from audits.const import DEFAULT_CITY
from common.utils import validate_ip, get_ip_city, get_request_ip
from common.utils import get_logger
from audits.models import UserLoginLog
from audits.const import DEFAULT_CITY
from .notifications import DifferentCityLoginMessage
logger = get_logger(__file__)
def gen_key_pair():
""" 生成加密key
用于登录页面提交用户名/密码时对密码进行加密前端/解密后端
"""
random_generator = Random.new().read
rsa = RSA.generate(1024, random_generator)
rsa_private_key = rsa.exportKey().decode()
rsa_public_key = rsa.publickey().exportKey().decode()
return rsa_private_key, rsa_public_key
def rsa_encrypt(message, rsa_public_key):
""" 加密登录密码 """
key = RSA.importKey(rsa_public_key)
cipher = PKCS1_v1_5.new(key)
cipher_text = base64.b64encode(cipher.encrypt(message.encode())).decode()
return cipher_text
def rsa_decrypt(cipher_text, rsa_private_key=None):
""" 解密登录密码 """
if rsa_private_key is None:
# rsa_private_key 为 None可以能是API请求认证不需要解密
return cipher_text
key = RSA.importKey(rsa_private_key)
cipher = PKCS1_v1_5.new(key)
cipher_decoded = base64.b64decode(cipher_text.encode())
# Todo: 弄明白为何要以下这么写https://xbuba.com/questions/57035263
if len(cipher_decoded) == 127:
hex_fixed = '00' + cipher_decoded.hex()
cipher_decoded = base64.b16decode(hex_fixed.upper())
message = cipher.decrypt(cipher_decoded, b'error').decode()
return message
def check_different_city_login_if_need(user, request):
if not settings.SECURITY_CHECK_DIFFERENT_CITY_LOGIN:
return
ip = get_request_ip(request) or '0.0.0.0'
if not (ip and validate_ip(ip)):
city = DEFAULT_CITY
else:

View File

@ -96,7 +96,7 @@ class UserLoginView(mixins.AuthMixin, FormView):
self.request.session.delete_test_cookie()
try:
self.check_user_auth(decrypt_passwd=True)
self.check_user_auth(form.cleaned_data)
except errors.AuthFailedError as e:
form.add_error(None, e.msg)
self.set_login_failed_mark()
@ -219,7 +219,7 @@ class UserLoginGuardView(mixins.AuthMixin, RedirectView):
def get_redirect_url(self, *args, **kwargs):
try:
user = self.check_user_auth_if_need()
user = self.get_user_from_session()
self.check_user_mfa_if_need(user)
self.check_user_login_confirm_if_need(user)
except (errors.CredentialError, errors.SessionEmptyError) as e:

View File

@ -5,7 +5,7 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import force_text
from django.core.validators import MinValueValidator, MaxValueValidator
from ..utils import signer, crypto
from common.utils import signer, crypto
__all__ = [

View File

@ -3,9 +3,10 @@
from rest_framework import serializers
from common.utils import rsa_decrypt_by_session_pkey
__all__ = [
'ReadableHiddenField',
'ReadableHiddenField', 'EncryptedField'
]
@ -23,3 +24,9 @@ class ReadableHiddenField(serializers.HiddenField):
if hasattr(value, 'id'):
return getattr(value, 'id')
return value
class EncryptedField(serializers.CharField):
def to_internal_value(self, value):
value = super().to_internal_value(value)
return rsa_decrypt_by_session_pkey(value)

View File

@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
#
from .model import *

View File

@ -1,7 +1,10 @@
import base64
from Cryptodome.Cipher import AES
import logging
from Cryptodome.Cipher import AES, PKCS1_v1_5
from Cryptodome.Util.Padding import pad
from Cryptodome.Random import get_random_bytes
from Cryptodome.PublicKey import RSA
from Cryptodome import Random
from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT
from django.conf import settings
@ -193,4 +196,55 @@ class Crypto:
continue
def gen_key_pair(length=1024):
""" 生成加密key
用于登录页面提交用户名/密码时对密码进行加密前端/解密后端
"""
random_generator = Random.new().read
rsa = RSA.generate(length, random_generator)
rsa_private_key = rsa.exportKey().decode()
rsa_public_key = rsa.publickey().exportKey().decode()
return rsa_private_key, rsa_public_key
def rsa_encrypt(message, rsa_public_key):
""" 加密登录密码 """
key = RSA.importKey(rsa_public_key)
cipher = PKCS1_v1_5.new(key)
cipher_text = base64.b64encode(cipher.encrypt(message.encode())).decode()
return cipher_text
def rsa_decrypt(cipher_text, rsa_private_key=None):
""" 解密登录密码 """
if rsa_private_key is None:
# rsa_private_key 为 None可以能是API请求认证不需要解密
return cipher_text
key = RSA.importKey(rsa_private_key)
cipher = PKCS1_v1_5.new(key)
cipher_decoded = base64.b64decode(cipher_text.encode())
# Todo: 弄明白为何要以下这么写https://xbuba.com/questions/57035263
if len(cipher_decoded) == 127:
hex_fixed = '00' + cipher_decoded.hex()
cipher_decoded = base64.b16decode(hex_fixed.upper())
message = cipher.decrypt(cipher_decoded, b'error').decode()
return message
def rsa_decrypt_by_session_pkey(value):
from jumpserver.utils import current_request
private_key_name = settings.SESSION_RSA_PRIVATE_KEY_NAME
private_key = current_request.session.get(private_key_name)
if not private_key or not value:
return value
try:
value= rsa_decrypt(value, private_key)
except Exception as e:
logging.error('Decrypt field error: {}'.format(e))
return value
crypto = Crypto()

View File

@ -95,6 +95,7 @@ MIDDLEWARE = [
'authentication.backends.cas.middleware.CASMiddleware',
'authentication.middleware.MFAMiddleware',
'authentication.middleware.SessionCookieMiddleware',
'authentication.middleware.EncryptedMiddleware',
'simple_history.middleware.HistoryRequestMiddleware',
]

View File

@ -169,3 +169,6 @@ ANNOUNCEMENT = CONFIG.ANNOUNCEMENT
# help
HELP_DOCUMENT_URL = CONFIG.HELP_DOCUMENT_URL
HELP_SUPPORT_URL = CONFIG.HELP_SUPPORT_URL
SESSION_RSA_PRIVATE_KEY_NAME = 'jms_private_key'
SESSION_RSA_PUBLIC_KEY_NAME = 'jms_public_key'

View File

@ -1,6 +1,6 @@
# Generated by Django 2.2.7 on 2019-12-17 09:58
import common.fields.model
import common.db.fields
from django.db import migrations
@ -18,17 +18,17 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='adhoc',
name='_become',
field=common.fields.model.EncryptJsonDictCharField(blank=True, null=True, default='', max_length=1024, verbose_name='Become'),
field=common.db.fields.EncryptJsonDictCharField(blank=True, null=True, default='', max_length=1024, verbose_name='Become'),
),
migrations.AlterField(
model_name='adhoc',
name='_options',
field=common.fields.model.JsonDictCharField(default='', max_length=1024, verbose_name='Options'),
field=common.db.fields.JsonDictCharField(default='', max_length=1024, verbose_name='Options'),
),
migrations.AlterField(
model_name='adhoc',
name='_tasks',
field=common.fields.model.JsonListTextField(verbose_name='Tasks'),
field=common.db.fields.JsonListTextField(verbose_name='Tasks'),
),
migrations.RenameField(
model_name='adhoc',
@ -48,12 +48,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='adhocrunhistory',
name='_result',
field=common.fields.model.JsonDictTextField(blank=True, null=True, verbose_name='Adhoc raw result'),
field=common.db.fields.JsonDictTextField(blank=True, null=True, verbose_name='Adhoc raw result'),
),
migrations.AlterField(
model_name='adhocrunhistory',
name='_summary',
field=common.fields.model.JsonDictTextField(blank=True, null=True, verbose_name='Adhoc result summary'),
field=common.db.fields.JsonDictTextField(blank=True, null=True, verbose_name='Adhoc result summary'),
),
migrations.RenameField(
model_name='adhocrunhistory',

View File

@ -1,6 +1,6 @@
# Generated by Django 2.2.7 on 2020-01-06 07:34
import common.fields.model
import common.db.fields
from django.db import migrations
@ -14,6 +14,6 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='adhoc',
name='become',
field=common.fields.model.EncryptJsonDictCharField(blank=True, default='', max_length=1024, null=True, verbose_name='Become'),
field=common.db.fields.EncryptJsonDictCharField(blank=True, default='', max_length=1024, null=True, verbose_name='Become'),
),
]

View File

@ -9,11 +9,11 @@ from celery import current_task
from django.db import models
from django.conf import settings
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _, gettext
from django.utils.translation import ugettext_lazy as _
from common.utils import get_logger, lazyproperty
from common.utils.translate import translate_value
from common.fields.model import (
from common.db.fields import (
JsonListTextField, JsonDictCharField, EncryptJsonDictCharField,
JsonDictTextField,
)

View File

@ -1,6 +1,8 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from common.drf.fields import EncryptedField
__all__ = [
'LDAPTestConfigSerializer', 'LDAPUserSerializer', 'LDAPTestLoginSerializer',
'LDAPSettingSerializer',
@ -20,7 +22,7 @@ class LDAPTestConfigSerializer(serializers.Serializer):
class LDAPTestLoginSerializer(serializers.Serializer):
username = serializers.CharField(max_length=1024, required=True)
password = serializers.CharField(max_length=2014, required=True)
password = EncryptedField(max_length=2014, required=True)
class LDAPUserSerializer(serializers.Serializer):

View File

@ -1501,3 +1501,19 @@ function getStatusIcon(status, mapping, title) {
}
return icon;
}
function encryptPassword(password) {
if (!password) {
return ''
}
var rsaPublicKeyText = getCookie('jms_public_key')
.replaceAll('"', '')
var rsaPublicKey = atob(rsaPublicKeyText)
var jsencrypt = new JSEncrypt(); //加密对象
jsencrypt.setPublicKey(rsaPublicKey); // 设置密钥
var value = jsencrypt.encrypt(password); //加密
return value
}
window.encryptPassword = encryptPassword

View File

@ -1,6 +1,6 @@
# Generated by Django 2.2.5 on 2019-11-22 10:07
import common.fields.model
import common.db.fields
from django.db import migrations, models
import uuid
@ -21,7 +21,7 @@ class Migration(migrations.Migration):
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('name', models.CharField(max_length=32, unique=True, verbose_name='Name')),
('type', models.CharField(choices=[('null', 'Null'), ('server', 'Server'), ('es', 'Elasticsearch')], default='server', max_length=16, verbose_name='Type')),
('meta', common.fields.model.EncryptJsonDictTextField(default={})),
('meta', common.db.fields.EncryptJsonDictTextField(default={})),
('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),
],
options={
@ -37,7 +37,7 @@ class Migration(migrations.Migration):
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('name', models.CharField(max_length=32, unique=True, verbose_name='Name')),
('type', models.CharField(choices=[('null', 'Null'), ('server', 'Server'), ('s3', 'S3'), ('ceph', 'Ceph'), ('swift', 'Swift'), ('oss', 'OSS'), ('azure', 'Azure')], default='server', max_length=16, verbose_name='Type')),
('meta', common.fields.model.EncryptJsonDictTextField(default={})),
('meta', common.db.fields.EncryptJsonDictTextField(default={})),
('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),
],
options={

View File

@ -1,7 +1,6 @@
# Generated by Django 3.1.14 on 2022-04-12 07:39
import copy
import common.fields.model
import common.db.fields
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
@ -83,13 +82,13 @@ class Migration(migrations.Migration):
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
('host', models.CharField(max_length=256, verbose_name='Host', blank=True)),
('https_port', common.fields.model.PortField(default=443, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='HTTPS Port')),
('http_port', common.fields.model.PortField(default=80, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='HTTP Port')),
('ssh_port', common.fields.model.PortField(default=2222, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='SSH Port')),
('rdp_port', common.fields.model.PortField(default=3389, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='RDP Port')),
('mysql_port', common.fields.model.PortField(default=33060, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='MySQL Port')),
('mariadb_port', common.fields.model.PortField(default=33061, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='MariaDB Port')),
('postgresql_port', common.fields.model.PortField(default=54320, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='PostgreSQL Port')),
('https_port', common.db.fields.PortField(default=443, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='HTTPS Port')),
('http_port', common.db.fields.PortField(default=80, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='HTTP Port')),
('ssh_port', common.db.fields.PortField(default=2222, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='SSH Port')),
('rdp_port', common.db.fields.PortField(default=3389, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='RDP Port')),
('mysql_port', common.db.fields.PortField(default=33060, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='MySQL Port')),
('mariadb_port', common.db.fields.PortField(default=33061, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='MariaDB Port')),
('postgresql_port', common.db.fields.PortField(default=54320, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='PostgreSQL Port')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
],
options={

View File

@ -2,7 +2,7 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.core.validators import MinValueValidator, MaxValueValidator
from common.db.models import JMSModel
from common.fields.model import PortField
from common.db.fields import PortField
from common.utils.ip import contains_ip

View File

@ -9,7 +9,7 @@ from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from common.mixins import CommonModelMixin
from common.utils import get_logger
from common.fields.model import EncryptJsonDictTextField
from common.db.fields import EncryptJsonDictTextField
from terminal.backends import TYPE_ENGINE_MAPPING
from .terminal import Terminal
from .command import Command

View File

@ -1,6 +1,6 @@
# Generated by Django 2.2.5 on 2019-11-15 06:57
import common.fields.model
import common.db.fields
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
@ -26,7 +26,7 @@ class Migration(migrations.Migration):
('user_display', models.CharField(max_length=128, verbose_name='User display name')),
('title', models.CharField(max_length=256, verbose_name='Title')),
('body', models.TextField(verbose_name='Body')),
('meta', common.fields.model.JsonDictTextField(default='{}', verbose_name='Meta')),
('meta', common.db.fields.JsonDictTextField(default='{}', verbose_name='Meta')),
('assignee_display', models.CharField(blank=True, max_length=128, null=True, verbose_name='Assignee display name')),
('assignees_display', models.CharField(blank=True, max_length=128, verbose_name='Assignees display name')),
('type', models.CharField(choices=[('general', 'General'), ('login_confirm', 'Login confirm')], default='general', max_length=16, verbose_name='Type')),

View File

@ -5,6 +5,7 @@ from django.utils.translation import gettext_lazy as _
from captcha.fields import CaptchaField
from common.utils import validate_ssh_public_key
from authentication.forms import EncryptedField
from ..models import User
@ -17,7 +18,7 @@ __all__ = [
class UserCheckPasswordForm(forms.Form):
password = forms.CharField(
password = EncryptedField(
label=_('Password'), widget=forms.PasswordInput,
max_length=1024, strip=False
)
@ -77,12 +78,12 @@ UserFirstLoginFinishForm.verbose_name = _("Finish")
class UserTokenResetPasswordForm(forms.Form):
new_password = forms.CharField(
new_password = EncryptedField(
min_length=5, max_length=128,
widget=forms.PasswordInput,
label=_("New password")
)
confirm_password = forms.CharField(
confirm_password = EncryptedField(
min_length=5, max_length=128,
widget=forms.PasswordInput,
label=_("Confirm password")
@ -103,7 +104,7 @@ class UserForgotPasswordForm(forms.Form):
class UserPasswordForm(UserTokenResetPasswordForm):
old_password = forms.CharField(
old_password = EncryptedField(
max_length=128, widget=forms.PasswordInput,
label=_("Old password")
)

View File

@ -1,6 +1,6 @@
# Generated by Django 2.1.7 on 2019-06-25 03:04
import common.fields.model
import common.db.fields
from django.db import migrations, models
@ -14,17 +14,17 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='user',
name='_otp_secret_key',
field=common.fields.model.EncryptCharField(blank=True, max_length=128, null=True),
field=common.db.fields.EncryptCharField(blank=True, max_length=128, null=True),
),
migrations.AlterField(
model_name='user',
name='_private_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='Private key'),
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Private key'),
),
migrations.AlterField(
model_name='user',
name='_public_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='Public key'),
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Public key'),
),
migrations.AlterField(
model_name='user',

View File

@ -1,6 +1,6 @@
# Generated by Django 3.1.13 on 2021-12-07 08:23
import common.fields.model
import common.db.fields
from django.db import migrations
@ -14,6 +14,6 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='user',
name='secret_key',
field=common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Secret key'),
field=common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Secret key'),
),
]

View File

@ -20,7 +20,7 @@ from django.shortcuts import reverse
from orgs.utils import current_org
from orgs.models import Organization
from rbac.const import Scope
from common import fields
from common.db import fields
from common.utils import (
date_expired_default, get_logger, lazyproperty, random_string, bulk_create_with_signal
)

View File

@ -3,6 +3,7 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from common.utils import validate_ssh_public_key
from common.drf.fields import EncryptedField
from ..models import User
from .user import UserSerializer
@ -16,9 +17,9 @@ class UserOrgSerializer(serializers.Serializer):
class UserUpdatePasswordSerializer(serializers.ModelSerializer):
old_password = serializers.CharField(required=True, max_length=128, write_only=True)
new_password = serializers.CharField(required=True, max_length=128, write_only=True)
new_password_again = serializers.CharField(required=True, max_length=128, write_only=True)
old_password = EncryptedField(required=True, max_length=128, write_only=True)
new_password = EncryptedField(required=True, max_length=128, write_only=True)
new_password_again = EncryptedField(required=True, max_length=128, write_only=True)
class Meta:
model = User
@ -41,11 +42,13 @@ class UserUpdatePasswordSerializer(serializers.ModelSerializer):
raise serializers.ValidationError(msg)
return value
def validate_new_password_again(self, value):
if value != self.initial_data.get('new_password', ''):
def validate(self, values):
new_password = values.get('new_password', '')
new_password_again = values.get('new_password_again', '')
if new_password != new_password_again:
msg = _('The newly set password is inconsistent')
raise serializers.ValidationError(msg)
return value
raise serializers.ValidationError({'new_password_again': msg})
return values
def update(self, instance, validated_data):
new_password = self.validated_data.get('new_password')

View File

@ -75,6 +75,19 @@ $(document).ready(function () {
var password = idPassword.val();
checkPasswordRules(password, minLength);
})
$("form").submit(function(){
// Let's find the input to check
var ids = ['id_new_password', 'id_confirm_password']
for (id of ids) {
var passwordRef = $('#' + id)
var value = passwordRef.val()
if (value) {
value = encryptPassword(value)
passwordRef.val(value)
}
}
});
})
</script>
</script>
{% endblock %}

View File

@ -15,10 +15,23 @@
{% endif %}
{% csrf_token %}
<div class="form-input form-group">
<input type="password" class="form-control" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required="">
<input type="password" id="password" class="form-control" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required="">
</div>
<button type="submit" class="btn btn-primary">{% trans 'Confirm' %}</button>
</form>
<script>
$("form").submit(function(){
// Let's find the input to check
var passwordRef = $('#password')
var value = passwordRef.val()
if (value) {
// Value is falsey (i.e. null), lets set a new one
value = encryptPassword(value)
passwordRef.val(value)
}
});
</script>
{% endblock %}

View File

@ -7,7 +7,7 @@ from django.shortcuts import redirect
from django.utils.translation import ugettext as _
from django.views.generic.edit import FormView
from authentication.mixins import PasswordEncryptionViewMixin, AuthMixin
from authentication.mixins import AuthMixin
from authentication import errors
from common.utils import get_logger
@ -31,7 +31,7 @@ class UserVerifyPasswordView(AuthMixin, FormView):
return redirect('authentication:login')
try:
password = self.get_decrypted_password(username=user.username)
password = form.cleaned_data['password']
except errors.AuthFailedError as e:
form.add_error("password", _(f"Password invalid") + f'({e.msg})')
return self.form_invalid(form)