GP-3295: Add askPassword to GhidraScript API.

This commit is contained in:
Dan 2023-06-29 17:05:47 -04:00
parent 92e77ff5cb
commit 899772973a
13 changed files with 127 additions and 57 deletions

View File

@ -23,6 +23,7 @@ import java.util.Arrays;
import java.util.List;
import ghidra.app.script.GhidraScript;
import ghidra.framework.generic.auth.Password;
import ghidra.framework.model.DomainFile;
import ghidra.framework.model.DomainFolder;
import ghidra.program.model.address.Address;
@ -117,6 +118,10 @@ public class AskScript extends GhidraScript {
boolean yesOrNo = askYesNo("yes or no", "is this a yes/no question?");
println("Yes or No? " + yesOrNo);
try (Password password = askPassword("Password", null)) {
println("Password has %d characters".formatted(password.getPasswordChars().length));
// try-with-resources ensures buffer is zeroed out before garbage collected
}
}
catch (IllegalArgumentException iae) {
Msg.warn(this, "Error during headless processing: " + iae.toString());

View File

@ -22,6 +22,7 @@ import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import docking.widgets.OptionDialog;
import docking.widgets.PasswordDialog;
import docking.widgets.dialogs.MultiLineMessageDialog;
import docking.widgets.filechooser.GhidraFileChooser;
import docking.widgets.filechooser.GhidraFileChooserMode;
@ -47,6 +48,7 @@ import ghidra.framework.Application;
import ghidra.framework.client.*;
import ghidra.framework.cmd.BackgroundCommand;
import ghidra.framework.cmd.Command;
import ghidra.framework.generic.auth.Password;
import ghidra.framework.main.DataTreeDialog;
import ghidra.framework.model.*;
import ghidra.framework.options.OptionType;
@ -2977,6 +2979,61 @@ public abstract class GhidraScript extends FlatProgramAPI {
return choice;
}
/**
* Returns a {@link Password}, using the String input parameters for guidance. This method can
* only be used in headed mode.
* <p>
* In the GUI environment, this method displays a password popup dialog that prompts the user
* for a secret, usually a password or other credential. There is no pre-population of the
* input. If the user cancels the dialog, it is immediately disposed, and any input to that
* dialog is cleared from memory. If the user completes the dialog, then the secret is returned
* in a wrapped buffer. The buffer can be cleared by calling {@link Secret#close()}; however, it
* is meant to be used in a {@code try-with-resources} block. The pattern does not guarantee
* protection of the secret, but it will help you avoid some typical pitfalls:
*
* <pre>
* String user = askString("Login", "Username:");
* Project project;
* try (Secret secret = askSecret("Login", "Password:")) {
* if (secret == null) {
* return; // User cancelled
* }
* project = doLoginAndOpenProject(user, secret.buffer());
* }
* </pre>
*
* The buffer will be zero-filled upon leaving the {@code try-with-resources} block. If, in the
* sample, the {@code doLoginAndOpenProject} method or any part of its implementation needs to
* retain the secret, it must make a copy. It is then the implementation's responsibility to
* protect its copy.
*
* @param title the title of the dialog
* @param serverType the server type, i.e., its protocol or URL schema
* @param serverName the host name of the server
* @param prompt the prompt to the left of the input field, or null to display "Password:"
* @return the password
* @throws CancelledException if the user cancels
* @throws ImproperUseException if in headless mode
*/
public Password askPassword(String title, String prompt) throws CancelledException {
if (isRunningHeadless()) {
throw new ImproperUseException(
"The askPassword() method can only be used when running headed Ghidra.");
}
PasswordDialog dialog =
new PasswordDialog(title, null, null, prompt, null, null);
try {
state.getTool().showDialog(dialog);
if (!dialog.okWasPressed()) {
throw new CancelledException("User cancelled password prompt.");
}
return Password.wrap(dialog.getPassword());
}
finally {
dialog.dispose();
}
}
/**
* Parses a choice from a string.
*

View File

@ -18,6 +18,7 @@ package ghidra.formats.gfilesystem.crypto;
import java.util.*;
import ghidra.formats.gfilesystem.FSRL;
import ghidra.framework.generic.auth.Password;
/**
* Caches passwords used to unlock a file.
@ -34,10 +35,10 @@ public class CachedPasswordProvider implements PasswordProvider {
* Adds a password / file combo to the cache.
*
* @param fsrl {@link FSRL} file
* @param password password to unlock the file. Specified PasswordValue is
* @param password password to unlock the file. Specified {@link Password} is
* only copied, clearing is still callers responsibility
*/
public synchronized void addPassword(FSRL fsrl, PasswordValue password) {
public synchronized void addPassword(FSRL fsrl, Password password) {
CryptoRec rec = new CryptoRec();
rec.fsrl = fsrl;
rec.value = password.clone();
@ -95,7 +96,7 @@ public class CachedPasswordProvider implements PasswordProvider {
}
@Override
public synchronized Iterator<PasswordValue> getPasswordsFor(FSRL fsrl, String prompt,
public synchronized Iterator<Password> getPasswordsFor(FSRL fsrl, String prompt,
Session session) {
Set<CryptoRec> uniqueFoundRecs = new LinkedHashSet<>();
uniqueFoundRecs.addAll(values.getOrDefault(fsrl.toString(), Collections.emptyList()));
@ -105,7 +106,7 @@ public class CachedPasswordProvider implements PasswordProvider {
uniqueFoundRecs.addAll(values.getOrDefault(fsrl.getMD5(), Collections.emptyList()));
}
List<PasswordValue> results = new ArrayList<>();
List<Password> results = new ArrayList<>();
for (CryptoRec rec : uniqueFoundRecs) {
results.add(rec.value);
}
@ -117,13 +118,13 @@ public class CachedPasswordProvider implements PasswordProvider {
private static class CryptoRec {
FSRL fsrl;
PasswordValue value;
Password value;
}
private class CloningPasswordIterator implements Iterator<PasswordValue> {
Iterator<PasswordValue> delegate;
private class CloningPasswordIterator implements Iterator<Password> {
Iterator<Password> delegate;
CloningPasswordIterator(Iterator<PasswordValue> it) {
CloningPasswordIterator(Iterator<Password> it) {
this.delegate = it;
}
@ -133,8 +134,8 @@ public class CachedPasswordProvider implements PasswordProvider {
}
@Override
public PasswordValue next() {
PasswordValue result = delegate.next();
public Password next() {
Password result = delegate.next();
return result.clone();
}

View File

@ -23,6 +23,7 @@ import java.util.*;
import org.apache.commons.io.FilenameUtils;
import ghidra.formats.gfilesystem.FSRL;
import ghidra.framework.generic.auth.Password;
import ghidra.util.Msg;
import utilities.util.FileUtilities;
@ -51,7 +52,7 @@ public class CmdLinePasswordProvider implements PasswordProvider {
public static final String CMDLINE_PASSWORD_PROVIDER_PROPERTY_NAME = "filesystem.passwords";
@Override
public Iterator<PasswordValue> getPasswordsFor(FSRL fsrl, String prompt, Session session) {
public Iterator<Password> getPasswordsFor(FSRL fsrl, String prompt, Session session) {
String propertyValue = System.getProperty(CMDLINE_PASSWORD_PROVIDER_PROPERTY_NAME);
if (propertyValue == null) {
return Collections.emptyIterator();
@ -60,8 +61,8 @@ public class CmdLinePasswordProvider implements PasswordProvider {
return load(passwordFile, fsrl).iterator();
}
private List<PasswordValue> load(File f, FSRL fsrl) {
List<PasswordValue> result = new ArrayList<>();
private List<Password> load(File f, FSRL fsrl) {
List<Password> result = new ArrayList<>();
try {
for (String s : FileUtilities.getLines(f)) {
String[] fields = s.split("\t");
@ -73,7 +74,7 @@ public class CmdLinePasswordProvider implements PasswordProvider {
if (fileIdStr == null) {
// no file identifier string, always matches
result.add(PasswordValue.wrap(password.toCharArray()));
result.add(Password.wrap(password.toCharArray()));
continue;
}
@ -82,7 +83,7 @@ public class CmdLinePasswordProvider implements PasswordProvider {
FSRL currentFSRL = FSRL.fromString(fileIdStr);
// was a fsrl string, only test as fsrl
if (currentFSRL.isEquivalent(fsrl)) {
result.add(PasswordValue.wrap(password.toCharArray()));
result.add(Password.wrap(password.toCharArray()));
}
continue;
}
@ -93,14 +94,14 @@ public class CmdLinePasswordProvider implements PasswordProvider {
if (!nameOnly.equals(fileIdStr)) {
// was a path str, only test against path component
if (fileIdStr.equals(fsrl.getPath())) {
result.add(PasswordValue.wrap(password.toCharArray()));
result.add(Password.wrap(password.toCharArray()));
}
continue;
}
// was a plain name, only test against name component
if (nameOnly.equals(fsrl.getName())) {
result.add(PasswordValue.wrap(password.toCharArray()));
result.add(Password.wrap(password.toCharArray()));
continue;
}
// no matches, try next line

View File

@ -18,6 +18,7 @@ package ghidra.formats.gfilesystem.crypto;
import java.util.Iterator;
import ghidra.formats.gfilesystem.FSRL;
import ghidra.framework.generic.auth.Password;
/**
* A stub implementation of CryptoSession that relies on a parent instance.
@ -41,12 +42,12 @@ public class CryptoProviderSessionChildImpl implements CryptoSession {
}
@Override
public Iterator<PasswordValue> getPasswordsFor(FSRL fsrl, String prompt) {
public Iterator<Password> getPasswordsFor(FSRL fsrl, String prompt) {
return parentSession.getPasswordsFor(fsrl, prompt);
}
@Override
public void addSuccessfulPassword(FSRL fsrl, PasswordValue password) {
public void addSuccessfulPassword(FSRL fsrl, Password password) {
parentSession.addSuccessfulPassword(fsrl, password);
}

View File

@ -21,6 +21,7 @@ import java.util.function.Supplier;
import java.util.stream.Collectors;
import ghidra.formats.gfilesystem.FSRL;
import ghidra.framework.generic.auth.Password;
/**
* Registry of {@link CryptoProvider crypto providers} and {@link #newSession() session creator}.
@ -122,7 +123,7 @@ public class CryptoProviders {
}
@Override
public void addSuccessfulPassword(FSRL fsrl, PasswordValue password) {
public void addSuccessfulPassword(FSRL fsrl, Password password) {
cachedCryptoProvider.addPassword(fsrl, password);
}
@ -159,16 +160,16 @@ public class CryptoProviders {
}
@Override
public Iterator<PasswordValue> getPasswordsFor(FSRL fsrl, String prompt) {
public Iterator<Password> getPasswordsFor(FSRL fsrl, String prompt) {
return new PasswordIterator(providers, fsrl, prompt);
}
/**
* Union iterator of all password providers
*/
class PasswordIterator implements Iterator<PasswordValue> {
class PasswordIterator implements Iterator<Password> {
private List<PasswordProvider> providers;
private Iterator<PasswordValue> currentIt;
private Iterator<Password> currentIt;
private String prompt;
private FSRL fsrl;
@ -194,7 +195,7 @@ public class CryptoProviders {
}
@Override
public PasswordValue next() {
public Password next() {
return currentIt.next();
}

View File

@ -19,6 +19,7 @@ import java.io.Closeable;
import java.util.Iterator;
import ghidra.formats.gfilesystem.FSRL;
import ghidra.framework.generic.auth.Password;
/**
* Provides the caller with the ability to perform crypto querying operations
@ -42,7 +43,7 @@ public interface CryptoSession extends Closeable {
* @param prompt optional prompt that may be displayed to a user
* @return {@link Iterator} of possible passwords
*/
Iterator<PasswordValue> getPasswordsFor(FSRL fsrl, String prompt);
Iterator<Password> getPasswordsFor(FSRL fsrl, String prompt);
/**
* Pushes a known good password into a cache for later re-retrieval.
@ -50,7 +51,7 @@ public interface CryptoSession extends Closeable {
* @param fsrl {@link FSRL} path to the file that was unlocked by the password
* @param password the good password
*/
void addSuccessfulPassword(FSRL fsrl, PasswordValue password);
void addSuccessfulPassword(FSRL fsrl, Password password);
/**
* Returns true if this session has been closed.

View File

@ -18,6 +18,7 @@ package ghidra.formats.gfilesystem.crypto;
import java.util.Iterator;
import ghidra.formats.gfilesystem.FSRL;
import ghidra.framework.generic.auth.Password;
/**
* Instances of this interface provide passwords to decrypt files.
@ -47,5 +48,5 @@ public interface PasswordProvider extends CryptoProvider {
* related queries
* @return {@link Iterator} of possible passwords
*/
Iterator<PasswordValue> getPasswordsFor(FSRL fsrl, String prompt, Session session);
Iterator<Password> getPasswordsFor(FSRL fsrl, String prompt, Session session);
}

View File

@ -22,6 +22,7 @@ import java.awt.Component;
import docking.DockingWindowManager;
import ghidra.formats.gfilesystem.FSRL;
import ghidra.formats.gfilesystem.crypto.PasswordDialog.RESULT_STATE;
import ghidra.framework.generic.auth.Password;
/**
* Pops up up a GUI dialog prompting the user to enter a password for the specified file.
@ -36,7 +37,7 @@ import ghidra.formats.gfilesystem.crypto.PasswordDialog.RESULT_STATE;
public class PopupGUIPasswordProvider implements PasswordProvider {
@Override
public Iterator<PasswordValue> getPasswordsFor(FSRL fsrl, String prompt, Session session) {
public Iterator<Password> getPasswordsFor(FSRL fsrl, String prompt, Session session) {
return new PasswordIterator(session, fsrl, prompt);
}
@ -44,11 +45,11 @@ public class PopupGUIPasswordProvider implements PasswordProvider {
boolean cancelAll;
}
class PasswordIterator implements Iterator<PasswordValue> {
class PasswordIterator implements Iterator<Password> {
private SessionState sessionState;
private FSRL fsrl;
private boolean cancelled;
private PasswordValue password;
private Password password;
private String prompt;
private int tryCount;
@ -73,7 +74,7 @@ public class PopupGUIPasswordProvider implements PasswordProvider {
DockingWindowManager.showDialog(rootFrame, pwdDialog);
cancelled = pwdDialog.resultState == RESULT_STATE.CANCELED;
password = cancelled ? null : PasswordValue.wrap(pwdDialog.passwordField.getPassword());
password = cancelled ? null : Password.wrap(pwdDialog.passwordField.getPassword());
sessionState.cancelAll |= cancelled && pwdDialog.cancelledAll;
pwdDialog.dispose();
}
@ -90,8 +91,8 @@ public class PopupGUIPasswordProvider implements PasswordProvider {
}
@Override
public PasswordValue next() {
PasswordValue result = password;
public Password next() {
Password result = password;
password = null;
return result;
}

View File

@ -25,12 +25,13 @@ import org.junit.*;
import generic.test.AbstractGenericTest;
import ghidra.formats.gfilesystem.FSRL;
import ghidra.framework.generic.auth.Password;
import util.CollectionUtils;
public class CachedPasswordProviderTest extends AbstractGenericTest {
private CryptoProviders cryptoProviders = CryptoProviders.getInstance();
private List<PasswordValue> getPasswords(CryptoSession cryptoSession, String fsrlStr)
private List<Password> getPasswords(CryptoSession cryptoSession, String fsrlStr)
throws MalformedURLException {
return CollectionUtils
.asList(cryptoSession.getPasswordsFor(FSRL.fromString(fsrlStr), "badbeef"));
@ -60,7 +61,7 @@ public class CachedPasswordProviderTest extends AbstractGenericTest {
// shouldn't match passwords: file1.txt to file2.txt
cryptoSession.addSuccessfulPassword(FSRL.fromString("file:///fake/path/file1.txt"),
PasswordValue.wrap("password_for_file2.txt".toCharArray()));
Password.wrap("password_for_file2.txt".toCharArray()));
assertEquals(1, getPasswords(cryptoSession, "file:///fake/path/file1.txt").size());
assertEquals(0, getPasswords(cryptoSession, "file:///fake/path/file2.txt").size());

View File

@ -26,6 +26,7 @@ import org.junit.*;
import generic.test.AbstractGenericTest;
import ghidra.formats.gfilesystem.FSRL;
import ghidra.framework.generic.auth.Password;
import util.CollectionUtils;
import utilities.util.FileUtilities;
@ -33,7 +34,7 @@ public class CmdLinePasswordProviderTest extends AbstractGenericTest {
private CryptoProviders cryptoProviders = CryptoProviders.getInstance();
private List<PasswordValue> getPasswords(CryptoSession cryptoSession, String fsrlStr)
private List<Password> getPasswords(CryptoSession cryptoSession, String fsrlStr)
throws MalformedURLException {
return CollectionUtils
.asList(cryptoSession.getPasswordsFor(FSRL.fromString(fsrlStr), "badbeef"));
@ -76,7 +77,7 @@ public class CmdLinePasswordProviderTest extends AbstractGenericTest {
System.setProperty(CmdLinePasswordProvider.CMDLINE_PASSWORD_PROVIDER_PROPERTY_NAME,
pwdFile.getPath());
try (CryptoSession cryptoSession = cryptoProviders.newSession()) {
List<PasswordValue> pwdList =
List<Password> pwdList =
getPasswords(cryptoSession, "file:///fake/path/file1.txt");
assertEquals(2, pwdList.size());
@ -93,12 +94,12 @@ public class CmdLinePasswordProviderTest extends AbstractGenericTest {
System.setProperty(CmdLinePasswordProvider.CMDLINE_PASSWORD_PROVIDER_PROPERTY_NAME,
pwdFile.getPath());
try (CryptoSession cryptoSession = cryptoProviders.newSession()) {
List<PasswordValue> pwdList =
List<Password> pwdList =
getPasswords(cryptoSession, "file:///not_matching/path/file1.txt");
assertEquals(0, pwdList.size());
List<PasswordValue> list2 = getPasswords(cryptoSession, "file:///path/to/a/file1.txt");
List<Password> list2 = getPasswords(cryptoSession, "file:///path/to/a/file1.txt");
assertEquals(1, list2.size());
}
}

View File

@ -18,10 +18,9 @@ package ghidra.file.formats.sevenzip;
import static ghidra.formats.gfilesystem.fileinfo.FileAttributeType.*;
import java.util.*;
import java.io.Closeable;
import java.io.IOException;
import java.util.*;
import org.apache.commons.io.FilenameUtils;
@ -31,9 +30,9 @@ import ghidra.formats.gfilesystem.FileCache.FileCacheEntry;
import ghidra.formats.gfilesystem.FileCache.FileCacheEntryBuilder;
import ghidra.formats.gfilesystem.annotations.FileSystemInfo;
import ghidra.formats.gfilesystem.crypto.CryptoSession;
import ghidra.formats.gfilesystem.crypto.PasswordValue;
import ghidra.formats.gfilesystem.fileinfo.FileAttributes;
import ghidra.formats.gfilesystem.fileinfo.FileType;
import ghidra.framework.generic.auth.Password;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.exception.CryptoException;
@ -141,9 +140,9 @@ public class SevenZipFileSystem implements GFileSystem {
String prompt = passwords.isEmpty()
? fsrl.getContainer().getName()
: String.format("%s in %s", file.getName(), fsrl.getContainer().getName());
for (Iterator<PasswordValue> pwIt =
for (Iterator<Password> pwIt =
cryptoSession.getPasswordsFor(fsrl.getContainer(), prompt); pwIt.hasNext();) {
try (PasswordValue passwordValue = pwIt.next()) {
try (Password passwordValue = pwIt.next()) {
monitor.setMessage("Testing password for " + file.getName());
String password = String.valueOf(passwordValue.getPasswordChars()); // we are forced to use strings by 7zip's api

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.formats.gfilesystem.crypto;
package ghidra.framework.generic.auth;
import java.io.Closeable;
import java.util.Arrays;
@ -23,44 +23,44 @@ import java.util.Arrays;
* <p>
* {@link #close() Closing} an instance will clear the characters of the char array.
*/
public class PasswordValue implements Closeable {
public class Password implements Closeable {
/**
* Creates a new PasswordValue using a copy the specified characters.
* Creates a new {@code Password} using a copy the specified characters.
*
* @param password password characters
* @return new PasswordValue instance
* @return new {@code Password} instance
*/
public static PasswordValue copyOf(char[] password) {
PasswordValue result = new PasswordValue();
public static Password copyOf(char[] password) {
Password result = new Password();
result.password = new char[password.length];
System.arraycopy(password, 0, result.password, 0, password.length);
return result;
}
/**
* Creates a new PasswordValue by wrapping the specified character array.
* Creates a new {@code Password} by wrapping the specified character array.
* <p>
* The new instance will take ownership of the char array, and
* clear it when the instance is {@link #close() closed}.
*
* @param password password characters
* @return new PasswordValue instance
* @return new {@code Password} instance
*/
public static PasswordValue wrap(char[] password) {
PasswordValue result = new PasswordValue();
public static Password wrap(char[] password) {
Password result = new Password();
result.password = password;
return result;
}
private char[] password;
private PasswordValue() {
private Password() {
// empty
}
@Override
public PasswordValue clone() {
public Password clone() {
return copyOf(password);
}
@ -101,7 +101,7 @@ public class PasswordValue implements Closeable {
if (getClass() != obj.getClass()) {
return false;
}
PasswordValue other = (PasswordValue) obj;
Password other = (Password) obj;
return Arrays.equals(password, other.password);
}