From 26a07ca3c8af2031e174af7f9f65ca2cd82d0569 Mon Sep 17 00:00:00 2001 From: Carlos Ruiz Date: Tue, 31 Jul 2012 18:46:45 -0500 Subject: [PATCH] IDEMPIERE-347 passwords hash Thanks to Adaxa Australia -> http://adempiere.hg.sourceforge.net/hgweb/adempiere/contribution_adaxa/rev/6d9090d8a9f6 Ken Longnan for iDempiere integration work at -> https://bitbucket.org/longnan/idempiere-longnan Juliana Corredor for further integration and tests on iDempiere and making the process safer and backward compatible via sysconfig Carlos Ruiz (globalqss) peer reviewed, tested and finished some extra required Sponsored by Trek Global -> http://www.trekglobal.com/ --- .../oracle/854_PasswordHash_IDEMPIERE-347.sql | 78 +++++++ .../854_PasswordHash_IDEMPIERE-347.sql | 78 +++++++ .../org/compiere/process/HashPasswords.java | 78 +++++++ .../org/compiere/process/UserPassword.java | 68 +++--- .../src/org/compiere/model/GridTable.java | 11 +- .../src/org/compiere/model/I_AD_User.java | 13 ++ .../src/org/compiere/model/MTable.java | 21 +- .../src/org/compiere/model/MUser.java | 190 +++++++++++++--- .../src/org/compiere/model/PO.java | 2 + .../src/org/compiere/model/SystemIDs.java | 2 + .../src/org/compiere/model/X_AD_User.java | 19 ++ .../src/org/compiere/util/Login.java | 203 +++++++++++++----- .../src/org/compiere/util/Secure.java | 25 +++ .../src/org/compiere/util/SecureEngine.java | 18 ++ .../org/compiere/util/SecureInterface.java | 13 ++ .../src/org/compiere/util/WebUser.java | 83 ++++++- 16 files changed, 758 insertions(+), 144 deletions(-) create mode 100644 migration/360lts-release/oracle/854_PasswordHash_IDEMPIERE-347.sql create mode 100644 migration/360lts-release/postgresql/854_PasswordHash_IDEMPIERE-347.sql create mode 100644 org.adempiere.base.process/src/org/compiere/process/HashPasswords.java diff --git a/migration/360lts-release/oracle/854_PasswordHash_IDEMPIERE-347.sql b/migration/360lts-release/oracle/854_PasswordHash_IDEMPIERE-347.sql new file mode 100644 index 0000000000..040bf33eca --- /dev/null +++ b/migration/360lts-release/oracle/854_PasswordHash_IDEMPIERE-347.sql @@ -0,0 +1,78 @@ +-- 29/06/2011 1:33:27 PM +-- - +UPDATE AD_Column SET FieldLength=1024,Updated=TO_DATE('2011-06-29 13:33:27','YYYY-MM-DD HH24:MI:SS'),UpdatedBy=100 WHERE AD_Column_ID=417 +; + +-- 29/06/2011 1:33:31 PM +-- - +ALTER TABLE AD_User MODIFY Password NVARCHAR2(1024) DEFAULT NULL +; + +-- 29/06/2011 1:34:52 PM +-- - +INSERT INTO AD_Element (AD_Client_ID,AD_Element_ID,AD_Org_ID,ColumnName,Created,CreatedBy,Description,EntityType,IsActive,Name,PrintName,Updated,UpdatedBy) VALUES (0,55218,0,'Salt',TO_DATE('2011-06-29 13:34:50','YYYY-MM-DD HH24:MI:SS'),100,'Random data added to improve password hash effectiveness','D','Y','Salt','Salt',TO_DATE('2011-06-29 13:34:50','YYYY-MM-DD HH24:MI:SS'),100) +; + +-- 29/06/2011 1:34:52 PM +-- - +INSERT INTO AD_Element_Trl (AD_Language,AD_Element_ID, Description,Help,Name,PO_Description,PO_Help,PO_Name,PO_PrintName,PrintName, IsTranslated,AD_Client_ID,AD_Org_ID,Created,Createdby,Updated,UpdatedBy) SELECT l.AD_Language,t.AD_Element_ID, t.Description,t.Help,t.Name,t.PO_Description,t.PO_Help,t.PO_Name,t.PO_PrintName,t.PrintName, 'N',t.AD_Client_ID,t.AD_Org_ID,t.Created,t.Createdby,t.Updated,t.UpdatedBy FROM AD_Language l, AD_Element t WHERE l.IsActive='Y' AND l.IsSystemLanguage='Y' AND l.IsBaseLanguage='N' AND t.AD_Element_ID=55218 AND NOT EXISTS (SELECT * FROM AD_Element_Trl tt WHERE tt.AD_Language=l.AD_Language AND tt.AD_Element_ID=t.AD_Element_ID) +; + +-- 29/06/2011 1:36:39 PM +-- - +ALTER TABLE AD_User ADD Salt NVARCHAR2(16) DEFAULT NULL +; + + +-- 29/06/2011 1:37:48 PM +-- - +INSERT INTO AD_Column (AD_Client_ID,AD_Column_ID,AD_Element_ID,AD_Org_ID,AD_Reference_ID,AD_Table_ID,ColumnName,Created,CreatedBy,Description,EntityType,FieldLength,IsActive,IsAllowLogging,IsAlwaysUpdateable,IsAutocomplete,IsEncrypted,IsIdentifier,IsKey,IsMandatory,IsParent,IsSelectionColumn,IsSyncDatabase,IsTranslated,IsUpdateable,Name,SeqNo,Updated,UpdatedBy,Version) VALUES (0,61756,55218,0,10,114,'Salt',TO_DATE('2011-06-29 13:37:47','YYYY-MM-DD HH24:MI:SS'),100,'Random data added to improve password hash effectiveness','D',16,'Y','Y','N','N','N','N','N','N','N','N','N','N','N','Salt',0,TO_DATE('2011-06-29 13:37:47','YYYY-MM-DD HH24:MI:SS'),100,0) +; + +-- 29/06/2011 1:37:48 PM +-- - +INSERT INTO AD_Column_Trl (AD_Language,AD_Column_ID, Name, IsTranslated,AD_Client_ID,AD_Org_ID,Created,Createdby,Updated,UpdatedBy) SELECT l.AD_Language,t.AD_Column_ID, t.Name, 'N',t.AD_Client_ID,t.AD_Org_ID,t.Created,t.Createdby,t.Updated,t.UpdatedBy FROM AD_Language l, AD_Column t WHERE l.IsActive='Y' AND l.IsSystemLanguage='Y' AND l.IsBaseLanguage='N' AND t.AD_Column_ID=61756 AND NOT EXISTS (SELECT * FROM AD_Column_Trl tt WHERE tt.AD_Language=l.AD_Language AND tt.AD_Column_ID=t.AD_Column_ID) +; + + +-- 29/06/2011 4:59:43 PM +-- - +INSERT INTO AD_Process (AccessLevel,AD_Client_ID,AD_Org_ID,AD_Process_ID,Classname,CopyFromProcess,Created,CreatedBy,Description,EntityType,Help,IsActive,IsBetaFunctionality,IsDirectPrint,IsReport,IsServerProcess,Name,ShowHelp,Statistic_Count,Statistic_Seconds,Updated,UpdatedBy,Value) VALUES ('4',0,0,53259,'org.compiere.process.HashPasswords','N',TO_DATE('2011-06-29 16:59:41','YYYY-MM-DD HH24:MI:SS'),100,'Convert existing plain text/encrypted user passwords to one way hash','D','This process will overwrite existing user passwords with a salted SHA-512 hash of the password so that they cannot be recovered if your database is compromised. +(Note: If your password column is currently encrypted, the hash will also be encrypted.)','Y','N','N','N','N','Convert passwords to hashes','Y',0,0,TO_DATE('2011-06-29 16:59:41','YYYY-MM-DD HH24:MI:SS'),100,'AD_User_HashPassword') +; + +-- 29/06/2011 4:59:43 PM +-- - +INSERT INTO AD_Process_Trl (AD_Language,AD_Process_ID, Description,Help,Name, IsTranslated,AD_Client_ID,AD_Org_ID,Created,Createdby,Updated,UpdatedBy) SELECT l.AD_Language,t.AD_Process_ID, t.Description,t.Help,t.Name, 'N',t.AD_Client_ID,t.AD_Org_ID,t.Created,t.Createdby,t.Updated,t.UpdatedBy FROM AD_Language l, AD_Process t WHERE l.IsActive='Y' AND l.IsSystemLanguage='Y' AND l.IsBaseLanguage='N' AND t.AD_Process_ID=53259 AND NOT EXISTS (SELECT * FROM AD_Process_Trl tt WHERE tt.AD_Language=l.AD_Language AND tt.AD_Process_ID=t.AD_Process_ID) +; + +-- 29/06/2011 5:00:28 PM +-- - +INSERT INTO AD_Menu (Action,AD_Client_ID,AD_Menu_ID,AD_Org_ID,AD_Process_ID,Created,CreatedBy,EntityType,IsActive,IsCentrallyMaintained,IsReadOnly,IsSOTrx,IsSummary,Name,Updated,UpdatedBy) VALUES ('P',0,53348,0,53259,TO_DATE('2011-06-29 17:00:27','YYYY-MM-DD HH24:MI:SS'),100,'D','Y','Y','N','N','N','Hash Passwords',TO_DATE('2011-06-29 17:00:27','YYYY-MM-DD HH24:MI:SS'),100) +; + +-- 29/06/2011 5:00:28 PM +-- - +INSERT INTO AD_Menu_Trl (AD_Language,AD_Menu_ID, Description,Name, IsTranslated,AD_Client_ID,AD_Org_ID,Created,Createdby,Updated,UpdatedBy) SELECT l.AD_Language,t.AD_Menu_ID, t.Description,t.Name, 'N',t.AD_Client_ID,t.AD_Org_ID,t.Created,t.Createdby,t.Updated,t.UpdatedBy FROM AD_Language l, AD_Menu t WHERE l.IsActive='Y' AND l.IsSystemLanguage='Y' AND l.IsBaseLanguage='N' AND t.AD_Menu_ID=53348 AND NOT EXISTS (SELECT * FROM AD_Menu_Trl tt WHERE tt.AD_Language=l.AD_Language AND tt.AD_Menu_ID=t.AD_Menu_ID) +; + +-- 29/06/2011 5:00:28 PM +-- - +INSERT INTO AD_TreeNodeMM (AD_Client_ID,AD_Org_ID, IsActive,Created,CreatedBy,Updated,UpdatedBy, AD_Tree_ID, Node_ID, Parent_ID, SeqNo) SELECT t.AD_Client_ID, 0, 'Y', SysDate, 100, SysDate, 100,t.AD_Tree_ID, 53348, 0, 999 FROM AD_Tree t WHERE t.AD_Client_ID=0 AND t.IsActive='Y' AND t.IsAllNodes='Y' AND t.TreeType='MM' AND NOT EXISTS (SELECT * FROM AD_TreeNodeMM e WHERE e.AD_Tree_ID=t.AD_Tree_ID AND Node_ID=53348) +; + +-- 29/06/2011 5:00:53 PM +-- - +UPDATE AD_TreeNodeMM SET Parent_ID=367, SeqNo=999, Updated=SysDate WHERE AD_Tree_ID=10 AND Node_ID=53348 +; + +-- Jul 24, 2012 5:50:06 PM COT +INSERT INTO AD_SysConfig (AD_SysConfig_ID,EntityType,ConfigurationLevel,Value,Description,AD_SysConfig_UU,Created,Updated,AD_Client_ID,AD_Org_ID,CreatedBy,IsActive,UpdatedBy,Name) VALUES (200013,'D','S','N','Enable hash passwords, please use Hash Password process to enable','569577c3-3bfa-4d0f-a2e3-d98752164667',TO_DATE('2012-07-24 17:50:04','YYYY-MM-DD HH24:MI:SS'),TO_DATE('2012-07-24 17:50:04','YYYY-MM-DD HH24:MI:SS'),0,0,0,'Y',0,'USER_PASSWORD_HASH') +; + +UPDATE AD_System + SET LastMigrationScriptApplied='854_PasswordHash_IDEMPIERE-347.sql' +WHERE LastMigrationScriptApplied<'854_PasswordHash_IDEMPIERE-347.sql' + OR LastMigrationScriptApplied IS NULL +; + diff --git a/migration/360lts-release/postgresql/854_PasswordHash_IDEMPIERE-347.sql b/migration/360lts-release/postgresql/854_PasswordHash_IDEMPIERE-347.sql new file mode 100644 index 0000000000..3487f38294 --- /dev/null +++ b/migration/360lts-release/postgresql/854_PasswordHash_IDEMPIERE-347.sql @@ -0,0 +1,78 @@ +-- 29/06/2011 1:33:27 PM +-- - +UPDATE AD_Column SET FieldLength=1024,Updated=TO_TIMESTAMP('2011-06-29 13:33:27','YYYY-MM-DD HH24:MI:SS'),UpdatedBy=100 WHERE AD_Column_ID=417 +; + +-- 29/06/2011 1:33:31 PM +-- - +INSERT INTO t_alter_column values('ad_user','Password','VARCHAR(1024)',null,'NULL') +; + +-- 29/06/2011 1:34:52 PM +-- - +INSERT INTO AD_Element (AD_Client_ID,AD_Element_ID,AD_Org_ID,ColumnName,Created,CreatedBy,Description,EntityType,IsActive,Name,PrintName,Updated,UpdatedBy) VALUES (0,55218,0,'Salt',TO_TIMESTAMP('2011-06-29 13:34:50','YYYY-MM-DD HH24:MI:SS'),100,'Random data added to improve password hash effectiveness','D','Y','Salt','Salt',TO_TIMESTAMP('2011-06-29 13:34:50','YYYY-MM-DD HH24:MI:SS'),100) +; + +-- 29/06/2011 1:34:52 PM +-- - +INSERT INTO AD_Element_Trl (AD_Language,AD_Element_ID, Description,Help,Name,PO_Description,PO_Help,PO_Name,PO_PrintName,PrintName, IsTranslated,AD_Client_ID,AD_Org_ID,Created,Createdby,Updated,UpdatedBy) SELECT l.AD_Language,t.AD_Element_ID, t.Description,t.Help,t.Name,t.PO_Description,t.PO_Help,t.PO_Name,t.PO_PrintName,t.PrintName, 'N',t.AD_Client_ID,t.AD_Org_ID,t.Created,t.Createdby,t.Updated,t.UpdatedBy FROM AD_Language l, AD_Element t WHERE l.IsActive='Y' AND l.IsSystemLanguage='Y' AND l.IsBaseLanguage='N' AND t.AD_Element_ID=55218 AND NOT EXISTS (SELECT * FROM AD_Element_Trl tt WHERE tt.AD_Language=l.AD_Language AND tt.AD_Element_ID=t.AD_Element_ID) +; + +-- 29/06/2011 1:36:39 PM +-- - +ALTER TABLE AD_User ADD COLUMN Salt VARCHAR(16) DEFAULT NULL +; + + +-- 29/06/2011 1:37:48 PM +-- - +INSERT INTO AD_Column (AD_Client_ID,AD_Column_ID,AD_Element_ID,AD_Org_ID,AD_Reference_ID,AD_Table_ID,ColumnName,Created,CreatedBy,Description,EntityType,FieldLength,IsActive,IsAllowLogging,IsAlwaysUpdateable,IsAutocomplete,IsEncrypted,IsIdentifier,IsKey,IsMandatory,IsParent,IsSelectionColumn,IsSyncDatabase,IsTranslated,IsUpdateable,Name,SeqNo,Updated,UpdatedBy,Version) VALUES (0,61756,55218,0,10,114,'Salt',TO_TIMESTAMP('2011-06-29 13:37:47','YYYY-MM-DD HH24:MI:SS'),100,'Random data added to improve password hash effectiveness','D',16,'Y','Y','N','N','N','N','N','N','N','N','N','N','N','Salt',0,TO_TIMESTAMP('2011-06-29 13:37:47','YYYY-MM-DD HH24:MI:SS'),100,0) +; + +-- 29/06/2011 1:37:48 PM +-- - +INSERT INTO AD_Column_Trl (AD_Language,AD_Column_ID, Name, IsTranslated,AD_Client_ID,AD_Org_ID,Created,Createdby,Updated,UpdatedBy) SELECT l.AD_Language,t.AD_Column_ID, t.Name, 'N',t.AD_Client_ID,t.AD_Org_ID,t.Created,t.Createdby,t.Updated,t.UpdatedBy FROM AD_Language l, AD_Column t WHERE l.IsActive='Y' AND l.IsSystemLanguage='Y' AND l.IsBaseLanguage='N' AND t.AD_Column_ID=61756 AND NOT EXISTS (SELECT * FROM AD_Column_Trl tt WHERE tt.AD_Language=l.AD_Language AND tt.AD_Column_ID=t.AD_Column_ID) +; + + +-- 29/06/2011 4:59:43 PM +-- - +INSERT INTO AD_Process (AccessLevel,AD_Client_ID,AD_Org_ID,AD_Process_ID,Classname,CopyFromProcess,Created,CreatedBy,Description,EntityType,Help,IsActive,IsBetaFunctionality,IsDirectPrint,IsReport,IsServerProcess,Name,ShowHelp,Statistic_Count,Statistic_Seconds,Updated,UpdatedBy,Value) VALUES ('4',0,0,53259,'org.compiere.process.HashPasswords','N',TO_TIMESTAMP('2011-06-29 16:59:41','YYYY-MM-DD HH24:MI:SS'),100,'Convert existing plain text/encrypted user passwords to one way hash','D','This process will overwrite existing user passwords with a salted SHA-512 hash of the password so that they cannot be recovered if your database is compromised. +(Note: If your password column is currently encrypted, the hash will also be encrypted.)','Y','N','N','N','N','Convert passwords to hashes','Y',0,0,TO_TIMESTAMP('2011-06-29 16:59:41','YYYY-MM-DD HH24:MI:SS'),100,'AD_User_HashPassword') +; + +-- 29/06/2011 4:59:43 PM +-- - +INSERT INTO AD_Process_Trl (AD_Language,AD_Process_ID, Description,Help,Name, IsTranslated,AD_Client_ID,AD_Org_ID,Created,Createdby,Updated,UpdatedBy) SELECT l.AD_Language,t.AD_Process_ID, t.Description,t.Help,t.Name, 'N',t.AD_Client_ID,t.AD_Org_ID,t.Created,t.Createdby,t.Updated,t.UpdatedBy FROM AD_Language l, AD_Process t WHERE l.IsActive='Y' AND l.IsSystemLanguage='Y' AND l.IsBaseLanguage='N' AND t.AD_Process_ID=53259 AND NOT EXISTS (SELECT * FROM AD_Process_Trl tt WHERE tt.AD_Language=l.AD_Language AND tt.AD_Process_ID=t.AD_Process_ID) +; + +-- 29/06/2011 5:00:28 PM +-- - +INSERT INTO AD_Menu (Action,AD_Client_ID,AD_Menu_ID,AD_Org_ID,AD_Process_ID,Created,CreatedBy,EntityType,IsActive,IsCentrallyMaintained,IsReadOnly,IsSOTrx,IsSummary,Name,Updated,UpdatedBy) VALUES ('P',0,53348,0,53259,TO_TIMESTAMP('2011-06-29 17:00:27','YYYY-MM-DD HH24:MI:SS'),100,'D','Y','Y','N','N','N','Hash Passwords',TO_TIMESTAMP('2011-06-29 17:00:27','YYYY-MM-DD HH24:MI:SS'),100) +; + +-- 29/06/2011 5:00:28 PM +-- - +INSERT INTO AD_Menu_Trl (AD_Language,AD_Menu_ID, Description,Name, IsTranslated,AD_Client_ID,AD_Org_ID,Created,Createdby,Updated,UpdatedBy) SELECT l.AD_Language,t.AD_Menu_ID, t.Description,t.Name, 'N',t.AD_Client_ID,t.AD_Org_ID,t.Created,t.Createdby,t.Updated,t.UpdatedBy FROM AD_Language l, AD_Menu t WHERE l.IsActive='Y' AND l.IsSystemLanguage='Y' AND l.IsBaseLanguage='N' AND t.AD_Menu_ID=53348 AND NOT EXISTS (SELECT * FROM AD_Menu_Trl tt WHERE tt.AD_Language=l.AD_Language AND tt.AD_Menu_ID=t.AD_Menu_ID) +; + +-- 29/06/2011 5:00:28 PM +-- - +INSERT INTO AD_TreeNodeMM (AD_Client_ID,AD_Org_ID, IsActive,Created,CreatedBy,Updated,UpdatedBy, AD_Tree_ID, Node_ID, Parent_ID, SeqNo) SELECT t.AD_Client_ID, 0, 'Y', CURRENT_TIMESTAMP, 100, CURRENT_TIMESTAMP, 100,t.AD_Tree_ID, 53348, 0, 999 FROM AD_Tree t WHERE t.AD_Client_ID=0 AND t.IsActive='Y' AND t.IsAllNodes='Y' AND t.TreeType='MM' AND NOT EXISTS (SELECT * FROM AD_TreeNodeMM e WHERE e.AD_Tree_ID=t.AD_Tree_ID AND Node_ID=53348) +; + +-- 29/06/2011 5:00:53 PM +-- - +UPDATE AD_TreeNodeMM SET Parent_ID=367, SeqNo=999, Updated=CURRENT_TIMESTAMP WHERE AD_Tree_ID=10 AND Node_ID=53348 +; + +-- Jul 24, 2012 5:50:06 PM COT +INSERT INTO AD_SysConfig (AD_SysConfig_ID,EntityType,ConfigurationLevel,Value,Description,AD_SysConfig_UU,Created,Updated,AD_Client_ID,AD_Org_ID,CreatedBy,IsActive,UpdatedBy,Name) VALUES (200013,'D','S','N','Enable hash passwords, please use Hash Password process to enable','569577c3-3bfa-4d0f-a2e3-d98752164667',TO_TIMESTAMP('2012-07-24 17:50:04','YYYY-MM-DD HH24:MI:SS'),TO_TIMESTAMP('2012-07-24 17:50:04','YYYY-MM-DD HH24:MI:SS'),0,0,0,'Y',0,'USER_PASSWORD_HASH') +; + +UPDATE AD_System + SET LastMigrationScriptApplied='854_PasswordHash_IDEMPIERE-347.sql' +WHERE LastMigrationScriptApplied<'854_PasswordHash_IDEMPIERE-347.sql' + OR LastMigrationScriptApplied IS NULL +; + diff --git a/org.adempiere.base.process/src/org/compiere/process/HashPasswords.java b/org.adempiere.base.process/src/org/compiere/process/HashPasswords.java new file mode 100644 index 0000000000..f294650a08 --- /dev/null +++ b/org.adempiere.base.process/src/org/compiere/process/HashPasswords.java @@ -0,0 +1,78 @@ +/****************************************************************************** + * Product: Adempiere ERP & CRM Smart Business Solution * + * Copyright (C) 1999-2006 ComPiere, Inc. All Rights Reserved. * + * This program is free software; you can redistribute it and/or modify it * + * under the terms version 2 of the GNU General Public License as published * + * by the Free Software Foundation. This program is distributed in the hope * + * that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * + * See the GNU General Public License for more details. * + * You should have received a copy of the GNU General Public License along * + * with this program; if not, write to the Free Software Foundation, Inc., * + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. * + * For the text or an alternative of this public license, you may reach us * + * ComPiere, Inc., 2620 Augustine Dr. #245, Santa Clara, CA 95054, USA * + * or via info@compiere.org or http://www.compiere.org/license.html * + *****************************************************************************/ +package org.compiere.process; + +import java.util.List; + +import org.adempiere.exceptions.AdempiereException; +import org.compiere.model.MSysConfig; +import org.compiere.model.MTable; +import org.compiere.model.MUser; +import org.compiere.model.SystemIDs; + +/** + * Hash existing passwords + * + */ +public class HashPasswords extends SvrProcess +{ + /** + * Prepare - e.g., get Parameters. + */ + protected void prepare() + { + } // prepare + + /** + * Perform process. + * @return Message + * @throws Exception if not successful + */ + protected String doIt() throws Exception + { + boolean hash_password = MSysConfig.getBooleanValue("USER_PASSWORD_HASH", false); + if (hash_password) + throw new AdempiereException("Passwords already hashed"); + + String where = " Password IS NOT NULL AND Salt IS NULL "; + + // update the sysconfig key to Y out of trx and reset the cache + MSysConfig conf = new MSysConfig(getCtx(), SystemIDs.SYSCONFIG_USER_HASH_PASSWORD, null); + conf.setValue("Y"); + conf.saveEx(); + MSysConfig.resetCache(); + + int count = 0; + try { + List users = MTable.get(getCtx(), MUser.Table_ID).createQuery( where, get_TrxName()).list(); + for ( MUser user : users ) + { + user.setPassword(user.getPassword()); + count++; + user.saveEx(); + } + } catch (Exception e) { + // reset to N on exception + conf.setValue("N"); + conf.saveEx(); + throw e; + } + + return "@Updated@ " + count; + } // doIt + +} // HashPasswords diff --git a/org.adempiere.base.process/src/org/compiere/process/UserPassword.java b/org.adempiere.base.process/src/org/compiere/process/UserPassword.java index d8b00aefed..ec17c8bd0e 100644 --- a/org.adempiere.base.process/src/org/compiere/process/UserPassword.java +++ b/org.adempiere.base.process/src/org/compiere/process/UserPassword.java @@ -18,8 +18,8 @@ package org.compiere.process; import java.util.logging.Level; +import org.compiere.model.MSysConfig; import org.compiere.model.MUser; -import org.compiere.util.DB; import org.compiere.util.Util; /** @@ -77,57 +77,39 @@ public class UserPassword extends SvrProcess MUser user = MUser.get(getCtx(), p_AD_User_ID); MUser operator = MUser.get(getCtx(), getAD_User_ID()); log.fine("User=" + user + ", Operator=" + operator); - - + + boolean hash_password = MSysConfig.getBooleanValue("USER_PASSWORD_HASH", false); // Do we need a password ? if (Util.isEmpty(p_OldPassword)) // Password required { if (p_AD_User_ID == 0 // change of System - || p_AD_User_ID == 100 // change of SuperUser - || !operator.isAdministrator()) + || p_AD_User_ID == 100 // change of SuperUser + || !operator.isAdministrator()) throw new IllegalArgumentException("@OldPasswordMandatory@"); } - // is entered Password correct ? - else if (!p_OldPassword.equals(user.getPassword())) - throw new IllegalArgumentException("@OldPasswordNoMatch@"); + else { + if (hash_password){ + if (!user.authenticateHash(p_OldPassword) ) + throw new IllegalArgumentException("@OldPasswordNoMatch@"); + } else{ + if (!p_OldPassword.equals(user.getPassword())) + throw new IllegalArgumentException("@OldPasswordNoMatch@"); + } + } - // Change Super User - if (p_AD_User_ID == 0) - { - String sql = "UPDATE AD_User SET Updated=SysDate, UpdatedBy=" + getAD_User_ID(); - if (!Util.isEmpty(p_NewPassword)) - sql += ", Password=" + DB.TO_STRING(p_NewPassword); - if (!Util.isEmpty(p_NewEMail)) - sql += ", Email=" + DB.TO_STRING(p_NewEMail); - if (!Util.isEmpty(p_NewEMailUser)) - sql += ", EmailUser=" + DB.TO_STRING(p_NewEMailUser); - if (!Util.isEmpty(p_NewEMailUserPW)) - sql += ", EmailUserPW=" + DB.TO_STRING(p_NewEMailUserPW); - sql += " WHERE AD_User_ID=0"; - if (DB.executeUpdate(sql, get_TrxName()) == 1) - return "OK"; - else - return "@Error@"; - } - else - { - if (!Util.isEmpty(p_NewPassword)) - user.setPassword(p_NewPassword); - if (!Util.isEmpty(p_NewEMail)) - user.setEMail(p_NewEMail); - if (!Util.isEmpty(p_NewEMailUser)) - user.setEMailUser(p_NewEMailUser); - if (!Util.isEmpty(p_NewEMailUserPW)) - user.setEMailUserPW(p_NewEMailUserPW); - // - if (user.save()) - return "OK"; - else - return "@Error@"; - } + if (!Util.isEmpty(p_NewPassword)) + user.setPassword(p_NewPassword); + if (!Util.isEmpty(p_NewEMail)) + user.setEMail(p_NewEMail); + if (!Util.isEmpty(p_NewEMailUser)) + user.setEMailUser(p_NewEMailUser); + if (!Util.isEmpty(p_NewEMailUserPW)) + user.setEMailUserPW(p_NewEMailUserPW); + user.saveEx(); + + return "OK"; } // doIt } // UserPassword - diff --git a/org.adempiere.base/src/org/compiere/model/GridTable.java b/org.adempiere.base/src/org/compiere/model/GridTable.java index 3fa2501f29..c975d2df22 100644 --- a/org.adempiere.base/src/org/compiere/model/GridTable.java +++ b/org.adempiere.base/src/org/compiere/model/GridTable.java @@ -1443,17 +1443,8 @@ public class GridTable extends AbstractTableModel && (Env.getAD_User_ID(m_ctx) == USER_SYSTEM || Env.getAD_User_ID(m_ctx) == USER_SUPERUSER) // user must know what is doing -> just allowed to System or SuperUser (Hardcoded) && getKeyID(m_rowChanged) == 0) { // the record being changed has ID = 0 String tablename = getTableName(); // just the allowed tables (HardCoded) - if (tablename.equals("AD_Org") || - tablename.equals("AD_ReportView") || - tablename.equals("AD_Role") || - tablename.equals("AD_System") || - tablename.equals("AD_User") || - tablename.equals("C_DocType") || - tablename.equals("GL_Category") || - tablename.equals("M_AttributeSet") || - tablename.equals("M_AttributeSetInstance")) { + if (MTable.isZeroIDTable(tablename)) specialZeroUpdate = true; - } } // Check Mandatory diff --git a/org.adempiere.base/src/org/compiere/model/I_AD_User.java b/org.adempiere.base/src/org/compiere/model/I_AD_User.java index e2d9f4f216..cf389ddd6b 100644 --- a/org.adempiere.base/src/org/compiere/model/I_AD_User.java +++ b/org.adempiere.base/src/org/compiere/model/I_AD_User.java @@ -458,6 +458,19 @@ public interface I_AD_User /** Get Process Now */ public boolean isProcessing(); + + /** Column name Salt */ + public static final String COLUMNNAME_Salt = "Salt"; + + /** Set Salt. + * Random data added to improve password hash effectiveness + */ + public void setSalt (String Salt); + + /** Get Salt. + * Random data added to improve password hash effectiveness + */ + public String getSalt(); /** Column name Supervisor_ID */ public static final String COLUMNNAME_Supervisor_ID = "Supervisor_ID"; diff --git a/org.adempiere.base/src/org/compiere/model/MTable.java b/org.adempiere.base/src/org/compiere/model/MTable.java index ec7b4895dd..3b98b4a2bf 100644 --- a/org.adempiere.base/src/org/compiere/model/MTable.java +++ b/org.adempiere.base/src/org/compiere/model/MTable.java @@ -50,11 +50,10 @@ import org.compiere.util.DB; */ public class MTable extends X_AD_Table { - /** - * + * */ - private static final long serialVersionUID = -2367316254623142732L; + private static final long serialVersionUID = 8264472455498363565L; public final static int MAX_OFFICIAL_ID = 999999; @@ -587,4 +586,20 @@ public class MTable extends X_AD_Table return sb.toString (); } // toString + /** + * Verify if the table contains ID=0 + * @return true if table has zero ID + */ + public static boolean isZeroIDTable(String tablename) { + return (tablename.equals("AD_Org") || + tablename.equals("AD_ReportView") || + tablename.equals("AD_Role") || + tablename.equals("AD_System") || + tablename.equals("AD_User") || + tablename.equals("C_DocType") || + tablename.equals("GL_Category") || + tablename.equals("M_AttributeSet") || + tablename.equals("M_AttributeSetInstance")); + } + } // MTable \ No newline at end of file diff --git a/org.adempiere.base/src/org/compiere/model/MUser.java b/org.adempiere.base/src/org/compiere/model/MUser.java index 522befcfcd..4c59a87807 100644 --- a/org.adempiere.base/src/org/compiere/model/MUser.java +++ b/org.adempiere.base/src/org/compiere/model/MUser.java @@ -16,6 +16,9 @@ *****************************************************************************/ package org.compiere.model; +import java.io.UnsupportedEncodingException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; @@ -33,6 +36,7 @@ import org.compiere.util.CCache; import org.compiere.util.CLogger; import org.compiere.util.DB; import org.compiere.util.Env; +import org.compiere.util.Secure; import org.compiere.util.SecureEngine; /** @@ -50,7 +54,7 @@ public class MUser extends X_AD_User /** * */ - private static final long serialVersionUID = -5845477151929518375L; + private static final long serialVersionUID = -5343496366428193731L; /** * Get active Users of BPartner @@ -167,44 +171,82 @@ public class MUser extends X_AD_User s_log.warning ("Invalid Name/Password = " + name + "/" + password); return null; } - int AD_Client_ID = Env.getAD_Client_ID(ctx); - + boolean hash_password = MSysConfig.getBooleanValue("USER_PASSWORD_HASH", false); MUser retValue = null; - String sql = "SELECT * FROM AD_User " - + "WHERE COALESCE(LDAPUser, Name)=? " // #1 - + " AND ((Password=? AND (SELECT IsEncrypted FROM AD_Column WHERE AD_Column_ID=417)='N') " // #2 - + "OR (Password=? AND (SELECT IsEncrypted FROM AD_Column WHERE AD_Column_ID=417)='Y'))" // #3 - + " AND IsActive='Y' AND AD_Client_ID=?" // #4 - ; - PreparedStatement pstmt = null; - ResultSet rs = null; - try + if (!hash_password) { - pstmt = DB.prepareStatement (sql, null); - pstmt.setString (1, name); - pstmt.setString (2, password); - pstmt.setString (3, SecureEngine.encrypt(password)); - pstmt.setInt(4, AD_Client_ID); - rs = pstmt.executeQuery (); - if (rs.next ()) + int AD_Client_ID = Env.getAD_Client_ID(ctx); + + + String sql = "SELECT * FROM AD_User " + + "WHERE COALESCE(LDAPUser, Name)=? " // #1 + + " AND ((Password=? AND (SELECT IsEncrypted FROM AD_Column WHERE AD_Column_ID=417)='N') " // #2 + + "OR (Password=? AND (SELECT IsEncrypted FROM AD_Column WHERE AD_Column_ID=417)='Y'))" // #3 + + " AND IsActive='Y' AND AD_Client_ID=?" // #4 + ; + PreparedStatement pstmt = null; + ResultSet rs = null; + try { - retValue = new MUser (ctx, rs, null); - if (rs.next()) - s_log.warning ("More then one user with Name/Password = " + name); + pstmt = DB.prepareStatement (sql, null); + pstmt.setString (1, name); + pstmt.setString (2, password); + pstmt.setString (3, SecureEngine.encrypt(password)); + pstmt.setInt(4, AD_Client_ID); + rs = pstmt.executeQuery (); + if (rs.next ()) + { + retValue = new MUser (ctx, rs, null); + if (rs.next()) + s_log.warning ("More then one user with Name/Password = " + name); + } + else + s_log.fine("No record"); + } + catch (Exception e) + { + s_log.log(Level.SEVERE, sql, e); + } + finally + { + DB.close(rs, pstmt); + rs = null; pstmt = null; + } + } else { + String where = " COALESCE(LDAPUser,Name) = ? AND" + + " EXISTS (SELECT * FROM AD_User_Roles ur" + + " INNER JOIN AD_Role r ON (ur.AD_Role_ID=r.AD_Role_ID)" + + " WHERE ur.AD_User_ID=AD_User.AD_User_ID AND ur.IsActive='Y' AND r.IsActive='Y') AND " + + " EXISTS (SELECT * FROM AD_Client c" + + " WHERE c.AD_Client_ID=AD_User.AD_Client_ID" + + " AND c.IsActive='Y') AND " + + " AD_User.IsActive='Y'"; + + MUser user = MTable.get(ctx, MUser.Table_ID).createQuery( where, null).setParameters(name).firstOnly(); // throws error if username collision occurs + + String hash = null; + String salt = null; + + if (user != null ) + { + hash = user.getPassword(); + salt = user.getSalt(); + } + + // always do calculation to confuse timing based attacks + if ( user == null ) + user = MUser.get(ctx, 0); + if ( hash == null ) + hash = "0000000000000000"; + if ( salt == null ) + salt = "0000000000000000"; + + if ( user.authenticateHash(password) ) + { + retValue=user; } - else - s_log.fine("No record"); - } - catch (Exception e) - { - s_log.log(Level.SEVERE, sql, e); } - finally - { - DB.close(rs, pstmt); - rs = null; pstmt = null; - } - return retValue; + return retValue; } // get /** @@ -308,6 +350,8 @@ public class MUser extends X_AD_User private Boolean m_isAdministrator = null; /** User Access Rights */ private X_AD_UserBPAccess[] m_bpAccess = null; + /** Password Hashed **/ + private boolean hashed = false; /** @@ -376,6 +420,77 @@ public class MUser extends X_AD_User return sb.toString (); } // cleanValue + /** + * Convert Password to SHA-512 hash with salt * 1000 iterations https://www.owasp.org/index.php/Hashing_Java + * @param password -- plain text password + */ + @Override + public void setPassword(String password) { + if ( password == null ) + { + super.setPassword(password); + return; + } + boolean hash_password = MSysConfig.getBooleanValue("USER_PASSWORD_HASH", false); + + if(!hash_password){ + super.setPassword(password); + return; + } + + if ( hashed ) + return; + + hashed = true; // prevents double call from beforeSave + + // Uses a secure Random not a simple Random + SecureRandom random; + try { + random = SecureRandom.getInstance("SHA1PRNG"); + // Salt generation 64 bits long + byte[] bSalt = new byte[8]; + random.nextBytes(bSalt); + // Digest computation + String hash; + hash = SecureEngine.getSHA512Hash(1000,password,bSalt); + + String sSalt = Secure.convertToHexString(bSalt); + super.setPassword(hash); + setSalt(sSalt); + } catch (NoSuchAlgorithmException e) { + super.setPassword(password); + } catch (UnsupportedEncodingException e) { + super.setPassword(password); + } + } + + /** + * check if hashed password matches + */ + public boolean authenticateHash (String password) { + + String hash = null; + String salt = null; + + hash = getPassword(); + salt = getSalt(); + + // always do calculation to prevent timing based attacks + if ( hash == null ) + hash = "0000000000000000"; + if ( salt == null ) + salt = "0000000000000000"; + + try { + return SecureEngine.getSHA512Hash(1000, password, Secure.convertHexString(salt)).equals(hash); + } catch (NoSuchAlgorithmException ignored) { + log.log(Level.WARNING, "Password hashing not supported by JVM"); + } catch (UnsupportedEncodingException ignored) { + log.log(Level.WARNING, "Password hashing not supported by JVM"); + } + return false; + } + /** * Get First Name * @return first name @@ -801,6 +916,13 @@ public class MUser extends X_AD_User setEMailVerifyDate(null); if (newRecord || super.getValue() == null || is_ValueChanged("Value")) setValue(super.getValue()); + + if (newRecord || is_ValueChanged("Password")) { + boolean hash_password = MSysConfig.getBooleanValue("USER_PASSWORD_HASH", false); + if (hash_password) + setPassword(getPassword()); + } + return true; } // beforeSave diff --git a/org.adempiere.base/src/org/compiere/model/PO.java b/org.adempiere.base/src/org/compiere/model/PO.java index 9d2c2cfbfc..49f01af8c6 100644 --- a/org.adempiere.base/src/org/compiere/model/PO.java +++ b/org.adempiere.base/src/org/compiere/model/PO.java @@ -1921,6 +1921,8 @@ public abstract class PO continue; return false; // one value is non-zero } + if (MTable.isZeroIDTable(get_TableName())) + return false; return true; } // is_new diff --git a/org.adempiere.base/src/org/compiere/model/SystemIDs.java b/org.adempiere.base/src/org/compiere/model/SystemIDs.java index c6c251bb41..d6e5afb7dd 100644 --- a/org.adempiere.base/src/org/compiere/model/SystemIDs.java +++ b/org.adempiere.base/src/org/compiere/model/SystemIDs.java @@ -145,4 +145,6 @@ public interface SystemIDs public final static int WINDOW_SHIPMENT_CUSTOMER = 169; public final static int WINDOW_WAREHOUSE_LOCATOR = 139; public final static int WINDOW_WINDOW_TAB_FIELD = 102; + + public final static int SYSCONFIG_USER_HASH_PASSWORD = 200013; } diff --git a/org.adempiere.base/src/org/compiere/model/X_AD_User.java b/org.adempiere.base/src/org/compiere/model/X_AD_User.java index 8b61c745d2..5447fa0dec 100644 --- a/org.adempiere.base/src/org/compiere/model/X_AD_User.java +++ b/org.adempiere.base/src/org/compiere/model/X_AD_User.java @@ -664,6 +664,25 @@ public class X_AD_User extends PO implements I_AD_User, I_Persistent } return false; } + + /** + * Set Salt. + * + * @param Salt + * Random data added to improve password hash effectiveness + */ + public void setSalt(String Salt) { + set_ValueNoCheck(COLUMNNAME_Salt, Salt); + } + + /** + * Get Salt. + * + * @return Random data added to improve password hash effectiveness + */ + public String getSalt() { + return (String) get_Value(COLUMNNAME_Salt); + } public I_AD_User getSupervisor() throws RuntimeException { diff --git a/org.adempiere.base/src/org/compiere/util/Login.java b/org.adempiere.base/src/org/compiere/util/Login.java index 12c6a7b37a..5b4432573d 100644 --- a/org.adempiere.base/src/org/compiere/util/Login.java +++ b/org.adempiere.base/src/org/compiere/util/Login.java @@ -15,7 +15,9 @@ * or via info@compiere.org or http://www.compiere.org/license.html * *****************************************************************************/ package org.compiere.util; - + +import java.io.UnsupportedEncodingException; +import java.security.NoSuchAlgorithmException; import java.security.Principal; import java.sql.Connection; import java.sql.PreparedStatement; @@ -35,8 +37,11 @@ import org.compiere.model.MAcctSchema; import org.compiere.model.MClientInfo; import org.compiere.model.MCountry; import org.compiere.model.MRole; +import org.compiere.model.MSysConfig; import org.compiere.model.MSystem; +import org.compiere.model.MTable; import org.compiere.model.MTree_Base; +import org.compiere.model.MUser; import org.compiere.model.ModelValidationEngine; @@ -232,7 +237,8 @@ public class Login */ private KeyNamePair[] getRoles (String app_user, String app_pwd, boolean force) { - log.info("User=" + app_user); + log.info("User=" + app_user); + long start = System.currentTimeMillis(); if (app_user == null) { @@ -240,7 +246,7 @@ public class Login return null; } - // Authentification + // Authentication boolean authenticated = false; MSystem system = MSystem.get(m_ctx); if (system == null) @@ -251,69 +257,159 @@ public class Login log.warning("No Apps Password"); return null; } + if (system.isLDAP()) { authenticated = system.isLDAP(app_user, app_pwd); - if (authenticated) + if (authenticated){ app_pwd = null; + authenticated=true; + } + // if not authenticated, use AD_User as backup } - KeyNamePair[] retValue = null; - ArrayList list = new ArrayList(); - // + boolean hash_password=MSysConfig.getBooleanValue("USER_PASSWORD_HASH", false); + KeyNamePair[] retValue = null; + ArrayList list = new ArrayList(); + + if(hash_password){ + // adaxa-pb: try to authenticate using hashed password -- falls back to plain text/encrypted + String where = " COALESCE(LDAPUser,Name) = ? AND" + + " EXISTS (SELECT * FROM AD_User_Roles ur" + + " INNER JOIN AD_Role r ON (ur.AD_Role_ID=r.AD_Role_ID)" + + " WHERE ur.AD_User_ID=AD_User.AD_User_ID AND ur.IsActive='Y' AND r.IsActive='Y') AND " + + " EXISTS (SELECT * FROM AD_Client c" + + " WHERE c.AD_Client_ID=AD_User.AD_Client_ID" + + " AND c.IsActive='Y') AND " + + " AD_User.IsActive='Y'"; + + MUser user = MTable.get(m_ctx, MUser.Table_ID).createQuery( where, null).setParameters(app_user).firstOnly(); // throws error if username collision occurs + + String hash = null; + String salt = null; + int AD_User_ID = -1; + + if (user != null ) + { + hash = user.getPassword(); + salt = user.getSalt(); + } + + // always do calculation to confuse timing based attacks + if ( user == null ) + user = MUser.get(m_ctx, 0); + if ( hash == null ) + hash = "0000000000000000"; + if ( salt == null ) + salt = "0000000000000000"; + + if ( user.authenticateHash(app_pwd) ) + { + authenticated = true; + AD_User_ID = user.getAD_User_ID(); + app_pwd = null; + } + } + else{ + StringBuffer sql = new StringBuffer("SELECT u.AD_User_ID,") + .append(" u.ConnectionProfile ") + .append(" FROM AD_User u"); + sql.append(" WHERE COALESCE(u.LDAPUser,u.Name)=?"); + sql.append(" AND u.IsActive='Y'").append(" AND EXISTS (SELECT * FROM AD_Client c WHERE u.AD_Client_ID=c.AD_Client_ID AND c.IsActive='Y')"); + + if (app_pwd != null) + sql.append(" AND ((u.Password=? AND (SELECT IsEncrypted FROM AD_Column WHERE AD_Column_ID=417)='N') " + + "OR (u.Password=? AND (SELECT IsEncrypted FROM AD_Column WHERE AD_Column_ID=417)='Y'))"); // #2/3 + + PreparedStatement pstmt1=null; + ResultSet rs1=null; + + try{ + pstmt1 = DB.prepareStatement(sql.toString(), null); + pstmt1.setString(1, app_user); + if (app_pwd != null) + { + pstmt1.setString(2, app_pwd); + pstmt1.setString(3, SecureEngine.encrypt(app_pwd)); + } + rs1 = pstmt1.executeQuery(); + + while(rs1.next()){ + authenticated=true; + } + + }catch (Exception ex) { + // TODO: handle exception + log.log(Level.SEVERE, sql.toString(), ex); + log.saveError("DBLogin", ex); + retValue = null; + } + finally + { + DB.close(rs1, pstmt1); + rs1 = null; pstmt1 = null; + } + } + + if(authenticated){ StringBuffer sql = new StringBuffer("SELECT u.AD_User_ID, r.AD_Role_ID,r.Name,") - .append(" u.ConnectionProfile ") - .append("FROM AD_User u") - .append(" INNER JOIN AD_User_Roles ur ON (u.AD_User_ID=ur.AD_User_ID AND ur.IsActive='Y')") - .append(" INNER JOIN AD_Role r ON (ur.AD_Role_ID=r.AD_Role_ID AND r.IsActive='Y') ") - .append("WHERE COALESCE(u.LDAPUser,u.Name)=?") // #1 - .append(" AND u.IsActive='Y'") - .append(" AND EXISTS (SELECT * FROM AD_Client c WHERE u.AD_Client_ID=c.AD_Client_ID AND c.IsActive='Y')"); - if (app_pwd != null) - sql.append(" AND ((u.Password=? AND (SELECT IsEncrypted FROM AD_Column WHERE AD_Column_ID=417)='N') " - + "OR (u.Password=? AND (SELECT IsEncrypted FROM AD_Column WHERE AD_Column_ID=417)='Y'))"); // #2/3 - sql.append(" ORDER BY r.Name"); + .append(" u.ConnectionProfile ") + .append("FROM AD_User u") + .append(" INNER JOIN AD_User_Roles ur ON (u.AD_User_ID=ur.AD_User_ID AND ur.IsActive='Y')") + .append(" INNER JOIN AD_Role r ON (ur.AD_Role_ID=r.AD_Role_ID AND r.IsActive='Y') "); + + sql.append("WHERE COALESCE(u.LDAPUser,u.Name)=?"); // #1 + + sql.append(" AND u.IsActive='Y'").append(" AND EXISTS (SELECT * FROM AD_Client c WHERE u.AD_Client_ID=c.AD_Client_ID AND c.IsActive='Y')"); + + /* if (app_pwd != null && !hash_password) + sql.append(" AND ((u.Password=? AND (SELECT IsEncrypted FROM AD_Column WHERE AD_Column_ID=417)='N') " + + "OR (u.Password=? AND (SELECT IsEncrypted FROM AD_Column WHERE AD_Column_ID=417)='Y'))"); // #2/3*/ + + sql.append(" ORDER BY r.Name"); + PreparedStatement pstmt = null; ResultSet rs = null; try { pstmt = DB.prepareStatement(sql.toString(), null); - pstmt.setString(1, app_user); - if (app_pwd != null) + pstmt.setString(1, app_user); + + /*if (app_pwd != null && !hash_password) { pstmt.setString(2, app_pwd); pstmt.setString(3, SecureEngine.encrypt(app_pwd)); - } + }*/ // execute a query - rs = pstmt.executeQuery(); - - if (!rs.next()) // no record found - if (force) - { - Env.setContext(m_ctx, "#AD_User_Name", "System"); - Env.setContext(m_ctx, "#AD_User_ID", "0"); - Env.setContext(m_ctx, "#AD_User_Description", "System Forced Login"); - Env.setContext(m_ctx, "#User_Level", "S "); // Format 'SCO' - Env.setContext(m_ctx, "#User_Client", "0"); // Format c1, c2, ... - Env.setContext(m_ctx, "#User_Org", "0"); // Format o1, o2, ... - rs.close(); - pstmt.close(); - retValue = new KeyNamePair[] {new KeyNamePair(0, "System Administrator")}; - return retValue; - } - else - { - rs.close(); - pstmt.close(); - log.saveError("UserPwdError", app_user, false); - return null; - } - - Env.setContext(m_ctx, "#AD_User_Name", app_user); - Env.setContext(m_ctx, "#AD_User_ID", rs.getInt(1)); - Env.setContext(m_ctx, "#SalesRep_ID", rs.getInt(1)); - // + rs = pstmt.executeQuery(); + + if (!rs.next()) // no record found + if (force) + { + Env.setContext(m_ctx, "#AD_User_Name", "System"); + Env.setContext(m_ctx, "#AD_User_ID", "0"); + Env.setContext(m_ctx, "#AD_User_Description", "System Forced Login"); + Env.setContext(m_ctx, "#User_Level", "S "); // Format 'SCO' + Env.setContext(m_ctx, "#User_Client", "0"); // Format c1, c2, ... + Env.setContext(m_ctx, "#User_Org", "0"); // Format o1, o2, ... + rs.close(); + pstmt.close(); + retValue = new KeyNamePair[] {new KeyNamePair(0, "System Administrator")}; + return retValue; + } + else + { + rs.close(); + pstmt.close(); + log.saveError("UserPwdError", app_user, false); + return null; + } + + Env.setContext(m_ctx, "#AD_User_Name", app_user); + Env.setContext(m_ctx, "#AD_User_ID", rs.getInt(1)); + Env.setContext(m_ctx, "#SalesRep_ID", rs.getInt(1)); + if (Ini.isClient()) { if (MSystem.isSwingRememberUserAllowed()) @@ -335,7 +431,7 @@ public class Login } } } - + do // read all roles { int AD_Role_ID = rs.getInt(2); @@ -349,8 +445,10 @@ public class Login // retValue = new KeyNamePair[list.size()]; list.toArray(retValue); - log.fine("User=" + app_user + " - roles #" + retValue.length); - } + log.fine("User=" + app_user + " - roles #" + retValue.length); + + } + catch (Exception ex) { log.log(Level.SEVERE, sql.toString(), ex); @@ -363,6 +461,7 @@ public class Login DB.close(rs, pstmt); rs = null; pstmt = null; } + } long ms = System.currentTimeMillis () - start; return retValue; } // getRoles diff --git a/org.adempiere.base/src/org/compiere/util/Secure.java b/org.adempiere.base/src/org/compiere/util/Secure.java index 9bc033e975..5956c26e1c 100644 --- a/org.adempiere.base/src/org/compiere/util/Secure.java +++ b/org.adempiere.base/src/org/compiere/util/Secure.java @@ -16,6 +16,7 @@ *****************************************************************************/ package org.compiere.util; +import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.security.AlgorithmParameters; import java.security.MessageDigest; @@ -378,6 +379,30 @@ public class Secure implements SecureInterface return (convertHexString(value) != null); } // isDigest + /** + * Convert String and salt to SHA-512 hash with iterations + * https://www.owasp.org/index.php/Hashing_Java + * + * @param value message + * @return HexString of message (length = 128 characters) + * @throws NoSuchAlgorithmException + * @throws UnsupportedEncodingException + */ + public String getSHA512Hash (int iterations, String value, byte[] salt) throws NoSuchAlgorithmException, UnsupportedEncodingException + { + MessageDigest digest = MessageDigest.getInstance("SHA-512"); + digest.reset(); + digest.update(salt); + byte[] input = digest.digest(value.getBytes("UTF-8")); + for (int i = 0; i < iterations; i++) { + digest.reset(); + input = digest.digest(input); + } + digest.reset(); + // + return convertToHexString(input); + } // getSHA512Hash + /** * String Representation * @return info diff --git a/org.adempiere.base/src/org/compiere/util/SecureEngine.java b/org.adempiere.base/src/org/compiere/util/SecureEngine.java index 388ff85f0a..5835e7a5e9 100644 --- a/org.adempiere.base/src/org/compiere/util/SecureEngine.java +++ b/org.adempiere.base/src/org/compiere/util/SecureEngine.java @@ -16,6 +16,8 @@ *****************************************************************************/ package org.compiere.util; +import java.io.UnsupportedEncodingException; +import java.security.NoSuchAlgorithmException; import java.util.Properties; import java.util.logging.Logger; @@ -70,6 +72,22 @@ public class SecureEngine return s_engine.implementation.getClass().getName(); } // getClassName + /** + * Convert String and salt to SHA-512 hash with iterations + * https://www.owasp.org/index.php/Hashing_Java + * + * @param value message + * @return HexString of message (length = 128 characters) + * @throws UnsupportedEncodingException + * @throws NoSuchAlgorithmException + */ + public static String getSHA512Hash (int iterations, String value, byte[] salt) throws NoSuchAlgorithmException, UnsupportedEncodingException + { + if (s_engine == null) + init(System.getProperties()); + return s_engine.implementation.getSHA512Hash(iterations, value, salt); + } // getDigest + /** * Convert String to Digest. * JavaScript version see - http://pajhome.org.uk/crypt/md5/index.html diff --git a/org.adempiere.base/src/org/compiere/util/SecureInterface.java b/org.adempiere.base/src/org/compiere/util/SecureInterface.java index 808bdd2c4e..c4917f2614 100644 --- a/org.adempiere.base/src/org/compiere/util/SecureInterface.java +++ b/org.adempiere.base/src/org/compiere/util/SecureInterface.java @@ -16,7 +16,9 @@ *****************************************************************************/ package org.compiere.util; +import java.io.UnsupportedEncodingException; import java.math.BigDecimal; +import java.security.NoSuchAlgorithmException; import java.sql.Timestamp; /** @@ -126,4 +128,15 @@ public interface SecureInterface */ public boolean isDigest (String value); + /** + * Convert String and salt to SHA-512 hash with iterations + * https://www.owasp.org/index.php/Hashing_Java + * + * @param value message + * @return HexString of message (length = 128 characters) + * @throws NoSuchAlgorithmException + * @throws UnsupportedEncodingException + */ + public String getSHA512Hash (int iterations, String value, byte[] salt) throws NoSuchAlgorithmException, UnsupportedEncodingException; + } // SecureInterface diff --git a/org.adempiere.base/src/org/compiere/util/WebUser.java b/org.adempiere.base/src/org/compiere/util/WebUser.java index 80fa106f3e..20b69bf200 100644 --- a/org.adempiere.base/src/org/compiere/util/WebUser.java +++ b/org.adempiere.base/src/org/compiere/util/WebUser.java @@ -30,6 +30,8 @@ import org.compiere.model.MBPartner; import org.compiere.model.MBPartnerLocation; import org.compiere.model.MLocation; import org.compiere.model.MRefList; +import org.compiere.model.MSysConfig; +import org.compiere.model.MTable; import org.compiere.model.MUser; /** @@ -367,6 +369,7 @@ public class WebUser m_loc = new MLocation (m_ctx, 0, null); // log.info("= " + m_bp + " - " + m_bpc); + } // load /** @@ -669,8 +672,84 @@ public class WebUser public boolean login (String password) { m_loggedIn = isValid () // we have a contact - && WebUtil.exists (password) // we have a password - && password.equals (getPassword ()); + && WebUtil.exists (password); // we have a password + //&& password.equals (getPassword ()); + boolean retValue = false; + if(m_loggedIn) + { + boolean hash_password=MSysConfig.getBooleanValue("USER_PASSWORD_HASH", false); + if(!hash_password) + { + String sql = "SELECT * FROM AD_User " + + "WHERE COALESCE(LDAPUser, Name)=? " // #1 + + " AND ((Password=? AND (SELECT IsEncrypted FROM AD_Column WHERE AD_Column_ID=417)='N') " // #2 + + "OR (Password=? AND (SELECT IsEncrypted FROM AD_Column WHERE AD_Column_ID=417)='Y'))" // #3 + + " AND IsActive='Y' " // #4 + ; + PreparedStatement pstmt = null; + ResultSet rs = null; + try + { + pstmt = DB.prepareStatement (sql, null); + pstmt.setString (1, m_bpc.getName()); + pstmt.setString (2, password); + pstmt.setString (3, SecureEngine.encrypt(password)); + rs = pstmt.executeQuery (); + if (rs.next ()) + { + retValue = true; + if (rs.next()) + log.warning ("More then one user with Name/Password = " + m_bpc.getName()); + } + else + log.fine("No record"); + } + catch (Exception e) + { + log.log(Level.SEVERE, sql, e); + } + finally + { + DB.close(rs, pstmt); + rs = null; pstmt = null; + } + } + else{ + String where = " COALESCE(LDAPUser,Name) = ? AND" + + " EXISTS (SELECT * FROM AD_User_Roles ur" + + " INNER JOIN AD_Role r ON (ur.AD_Role_ID=r.AD_Role_ID)" + + " WHERE ur.AD_User_ID=AD_User.AD_User_ID AND ur.IsActive='Y' AND r.IsActive='Y') AND " + + " EXISTS (SELECT * FROM AD_Client c" + + " WHERE c.AD_Client_ID=AD_User.AD_Client_ID" + + " AND c.IsActive='Y') AND " + + " AD_User.IsActive='Y'"; + + MUser user = MTable.get(m_ctx, MUser.Table_ID).createQuery( where, null).setParameters(m_bpc.getName()).firstOnly(); // throws error if username collision occurs + + String hash = null; + String salt = null; + + if (user != null ) + { + hash = user.getPassword(); + salt = user.getSalt(); + } + + // always do calculation to confuse timing based attacks + if ( user == null ) + user = MUser.get(null, 0); + if ( hash == null ) + hash = "0000000000000000"; + if ( salt == null ) + salt = "0000000000000000"; + + if ( user.authenticateHash(password) ) + { + retValue=true; + } + } + } + m_loggedIn=m_loggedIn && retValue; setPasswordOK (m_loggedIn, password); log.fine("success=" + m_loggedIn); if (m_loggedIn)