Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.6.28
2.6.29
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/***************************************************************************************************
*
* Copyright (c) 2013 - 2020 Universitat Politecnica de Valencia - www.upv.es
* Copyright (c) 2018 - 2020 Open Universiteit - www.ou.nl
* Copyright (c) 2013 - 2025 Universitat Politecnica de Valencia - www.upv.es
* Copyright (c) 2018 - 2025 Open Universiteit - www.ou.nl
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
Expand Down
2 changes: 2 additions & 0 deletions testar/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ dependencies {
implementation group: 'org.jacoco', name: 'jacoco-maven-plugin', version: '0.8.12'
// https://mvnrepository.com/artifact/org.apache.commons/commons-csv
implementation group: 'org.apache.commons', name: 'commons-csv', version: '1.11.0'
// https://mvnrepository.com/artifact/org.eclipse.angus/jakarta.mail
implementation group: 'org.eclipse.angus', name: 'jakarta.mail', version: '2.0.3'
}

task prepareSettings(type: Copy) {
Expand Down
2 changes: 1 addition & 1 deletion testar/src/org/testar/monkey/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@

public class Main {

public static final String TESTAR_VERSION = "2.6.28 (22-Jan-2025)";
public static final String TESTAR_VERSION = "2.6.29 (28-Jan-2025)";

//public static final String TESTAR_DIR_PROPERTY = "DIRNAME"; //Use the OS environment to obtain TESTAR directory
public static final String SETTINGS_FILE = "test.settings";
Expand Down
190 changes: 190 additions & 0 deletions testar/src/org/testar/otp/ImapGmailReader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/**
*
* Copyright (c) 2024 - 2025 Universitat Politecnica de Valencia - www.upv.es
* Copyright (c) 2024 - 2025 Open Universiteit - www.ou.nl
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
*/

package org.testar.otp;

import java.io.IOException;
import java.util.Date;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import jakarta.mail.FetchProfile;
import jakarta.mail.Folder;
import jakarta.mail.Message;
import jakarta.mail.MessagingException;
import jakarta.mail.Multipart;
import jakarta.mail.Part;
import jakarta.mail.Session;
import jakarta.mail.Store;
import jakarta.mail.UIDFolder.FetchProfileItem;
import jakarta.mail.search.ComparisonTerm;
import jakarta.mail.search.ReceivedDateTerm;
import jakarta.mail.search.SearchTerm;

