mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-01 18:40:18 +08:00
feat: add organization's PasswordObfuscator to obfuscate login API's password (#3260)
* feat: add PasswordObfuscator to the login API * fix: change key error message * fix: remove unnecessary change * fix: fix one * fix: fix two * fix: fix three * fix: fix five * fix: disable organization update when key is invalid * fix: fix six * fix: use Form.Item to control key * fix: update obfuscator.js * Update obfuscator.go * Update obfuscator.go * Update auth.go * fix: remove real-time key monitoring --------- Co-authored-by: Yang Luo <hsluoyz@qq.com>
This commit is contained in:
@ -463,6 +463,15 @@ func (c *ApiController) Login() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
password := authForm.Password
|
password := authForm.Password
|
||||||
|
|
||||||
|
if application.OrganizationObj != nil {
|
||||||
|
password, err = util.GetUnobfuscatedPassword(application.OrganizationObj.PasswordObfuscatorType, application.OrganizationObj.PasswordObfuscatorKey, authForm.Password)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
isSigninViaLdap := authForm.SigninMethod == "LDAP"
|
isSigninViaLdap := authForm.SigninMethod == "LDAP"
|
||||||
var isPasswordWithLdapEnabled bool
|
var isPasswordWithLdapEnabled bool
|
||||||
if authForm.SigninMethod == "Password" {
|
if authForm.SigninMethod == "Password" {
|
||||||
|
@ -60,6 +60,8 @@ type Organization struct {
|
|||||||
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
|
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
|
||||||
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
|
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
|
||||||
PasswordOptions []string `xorm:"varchar(100)" json:"passwordOptions"`
|
PasswordOptions []string `xorm:"varchar(100)" json:"passwordOptions"`
|
||||||
|
PasswordObfuscatorType string `xorm:"varchar(100)" json:"passwordObfuscatorType"`
|
||||||
|
PasswordObfuscatorKey string `xorm:"varchar(100)" json:"passwordObfuscatorKey"`
|
||||||
CountryCodes []string `xorm:"varchar(200)" json:"countryCodes"`
|
CountryCodes []string `xorm:"varchar(200)" json:"countryCodes"`
|
||||||
DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"`
|
DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"`
|
||||||
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
|
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
|
||||||
|
76
util/obfuscator.go
Normal file
76
util/obfuscator.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/des"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func unPaddingPkcs7(s []byte) []byte {
|
||||||
|
length := len(s)
|
||||||
|
if length == 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
unPadding := int(s[length-1])
|
||||||
|
return s[:(length - unPadding)]
|
||||||
|
}
|
||||||
|
|
||||||
|
func decryptDesOrAes(passwordCipher string, block cipher.Block) (string, error) {
|
||||||
|
passwordCipherBytes, err := hex.DecodeString(passwordCipher)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(passwordCipherBytes) < block.BlockSize() {
|
||||||
|
return "", fmt.Errorf("the password ciphertext should contain a random hexadecimal string of length %d at the beginning", block.BlockSize()*2)
|
||||||
|
}
|
||||||
|
|
||||||
|
iv := passwordCipherBytes[:block.BlockSize()]
|
||||||
|
password := make([]byte, len(passwordCipherBytes)-block.BlockSize())
|
||||||
|
|
||||||
|
mode := cipher.NewCBCDecrypter(block, iv)
|
||||||
|
mode.CryptBlocks(password, passwordCipherBytes[block.BlockSize():])
|
||||||
|
|
||||||
|
return string(unPaddingPkcs7(password)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUnobfuscatedPassword(passwordObfuscatorType string, passwordObfuscatorKey string, passwordCipher string) (string, error) {
|
||||||
|
if passwordObfuscatorType == "Plain" || passwordObfuscatorType == "" {
|
||||||
|
return passwordCipher, nil
|
||||||
|
} else if passwordObfuscatorType == "DES" || passwordObfuscatorType == "AES" {
|
||||||
|
key, err := hex.DecodeString(passwordObfuscatorKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var block cipher.Block
|
||||||
|
if passwordObfuscatorType == "DES" {
|
||||||
|
block, err = des.NewCipher(key)
|
||||||
|
} else {
|
||||||
|
block, err = aes.NewCipher(key)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return decryptDesOrAes(passwordCipher, block)
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf("unsupported password obfuscator type: %s", passwordObfuscatorType)
|
||||||
|
}
|
||||||
|
}
|
@ -27,6 +27,7 @@
|
|||||||
"copy-to-clipboard": "^3.3.1",
|
"copy-to-clipboard": "^3.3.1",
|
||||||
"core-js": "^3.25.0",
|
"core-js": "^3.25.0",
|
||||||
"craco-less": "^2.0.0",
|
"craco-less": "^2.0.0",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
"echarts": "^5.4.3",
|
"echarts": "^5.4.3",
|
||||||
"ethers": "5.6.9",
|
"ethers": "5.6.9",
|
||||||
"face-api.js": "^0.22.2",
|
"face-api.js": "^0.22.2",
|
||||||
|
@ -19,6 +19,7 @@ import * as ApplicationBackend from "./backend/ApplicationBackend";
|
|||||||
import * as LdapBackend from "./backend/LdapBackend";
|
import * as LdapBackend from "./backend/LdapBackend";
|
||||||
import * as Setting from "./Setting";
|
import * as Setting from "./Setting";
|
||||||
import * as Conf from "./Conf";
|
import * as Conf from "./Conf";
|
||||||
|
import * as Obfuscator from "./auth/Obfuscator";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import {LinkOutlined} from "@ant-design/icons";
|
import {LinkOutlined} from "@ant-design/icons";
|
||||||
import LdapTable from "./table/LdapTable";
|
import LdapTable from "./table/LdapTable";
|
||||||
@ -112,6 +113,22 @@ class OrganizationEditPage extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updatePasswordObfuscator(key, value) {
|
||||||
|
const organization = this.state.organization;
|
||||||
|
if (organization.passwordObfuscatorType === "") {
|
||||||
|
organization.passwordObfuscatorType = "Plain";
|
||||||
|
}
|
||||||
|
if (key === "type") {
|
||||||
|
organization.passwordObfuscatorType = value;
|
||||||
|
organization.passwordObfuscatorKey = Obfuscator.getRandomKeyForObfuscator(value);
|
||||||
|
} else if (key === "key") {
|
||||||
|
organization.passwordObfuscatorKey = value;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
organization: organization,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
renderOrganization() {
|
renderOrganization() {
|
||||||
return (
|
return (
|
||||||
<Card size="small" title={
|
<Card size="small" title={
|
||||||
@ -294,6 +311,34 @@ class OrganizationEditPage extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("general:Password obfuscator"), i18next.t("general:Password obfuscator - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22} >
|
||||||
|
<Select virtual={false} style={{width: "100%"}}
|
||||||
|
value={this.state.organization.passwordObfuscatorType}
|
||||||
|
onChange={(value => {this.updatePasswordObfuscator("type", value);})}>
|
||||||
|
{
|
||||||
|
[
|
||||||
|
{id: "Plain", name: "Plain"},
|
||||||
|
{id: "AES", name: "AES"},
|
||||||
|
{id: "DES", name: "DES"},
|
||||||
|
].map((obfuscatorType, index) => <Option key={index} value={obfuscatorType.id}>{obfuscatorType.name}</Option>)
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
{
|
||||||
|
(this.state.organization.passwordObfuscatorType === "Plain" || this.state.organization.passwordObfuscatorType === "") ? null : (<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("general:Password obf key"), i18next.t("general:Password obf key - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22} >
|
||||||
|
<Input value={this.state.organization.passwordObfuscatorKey} onChange={(e) => {this.updatePasswordObfuscator("key", e.target.value);}} />
|
||||||
|
</Col>
|
||||||
|
</Row>)
|
||||||
|
}
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("general:Supported country codes"), i18next.t("general:Supported country codes - Tooltip"))} :
|
{Setting.getLabel(i18next.t("general:Supported country codes"), i18next.t("general:Supported country codes - Tooltip"))} :
|
||||||
@ -528,6 +573,12 @@ class OrganizationEditPage extends React.Component {
|
|||||||
const organization = Setting.deepCopy(this.state.organization);
|
const organization = Setting.deepCopy(this.state.organization);
|
||||||
organization.accountItems = organization.accountItems?.filter(accountItem => accountItem.name !== "Please select an account item");
|
organization.accountItems = organization.accountItems?.filter(accountItem => accountItem.name !== "Please select an account item");
|
||||||
|
|
||||||
|
const passwordObfuscatorErrorMessage = Obfuscator.checkPasswordObfuscator(organization.passwordObfuscatorType, organization.passwordObfuscatorKey);
|
||||||
|
if (passwordObfuscatorErrorMessage.length > 0) {
|
||||||
|
Setting.showMessage("error", passwordObfuscatorErrorMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
OrganizationBackend.updateOrganization(this.state.organization.owner, this.state.organizationName, organization)
|
OrganizationBackend.updateOrganization(this.state.organization.owner, this.state.organizationName, organization)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
|
@ -35,6 +35,8 @@ class OrganizationListPage extends BaseListPage {
|
|||||||
passwordType: "plain",
|
passwordType: "plain",
|
||||||
PasswordSalt: "",
|
PasswordSalt: "",
|
||||||
passwordOptions: [],
|
passwordOptions: [],
|
||||||
|
passwordObfuscatorType: "Plain",
|
||||||
|
passwordObfuscatorKey: "",
|
||||||
countryCodes: ["US"],
|
countryCodes: ["US"],
|
||||||
defaultAvatar: `${Setting.StaticBaseUrl}/img/casbin.svg`,
|
defaultAvatar: `${Setting.StaticBaseUrl}/img/casbin.svg`,
|
||||||
defaultApplication: "",
|
defaultApplication: "",
|
||||||
|
@ -19,6 +19,7 @@ import {withRouter} from "react-router-dom";
|
|||||||
import * as UserWebauthnBackend from "../backend/UserWebauthnBackend";
|
import * as UserWebauthnBackend from "../backend/UserWebauthnBackend";
|
||||||
import OrganizationSelect from "../common/select/OrganizationSelect";
|
import OrganizationSelect from "../common/select/OrganizationSelect";
|
||||||
import * as Conf from "../Conf";
|
import * as Conf from "../Conf";
|
||||||
|
import * as Obfuscator from "./Obfuscator";
|
||||||
import * as AuthBackend from "./AuthBackend";
|
import * as AuthBackend from "./AuthBackend";
|
||||||
import * as OrganizationBackend from "../backend/OrganizationBackend";
|
import * as OrganizationBackend from "../backend/OrganizationBackend";
|
||||||
import * as ApplicationBackend from "../backend/ApplicationBackend";
|
import * as ApplicationBackend from "../backend/ApplicationBackend";
|
||||||
@ -379,6 +380,14 @@ class LoginPage extends React.Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.state.loginMethod === "password" || this.state.loginMethod === "ldap") {
|
if (this.state.loginMethod === "password" || this.state.loginMethod === "ldap") {
|
||||||
|
const organization = this.getApplicationObj()?.organizationObj;
|
||||||
|
const [passwordCipher, errorMessage] = Obfuscator.encryptByPasswordObfuscator(organization?.passwordObfuscatorType, organization?.passwordObfuscatorKey, values["password"]);
|
||||||
|
if (errorMessage.length > 0) {
|
||||||
|
Setting.showMessage("error", errorMessage);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
values["password"] = passwordCipher;
|
||||||
|
}
|
||||||
if (this.state.enableCaptchaModal === CaptchaRule.Always) {
|
if (this.state.enableCaptchaModal === CaptchaRule.Always) {
|
||||||
this.setState({
|
this.setState({
|
||||||
openCaptchaModal: true,
|
openCaptchaModal: true,
|
||||||
|
95
web/src/auth/Obfuscator.js
Normal file
95
web/src/auth/Obfuscator.js
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import CryptoJS from "crypto-js";
|
||||||
|
import i18next from "i18next";
|
||||||
|
|
||||||
|
export function getRandomKeyForObfuscator(obfuscatorType) {
|
||||||
|
if (obfuscatorType === "DES") {
|
||||||
|
return getRandomHexKey(16);
|
||||||
|
} else if (obfuscatorType === "AES") {
|
||||||
|
return getRandomHexKey(32);
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const passwordObfuscatorKeyRegexes = {
|
||||||
|
"DES": /^[1-9a-f]{16}$/,
|
||||||
|
"AES": /^[1-9a-f]{32}$/,
|
||||||
|
};
|
||||||
|
|
||||||
|
function encrypt(cipher, key, iv, password) {
|
||||||
|
const encrypted = cipher.encrypt(
|
||||||
|
CryptoJS.enc.Hex.parse(Buffer.from(password, "utf-8").toString("hex")),
|
||||||
|
CryptoJS.enc.Hex.parse(key),
|
||||||
|
{
|
||||||
|
iv: iv,
|
||||||
|
mode: CryptoJS.mode.CBC,
|
||||||
|
pad: CryptoJS.pad.Pkcs7,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return iv.concat(encrypted.ciphertext).toString(CryptoJS.enc.Hex);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkPasswordObfuscator(passwordObfuscatorType, passwordObfuscatorKey) {
|
||||||
|
if (passwordObfuscatorType === undefined) {
|
||||||
|
return i18next.t("organization:failed to get password obfuscator");
|
||||||
|
} else if (passwordObfuscatorType === "Plain" || passwordObfuscatorType === "") {
|
||||||
|
return "";
|
||||||
|
} else if (passwordObfuscatorType === "AES" || passwordObfuscatorType === "DES") {
|
||||||
|
if (passwordObfuscatorKeyRegexes[passwordObfuscatorType].test(passwordObfuscatorKey)) {
|
||||||
|
return "";
|
||||||
|
} else {
|
||||||
|
return `${i18next.t("organization:The password obfuscator key doesn't match the regex")}: ${passwordObfuscatorKeyRegexes[passwordObfuscatorType].source}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return `${i18next.t("organization:unsupported password obfuscator type")}: ${passwordObfuscatorType}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encryptByPasswordObfuscator(passwordObfuscatorType, passwordObfuscatorKey, password) {
|
||||||
|
const passwordObfuscatorErrorMessage = checkPasswordObfuscator(passwordObfuscatorType, passwordObfuscatorKey);
|
||||||
|
if (passwordObfuscatorErrorMessage.length > 0) {
|
||||||
|
return ["", passwordObfuscatorErrorMessage];
|
||||||
|
} else {
|
||||||
|
if (passwordObfuscatorType === "Plain" || passwordObfuscatorType === "") {
|
||||||
|
return [password, ""];
|
||||||
|
} else if (passwordObfuscatorType === "AES") {
|
||||||
|
return [encryptByAes(passwordObfuscatorKey, password), ""];
|
||||||
|
} else if (passwordObfuscatorType === "DES") {
|
||||||
|
return [encryptByDes(passwordObfuscatorKey, password), ""];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function encryptByDes(key, password) {
|
||||||
|
const iv = CryptoJS.lib.WordArray.random(8);
|
||||||
|
return encrypt(CryptoJS.DES, key, iv, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
function encryptByAes(key, password) {
|
||||||
|
const iv = CryptoJS.lib.WordArray.random(16);
|
||||||
|
return encrypt(CryptoJS.AES, key, iv, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRandomHexKey(length) {
|
||||||
|
const characters = "123456789abcdef";
|
||||||
|
let key = "";
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
const randomIndex = Math.floor(Math.random() * characters.length);
|
||||||
|
key += characters[randomIndex];
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
@ -6101,6 +6101,11 @@ crypto-es@^1.2.2:
|
|||||||
resolved "https://registry.yarnpkg.com/crypto-es/-/crypto-es-1.2.7.tgz#754a6d52319a94fb4eb1f119297f17196b360f88"
|
resolved "https://registry.yarnpkg.com/crypto-es/-/crypto-es-1.2.7.tgz#754a6d52319a94fb4eb1f119297f17196b360f88"
|
||||||
integrity sha512-UUqiVJ2gUuZFmbFsKmud3uuLcNP2+Opt+5ysmljycFCyhA0+T16XJmo1ev/t5kMChMqWh7IEvURNCqsg+SjZGQ==
|
integrity sha512-UUqiVJ2gUuZFmbFsKmud3uuLcNP2+Opt+5ysmljycFCyhA0+T16XJmo1ev/t5kMChMqWh7IEvURNCqsg+SjZGQ==
|
||||||
|
|
||||||
|
crypto-js@^4.2.0:
|
||||||
|
version "4.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631"
|
||||||
|
integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==
|
||||||
|
|
||||||
crypto-random-string@^2.0.0:
|
crypto-random-string@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
|
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
|
||||||
|
Reference in New Issue
Block a user