Extracting a password from a String using regular expressions

Viewed 380

I am trying to solve an exercise in which I have to print the password in a given text. The rules are:

a password consists of digits and Latin upper- and lowercase letters; a password always follows the word "password" (it can be written in upper- or lowercase letters), but can be separated from it by any number of whitespaces and colon : characters.

My problem is that I need to make sure that the password is preceded by "password" and a random number of whitespaces and a colon, but I also must print the password only.

For example, if the input is:

My email javacoder@gmail.com with password     SECRET115. Here is my old PASSWORD: PASS111.

The output should be:

SECRET115

PASS111

I stumbled upon lookaheads and lookbehinds and tried them in my regex:

import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

class Main {

    public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    String text = scanner.nextLine();
    Pattern pattern = Pattern.compile("(?<=password[\\s:]*)\\w*(?=\\W)", Pattern.CASE_INSENSITIVE);
    Matcher matcher = pattern.matcher(text);
    if (!matcher.find()) {
        System.out.println("No passwords found.");
    }
    while (matcher.find()) {
        System.out.println(matcher.group());
    }  
    }
}

This solution does print passwords only, but it also prints extra newlines for no reason. The output for the above input looked like this:

SECRET115



PASS111

Also, when I try to change the regex to "(?<=password[\\s:]*)\\w{5,}(?=\\W)" so that the password accepted be at least of length 5, the program outputs just:

PASS111

The other password is clearly longer than 5 characters, why was it left out?

5 Answers

You can use the regex, (?<=password|password:)\\s*(\\p{Alnum}+) which is not only simple to understand but also gives you the result precisely.

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {
    public static void main(String[] args) {
        Pattern pattern = Pattern.compile("(?<=password|password:)\\s*(\\p{Alnum}+)", Pattern.CASE_INSENSITIVE);
        Matcher matcher = pattern
                .matcher("My email javacoder@gmail.com with password     SECRET115. Here is my old PASSWORD: PASS111.");

        while (matcher.find()) {
            System.out.println(matcher.group(1));
        }
    }
}

Output:

SECRET115
PASS111

Explanation of the regex:

  1. \p{Alnum} matches an alphanumeric character. Check this to learn more about it. Note that you should not use \w for your requirement because it also matches underscore(_) apart from alphabets and digits.
  2. The regex uses the positive lookbehind to assert that \\s*(\\p{Alnum}+) should be followed by (?<=password|password:) i.e. password or password:
  3. The desired result comes from group(1) which is specified as (\\p{Alnum}+)

If you are not comfortable with \p{Alnum}, you can use [A-Za-z0-9] instead.

When your password regex contains \w* the regex matches an empty string first. This match is not shown because you call matcher.find() in the if condition. When you use \w{5}, the first match is the SECRET115, and it is not displayed.

Use

\bpassword[\s:]*(\w+)

See proof

EXPLANATION

NODE                     EXPLANATION
--------------------------------------------------------------------------------
  \b                       the boundary between a word char (\w) and
                           something that is not a word char
--------------------------------------------------------------------------------
  password                 'password'
--------------------------------------------------------------------------------
  [\s:]*                   any character of: whitespace (\n, \r, \t,
                           \f, and " "), ':' (0 or more times
                           (matching the most amount possible))
--------------------------------------------------------------------------------
  (                        group and capture to \1:
--------------------------------------------------------------------------------
    \w+                      word characters (a-z, A-Z, 0-9, _) (1 or
                             more times (matching the most amount
                             possible))
--------------------------------------------------------------------------------
  )                        end of \1

Java code:

Scanner scanner = new Scanner(System.in);
String text = scanner.nextLine();
Pattern pattern = Pattern.compile("\\bpassword[\\s:]*(\\w+)", Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(text);
Boolean found = false;
while (matcher.find()) {
    System.out.println(matcher.group(1));
    found = true;
}
if (!found) {
    System.out.println("No passwords found.");
}

Output:

SECRET115
PASS111

The first match is eaten because of the matcher.find() call in the if condition. Only starting from the second match you produce output.

Try (?i)(password[\s:]*)(\w+)
and extract second group in every match In java variable length lookbehind are not possible...

private static final Pattern PASSWORD_PATTERN =
 Pattern.compile("password\\s*:?\\s*(?<password>[A-Za-z0-9]+)", Pattern.CASE_INSENSITIVE);

public static List<String> getAllPasswords(String str) {
    Matcher matcher = PASSWORD_PATTERN.matcher(str);
    List<String> passwords = new ArrayList<>();

    while (matcher.find()) {
        passwords.add(matcher.group("password"));
    }

    return passwords;
}

Demo you can find at regex101.com

Related