-- +----------------------------------------------------------------------------+
-- |                          Jeffrey M. Hunter                                 |
-- |                      jhunter@idevelopment.info                             |
-- |                         www.idevelopment.info                              |
-- |----------------------------------------------------------------------------|
-- |      Copyright (c) 1998-2012 Jeffrey M. Hunter. All rights reserved.       |
-- |----------------------------------------------------------------------------|
-- | DATABASE : Oracle                                                          |
-- | FILE     : example_create_profile_password_parameters.sql                  |
-- | CLASS    : Examples                                                        |
-- | PURPOSE  : The following CREATE FUNCTION and CREATE PROFILE script allow   |
-- |            the DBA to set better password controls for accounts in the     |
-- |            Oracle database. This script is based heavily on the default    |
-- |            script: utlpwdmg.sql                                            |
-- |            Note that this profile does not include parameters used to      |
-- |            limit resources.                                                |
-- | NOTE     : As with any code, ensure to test this script in a development   |
-- |            environment before attempting to run it in production.          |
-- +----------------------------------------------------------------------------+

CONNECT / as sysdba

/*
 * ------------------------------------------------------------------
 * FIRST CREATE THE PL/SQL PASSWORD VERIFY FUNCTION
 * ------------------------------------------------------------------
 */

CREATE OR REPLACE FUNCTION verify_function (
    username      VARCHAR2
  , password      VARCHAR2
  , old_password  VARCHAR2
) RETURN boolean IS 

  n           BOOLEAN;
  m           INTEGER;
  differ      INTEGER;
  isdigit     BOOLEAN;
  ischar      BOOLEAN;
  ispunct     BOOLEAN;
  digitarray  VARCHAR2(20);
  punctarray  VARCHAR2(25);
  chararray   VARCHAR2(52);

BEGIN

  digitarray:= '0123456789';
  chararray:= 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
  punctarray:='!"#$%&()``*+,-/:;<=>?_';

  -- ---------------------------------------------
  -- CHECK IF THE PASSWORD IS SAME AS THE USERNAME
  -- ---------------------------------------------
  IF NLS_LOWER(password) = NLS_LOWER(username)
  THEN
    raise_application_error(-20001, 'Password same as or similar to user');
  END IF;

  -- ---------------------------------------------
  -- CHECK FOR THE MINIMUM LENGTH OF THE PASSWORD
  -- ---------------------------------------------
  IF length(password) < 4 THEN
    raise_application_error(-20002, 'Password length less than 4');
  END IF;

  -- ---------------------------------------------
  -- CHECK IF THE PASSWORD IS TOO SIMPLE. A 
  -- DICTIONARY OF WORDS MAY BE MAINTAINED AND A 
  -- CHECK MAY BE MADE SO AS NOT TO ALLOW THE
  -- WORDS THAT ARE TOO SIMPLE FOR THE PASSWORD.
  -- ---------------------------------------------
  IF NLS_LOWER(password) IN ('welcome', 'database', 'account', 'user', 'password', 'oracle', 'computer', 'abcd') THEN
    raise_application_error(-20002, 'Password too simple');
  END IF;

  -- ---------------------------------------------
  -- CHECK IF THE PASSWORD CONTAINS AT LEAST ONE
  -- LETTER, ONE DIGIT AND ONE PUNCTUATION MARK.
  -- ---------------------------------------------
  -- 1. Check for the digit
  -- ---------------------------------------------
  isdigit:=FALSE;
  m := length(password);
  FOR i IN 1..10 LOOP 
    FOR j IN 1..m LOOP 
      IF substr(password,j,1) = substr(digitarray,i,1) THEN
        isdigit:=TRUE;
        GOTO findchar;
      END IF;
    END LOOP;
  END LOOP;
  IF isdigit = FALSE THEN
    raise_application_error(-20003, 'Password should contain at least one digit, one character and one punctuation');
  END IF;

  -- ---------------------------------------------
  -- 2. Check for the character
  -- ---------------------------------------------
  <<findchar>>
  ischar:=FALSE;
  FOR i IN 1..length(chararray) LOOP 
    FOR j IN 1..m LOOP 
      IF substr(password,j,1) = substr(chararray,i,1) THEN
        ischar:=TRUE;
        GOTO findpunct;
      END IF;
    END LOOP;
  END LOOP;
  IF ischar = FALSE THEN
    raise_application_error(-20003, 'Password should contain at least one digit, one character and one punctuation');
  END IF;

  -- ---------------------------------------------
  -- 3. Check for the punctuation
  -- ---------------------------------------------
  <<findpunct>>
  ispunct:=FALSE;
  FOR i IN 1..length(punctarray) LOOP 
    FOR j IN 1..m LOOP 
      IF substr(password,j,1) = substr(punctarray,i,1) THEN
        ispunct:=TRUE;
        GOTO endsearch;
      END IF;
    END LOOP;
  END LOOP;
  IF ispunct = FALSE THEN
    raise_application_error(-20003, 'Password should contain at least one digit, one character and one punctuation');
  END IF;

  <<endsearch>>
  -- ---------------------------------------------
  -- CHECK IF THE PASSWORD DIFFERS FROM THE
  -- PREVIOUS PASSWORD BY AT LEAST 3 LETTERS
  -- ---------------------------------------------
  IF old_password = '' THEN
    raise_application_error(-20004, 'Old password is null');
  END IF;

  differ := length(old_password) - length(password);

  IF abs(differ) < 3 THEN
    IF length(password) < length(old_password) THEN
      m := length(password);
    ELSE
      m := length(old_password);
    END IF;

    differ := abs(differ);

    FOR i IN 1..m LOOP
      IF substr(password,i,1) != substr(old_password,i,1) THEN
        differ := differ + 1;
      END IF;
    END LOOP;
    IF differ < 3 THEN
      raise_application_error(-20004, 'Password should differ by at least 3 characters');
    END IF;
  END IF;

  -- ---------------------------------------------
  -- Everything is fine; return TRUE
  -- ---------------------------------------------
  RETURN(TRUE);