public class ImapGmailReader {

private final String email;
private final char[] app_password;

public ImapGmailReader(String email, char[] app_password) {
this.email = email;
this.app_password = app_password;
}

public String readOtpNumber(int seconds, String otpRegex) {
String otpCode = "";

Store store = null;
Folder folder = null;

try {
store = getImapStore();
folder = getFolderFromStore(store, "INBOX");

// Search for yesterday messages
Message[] messages = folder.search(getYesterdayMessages());
folder.fetch(messages, getFetchProfile());

// Compile the regex pattern
Pattern pattern = Pattern.compile(otpRegex);

for (Message message : messages) {
// Extract email content as a string using the last 'seconds' interval
String messageText = getMessageText(message, seconds);

// Match the content against the OTP regex
Matcher matcher = pattern.matcher(messageText);

if (matcher.find()) {
otpCode = matcher.group(0); // Capture group if present
break; // Stop after finding the first OTP
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
closeFolder(folder);
closeStore(store);
}

return otpCode;
}

Store getImapStore() throws Exception {
Session session = Session.getInstance(getImapProperties());
Store store = session.getStore("imaps");

// Use new String(app_password) to convert char[] to String for authentication
store.connect("imap.gmail.com", this.email, new String(this.app_password));

// Clear the password from memory after use
java.util.Arrays.fill(this.app_password, ' ');

return store;
}

private Properties getImapProperties() {
Properties props = new Properties();
props.put("mail.imaps.host", "imap.gmail.com");
props.put("mail.imaps.ssl.trust", "imap.gmail.com");
props.put("mail.imaps.port", "993");
props.put("mail.imaps.starttls.enable", "true");
props.put("mail.imaps.connectiontimeout", "10000");
props.put("mail.imaps.timeout", "10000");
return props;
}

private Folder getFolderFromStore(Store store, String folderName) throws MessagingException {
Folder folder = store.getFolder(folderName);
folder.open(Folder.READ_ONLY);
return folder;
}

private SearchTerm getYesterdayMessages() {
Date yesterdayDate = new Date(new Date().getTime() - (1000 * 60 * 60 * 24));
return new ReceivedDateTerm(ComparisonTerm.GT, yesterdayDate);
}

private FetchProfile getFetchProfile() {
FetchProfile fetchProfile = new FetchProfile();
fetchProfile.add(FetchProfileItem.ENVELOPE);
fetchProfile.add(FetchProfileItem.CONTENT_INFO);
fetchProfile.add("X-mailer");
return fetchProfile;
}

private String getMessageText(Message message, int seconds) throws MessagingException, IOException {
Date filteredDateBySeconds = new Date(System.currentTimeMillis() - (1000L * seconds));

// If the message does not meet the time seconds criteria, return an empty string
Date messageReceivedDate = message.getReceivedDate();
if (messageReceivedDate == null || messageReceivedDate.before(filteredDateBySeconds)) {
return "";
}

// Collect and return the text content of the message
StringBuilder textCollector = new StringBuilder();
collectTextFromMessage(textCollector, message);
return textCollector.toString();
}

private void collectTextFromMessage(StringBuilder textCollector, Part part) throws MessagingException, IOException {
if (part.isMimeType("text/plain")) {
textCollector.append((String) part.getContent());
} else if (part.isMimeType("multipart/*") && part.getContent() instanceof Multipart) {
Multipart multiPart = (Multipart) part.getContent();
for (int i = 0; i < multiPart.getCount(); i++) {
collectTextFromMessage(textCollector, multiPart.getBodyPart(i));
}
}
}

private void closeFolder(Folder folder) {
if (folder != null && folder.isOpen()) {
try {
folder.close(true);
} catch (MessagingException e) {
e.printStackTrace();
}
}
}

private void closeStore(Store store) {
if (store != null && store.isConnected()) {
try {
store.close();
} catch (MessagingException e) {
e.printStackTrace();
}
}
}
}
106 changes: 106 additions & 0 deletions testar/src/org/testar/otp/TOTPGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
*
* Copyright (c) 2024 - 2025 Universitat Politecnica de Valencia - www.upv.es
* Copyright (c) 2024 - 2025 Open Universiteit - www.ou.nl
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
*/

package org.testar.otp;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base32;

public class TOTPGenerator {

private final char[] secretKey;

// Time step in seconds (30 seconds is standard for TOTP)
private final int TIME_STEP = 30;

/**
* Constructs a TOTPGenerator instance using a secret key
*/
public TOTPGenerator(char[] secretKey) {
this.secretKey = secretKey;
}

public String generateTOTP() {
try {
long currentTimeSeconds = System.currentTimeMillis() / 1000;
long timeStep = currentTimeSeconds / TIME_STEP;

// Convert time step into a byte array (8 bytes)
byte[] timeBytes = new byte[8];
for (int i = 7; i >= 0; i--) {
timeBytes[i] = (byte) (timeStep & 0xFF);
timeStep >>= 8;
}

// Convert char[] secret key to byte[] securely
byte[] keyBytes = decodeBase32(new String(secretKey));

// Create HMAC-SHA1 hash
Mac mac = Mac.getInstance("HmacSHA1");
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "HmacSHA1");
mac.init(keySpec);
byte[] hash = mac.doFinal(timeBytes);

// Extract offset from last nibble
int offset = hash[hash.length - 1] & 0xF;

// Generate 4-byte dynamic binary code
int binaryCode = ((hash[offset] & 0x7F) << 24)
| ((hash[offset + 1] & 0xFF) << 16)
| ((hash[offset + 2] & 0xFF) << 8)
| (hash[offset + 3] & 0xFF);

// Convert to 6-digit OTP
int otp = binaryCode % 1_000_000;

// Pad with leading zeros if necessary
return String.format("%06d", otp);
} catch (Exception e) {
System.err.println("Exception trying to generateTOTP");
return "";
}
}

// Decode a Base32-encoded string into a byte array
private byte[] decodeBase32(String base32) {
Base32 base32Decoder = new Base32();
return base32Decoder.decode(base32);
}

public void clearSecretKey() {
for (int i = 0; i < secretKey.length; i++) {
secretKey[i] = '\0';
}
}

}
Loading
Loading