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)