diff --git a/controllers/auth.go b/controllers/auth.go
index acf2cf10..900cfd5b 100644
--- a/controllers/auth.go
+++ b/controllers/auth.go
@@ -463,6 +463,15 @@ func (c *ApiController) Login() {
}
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"
var isPasswordWithLdapEnabled bool
if authForm.SigninMethod == "Password" {
diff --git a/object/organization.go b/object/organization.go
index 097b5d7a..b54885e7 100644
--- a/object/organization.go
+++ b/object/organization.go
@@ -60,6 +60,8 @@ type Organization struct {
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
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"`
DefaultAvatar string `xorm:"varchar(200)" json:"defaultAvatar"`
DefaultApplication string `xorm:"varchar(100)" json:"defaultApplication"`
diff --git a/util/obfuscator.go b/util/obfuscator.go
new file mode 100644
index 00000000..deabb031
--- /dev/null
+++ b/util/obfuscator.go
@@ -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)
+ }
+}
diff --git a/web/package.json b/web/package.json
index 17fa87e6..3d9d210d 100644
--- a/web/package.json
+++ b/web/package.json
@@ -27,6 +27,7 @@
"copy-to-clipboard": "^3.3.1",
"core-js": "^3.25.0",
"craco-less": "^2.0.0",
+ "crypto-js": "^4.2.0",
"echarts": "^5.4.3",
"ethers": "5.6.9",
"face-api.js": "^0.22.2",
diff --git a/web/src/OrganizationEditPage.js b/web/src/OrganizationEditPage.js
index 4dcc468c..412cefc4 100644
--- a/web/src/OrganizationEditPage.js
+++ b/web/src/OrganizationEditPage.js
@@ -19,6 +19,7 @@ import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as LdapBackend from "./backend/LdapBackend";
import * as Setting from "./Setting";
import * as Conf from "./Conf";
+import * as Obfuscator from "./auth/Obfuscator";
import i18next from "i18next";
import {LinkOutlined} from "@ant-design/icons";
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() {
return (
+
+
+ {Setting.getLabel(i18next.t("general:Password obfuscator"), i18next.t("general:Password obfuscator - Tooltip"))} :
+
+
+
+
+
+ {
+ (this.state.organization.passwordObfuscatorType === "Plain" || this.state.organization.passwordObfuscatorType === "") ? null : (
+
+ {Setting.getLabel(i18next.t("general:Password obf key"), i18next.t("general:Password obf key - Tooltip"))} :
+
+
+ {this.updatePasswordObfuscator("key", e.target.value);}} />
+
+
)
+ }
{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);
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)
.then((res) => {
if (res.status === "ok") {
diff --git a/web/src/OrganizationListPage.js b/web/src/OrganizationListPage.js
index ec87adea..75e71a61 100644
--- a/web/src/OrganizationListPage.js
+++ b/web/src/OrganizationListPage.js
@@ -35,6 +35,8 @@ class OrganizationListPage extends BaseListPage {
passwordType: "plain",
PasswordSalt: "",
passwordOptions: [],
+ passwordObfuscatorType: "Plain",
+ passwordObfuscatorKey: "",
countryCodes: ["US"],
defaultAvatar: `${Setting.StaticBaseUrl}/img/casbin.svg`,
defaultApplication: "",
diff --git a/web/src/auth/LoginPage.js b/web/src/auth/LoginPage.js
index bac10773..c1151638 100644
--- a/web/src/auth/LoginPage.js
+++ b/web/src/auth/LoginPage.js
@@ -19,6 +19,7 @@ import {withRouter} from "react-router-dom";
import * as UserWebauthnBackend from "../backend/UserWebauthnBackend";
import OrganizationSelect from "../common/select/OrganizationSelect";
import * as Conf from "../Conf";
+import * as Obfuscator from "./Obfuscator";
import * as AuthBackend from "./AuthBackend";
import * as OrganizationBackend from "../backend/OrganizationBackend";
import * as ApplicationBackend from "../backend/ApplicationBackend";
@@ -379,6 +380,14 @@ class LoginPage extends React.Component {
return;
}
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) {
this.setState({
openCaptchaModal: true,
diff --git a/web/src/auth/Obfuscator.js b/web/src/auth/Obfuscator.js
new file mode 100644
index 00000000..fae8c15c
--- /dev/null
+++ b/web/src/auth/Obfuscator.js
@@ -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;
+}
diff --git a/web/yarn.lock b/web/yarn.lock
index 4d1220de..f6b61818 100644
--- a/web/yarn.lock
+++ b/web/yarn.lock
@@ -6101,6 +6101,11 @@ crypto-es@^1.2.2:
resolved "https://registry.yarnpkg.com/crypto-es/-/crypto-es-1.2.7.tgz#754a6d52319a94fb4eb1f119297f17196b360f88"
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:
version "2.0.0"
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"