END;
/



/*
 ** +-----------------------------------------------------------------------------------+
 ** | CREATE PASSWORD PROFILE: developer_profile                                        |
 ** | --------------------------------------------------------------------------------- |
 ** |                                                                                   |
 ** | => FAILED_LOGIN_ATTEMPTS    : Represents the number of failed login attempts that |
 ** |                               can be tried before Oracle locks out an account.    |
 ** |                               Note that the user receives an error message:       |
 ** |                               "ERROR: ORA-28000": The account is locked" upon     |
 ** |                               the locking out of the account due to excessive     |
 ** |                               failed connect attempts.                            |
 ** |                                                                                   |
 ** | => PASSWORD_GRACE_TIME      : This setting is the amount of time a user has to    |
 ** |                               change his or her password once the password        |
 ** |                               expires (from "password_life_time"). This parameter |
 ** |                               is set by using by using either a number that       |
 ** |                               represents days or a number that represents a       |
 ** |                               fraction of a day.                                  |
 ** |                                                                                   |
 ** | => PASSWORD_LIFE_TIME       : This setting determines how long a user's password  |
 ** |                               is good for. Once the time has passed, the password |
 ** |                               expires and the user cannot sign onto the system.   |
 ** |                               To delay the password expiration, use the           |
 ** |                               "PASSWORD_GRACE_TIME" parameter (above).            |
 ** |                                                                                   |
 ** | => PASSWORD_LOCK_TIME       : Determines how long an account will remain locked   |
 ** |                               out if the number of failed attempts, as defined    |
 ** |                               by "FAILED_LOGIN_ATTEMPTS", is exceeded.            |
 ** |                                                                                   |
 ** | => PASSWORD_REUSE_MAX       : This setting defines the number of times a password |
 ** |                               has to be changed before it can be reused. If this  |
 ** |                               parameter is set, the parameter                     |
 ** |                               "PASSWORD_REUSE_TIME" parameter MUST be set to      |
 ** |                               UNLIMITED.                                          |
 ** |                                                                                   |
 ** | => PASSWORD_REUSE_TIME      : This setting defines the number of days before a    |
 ** |                               password can be reused.                             |
 ** |                                                                                   |
 ** | => PASSWORD_VERIFY_FUNCTION : This setting defines the user-defined PL/SQL        |
 ** |                               function that is called to control the complexity   |
 ** |                               of the password.                                    |
 ** |                                                                                   |
 ** | NOTES ON REPRESENTING TIME  :                                                     |
 ** |   To express a fraction of a day for setting, use the notation y/z. In this       |
 ** |   format, z is the total of the fractional part of the day you are representing.  |
 ** |   Therefore, if you use hours, z is 24 (24 hours in a day). If you use minutes,   |
 ** |   z is 1440. If you use seconds, z is 86400.                                      |
 ** |                                                                                   |
 ** |   The y part of the fraction is the fractional part of the z quantity you wish to |
 ** |   represent. For example, if you didn't want to immediately shut a user off when  |
 ** |   his or her password expired - but wanted to give the user six hours to change   |
 ** |   the password - you would use the setting of 1/4 (which is really 6/24, because  |
 ** |   1/4 of a day is six hours). In another example, if you wanted to use 90         |
 ** |   minutes, the proper setting would be 1/16 (90/1440 mathematically reduced).     |
 ** |                                                                                   |
 ** +-----------------------------------------------------------------------------------+
*/

CREATE PROFILE DEVELOPER_PROFILE LIMIT
PASSWORD_LIFE_TIME        60
PASSWORD_GRACE_TIME       10
PASSWORD_REUSE_TIME       1800
PASSWORD_REUSE_MAX        UNLIMITED
FAILED_LOGIN_ATTEMPTS     3
PASSWORD_LOCK_TIME        1/1440
PASSWORD_VERIFY_FUNCTION  verify_function;
