Implement incremental cracking

This commit is contained in:
William Brawner 2018-04-07 09:06:12 -05:00
parent 43cf53c0ca
commit ef15a10a4e
28 changed files with 833 additions and 189 deletions

View file

@ -3,7 +3,10 @@
[![Test Coverage](https://api.codeclimate.com/v1/badges/a35dff49221e36abf189/test_coverage)](https://codeclimate.com/github/wbrawner/keecrack/test_coverage)
KeeCrack is a Java program used for brute-forcing KeePass database file master passwords. This should go without saying
but use of this application is prohibited without the express consent of the owner of the database file.
but use of this application is prohibited without the express consent of the owner of the database file. KeeCrack works
by taking a KeePass database file, an optional key file, and a word list, then attempts to open the database with the
give key file/password pair until it finds a successful password. KeeCrack does not do incremental word list generation
at this time, though you can
## Usage
@ -17,6 +20,13 @@ KeeCrack makes use of Gradle, so to build it yourself, you can just run
./gradlew jfxJar
This will produce a JAR output, though you can also create platform-specific binaries with the following:
./gradlew jfxNative
For more information on building for your OS, please see the README for the
[javafx-gradle-plugin](https://github .com/FibreFoX/javafx-gradle-plugin#requirements)
## Contributing
If you'd like to contribute, please fork the repository, make your changes, squash your commits, and send a pull request

View file

@ -1,3 +1,18 @@
/*
* Copyright 2018 William Brawner
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
buildscript {
repositories {
jcenter()

Binary file not shown.

View file

@ -1,6 +1,6 @@
#Sat Apr 07 16:39:20 CDT 2018
#Sun Apr 15 14:52:35 CDT 2018
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.0-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.0-bin.zip

View file

@ -1,3 +1,18 @@
/*
* Copyright 2018 William Brawner
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
dependencies {
compile project(':keecrack-lib')
compile project(':keecrack-gui')

View file

@ -1,3 +1,18 @@
/*
* Copyright 2018 William Brawner
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.wbrawner.keecrack.cli;
import com.wbrawner.keecrack.lib.Code;
@ -10,13 +25,22 @@ import net.sourceforge.argparse4j.inf.ArgumentParserException;
import net.sourceforge.argparse4j.inf.Namespace;
import java.io.File;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.Date;
import java.util.Locale;
public class Main {
private static final String LOG_SEPARATOR = " - ";
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss.SSS]");
private static boolean isVerbose = false;
private static boolean isIncremental = false;
public static void main(String[] args) {
if (args.length == 0) {
com.wbrawner.keecrack.gui.Main.main(new String[]{});
return;
}
ArgumentParser parser = ArgumentParsers.newFor("KeeCrack")
.build()
.description("Brute force KeePass database files");
@ -24,13 +48,15 @@ public class Main {
.help("Increase logging output")
.action(Arguments.storeTrue())
.dest("verbose");
parser.addArgument("--gui")
parser.addArgument("--incremental", "-i")
.action(Arguments.storeTrue())
.help("launch the graphical interface (ignores other options)")
.dest("gui");
.help("Use pattern-based (incremental) guesses instead of a list of words from a file")
.dest("incremental");
parser.addArgument("--word-list", "-w")
.help("a file containing newline-separated words to use as the passwords")
.help("a file containing newline-separated words to use as the passwords, or the pattern to generate " +
"words from if the --incremental flag is set")
.dest("wordlist")
.required(true)
.metavar("WORD-LIST-FILE");
parser.addArgument("--key-file", "-k")
.help("the key file to use with the database")
@ -44,6 +70,7 @@ public class Main {
try {
Namespace res = parser.parseArgs(args);
isVerbose = res.getBoolean("verbose");
isIncremental = res.getBoolean("incremental");
KeeCrack keeCrack = KeeCrack.getInstance();
keeCrack.setCrackingView(new CLICrackingView());
@ -57,25 +84,38 @@ public class Main {
keeCrack.setKeyFile(new File(keyfilePath));
}
String wordlistPath = res.getString("wordlist");
if (wordlistPath != null) ;
keeCrack.setWordlistFile(new File(wordlistPath));
if (res.getBoolean("gui")) {
com.wbrawner.keecrack.gui.Main.main(new String[]{});
String wordlist = res.getString("wordlist");
if (wordlist != null) {
if (isIncremental) {
keeCrack.setWordListPattern(wordlist);
} else {
keeCrack.attack();
keeCrack.setWordListFile(new File(wordlist));
}
}
keeCrack.attack();
} catch (ArgumentParserException e) {
parser.handleError(e);
}
}
private static void print(String message) {
System.out.print(dateFormat.format(new Date()));
System.out.print(LOG_SEPARATOR);
System.out.println(message);
}
private static void error(String message) {
System.err.print(dateFormat.format(new Date()));
System.err.print(LOG_SEPARATOR);
System.err.println(message);
}
static class CLICrackingView implements CrackingView {
@Override
public void onPasswordGuess(String password) {
if (isVerbose)
System.out.println("Guessing password: " + password);
print("Guessing password: " + password);
}
@Override
@ -95,17 +135,17 @@ public class Main {
timeElapsed.toString().toLowerCase().substring(2),
password
);
System.out.println(message);
print(message);
}
@Override
public void onError(Code code) {
String message = "";
switch (code) {
case ERROR_MISSING_DATABASE_FILE:
case ERROR_INVALID_DATABASE_FILE:
message = "Please specify a database file that you have read access to";
break;
case ERROR_MISSING_WORD_LIST_FILE:
case ERROR_INVALID_WORD_LIST:
message = "Please specify a word list file that you have read access to";
break;
case ERROR_CRACKING_INTERRUPTED:
@ -115,7 +155,7 @@ public class Main {
message = "An error occurred while trying to read one of the files";
break;
}
System.err.println(message);
error(message);
System.exit(code.ordinal());
}
}

View file

@ -1,3 +1,18 @@
/*
* Copyright 2018 William Brawner
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
dependencies {
compile project(':keecrack-lib')
}

View file

@ -1,3 +1,18 @@
/*
* Copyright 2018 William Brawner
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.wbrawner.keecrack.gui;
import com.wbrawner.keecrack.lib.Code;

View file

@ -1,3 +1,18 @@
/*
* Copyright 2018 William Brawner
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.wbrawner.keecrack.gui;
import javafx.application.Application;
@ -10,15 +25,15 @@ import java.io.IOException;
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) throws IOException {
Parent root = FXMLLoader.load(getClass().getResource("/fxml/main.fxml"));
primaryStage.setTitle("Keecrack");
primaryStage.setScene(new Scene(root, 400, 400));
primaryStage.setScene(new Scene(root, 400, 300));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

View file

@ -1,3 +1,18 @@
/*
* Copyright 2018 William Brawner
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.wbrawner.keecrack.gui;
import com.wbrawner.keecrack.lib.Code;
@ -6,10 +21,13 @@ import com.wbrawner.keecrack.lib.view.FormView;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Cursor;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.RadioButton;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleGroup;
import javafx.stage.FileChooser;
import javafx.stage.Modality;
import javafx.stage.Stage;
@ -21,14 +39,25 @@ import java.util.ResourceBundle;
public class MainController implements Initializable, FormView {
@FXML private TextField database;
@FXML private TextField key;
@FXML private TextField wordlist;
@FXML private Button crackButton;
private static KeeCrack keeCrack;
@FXML
private TextField database;
@FXML
private TextField key;
@FXML
private TextField wordlist;
@FXML
private Button crackButton;
@FXML
private ToggleGroup wordlistType;
@FXML
private RadioButton wordlistFile;
@FXML
private RadioButton wordlistPattern;
@Override
public void initialize(URL location, ResourceBundle resources) {
KeeCrack keeCrack = KeeCrack.getInstance();
keeCrack = KeeCrack.getInstance();
keeCrack.setFormView(this);
database.setOnMouseClicked(event -> {
if (KeeCrack.getInstance().isCracking()) {
@ -48,14 +77,7 @@ public class MainController implements Initializable, FormView {
keeCrack.setKeyFile(keyFile);
});
wordlist.setOnMouseClicked(event -> {
if (KeeCrack.getInstance().isCracking()) {
return;
}
File wordlistFile = getFile("Text Files", "txt");
keeCrack.setWordlistFile(wordlistFile);
});
updateWordListHandler();
crackButton.setOnMouseClicked(event -> {
try {
@ -79,19 +101,21 @@ public class MainController implements Initializable, FormView {
return null;
});
Parent root = loader.load();
stage.setScene(new Scene(root, 200, 200));
stage.setScene(new Scene(root, 400, 350));
stage.showAndWait();
} catch (IOException e) {
e.printStackTrace();
}
});
wordlistType.selectedToggleProperty().addListener((observableValue, oldToggle, newToggle) ->
updateWordListHandler());
if (keeCrack.getDatabaseFile() != null)
onDatabaseFileSet(keeCrack.getDatabaseFile().getName());
if (keeCrack.getKeyFile() != null)
onKeyFileSet(keeCrack.getKeyFile().getName());
if (keeCrack.getWordlistFile() != null)
onWordListFileSet(keeCrack.getWordlistFile().getName());
if (keeCrack.getWordListName() != null)
onWordListSet(keeCrack.getWordListName());
}
@ -107,6 +131,26 @@ public class MainController implements Initializable, FormView {
return fileChooser.showOpenDialog(stage);
}
private void updateWordListHandler() {
wordlist.clear();
if (wordlistFile.isSelected()) {
wordlist.setEditable(false);
wordlist.setCursor(Cursor.HAND);
wordlist.setOnMouseClicked(event -> {
if (KeeCrack.getInstance().isCracking()) {
return;
}
File wordlistFile = getFile("Text Files", "txt");
keeCrack.setWordListFile(wordlistFile);
});
} else if (wordlistPattern.isSelected()) {
wordlist.setEditable(true);
wordlist.setCursor(Cursor.TEXT);
wordlist.setOnMouseExited(mouseEvent -> keeCrack.setWordListPattern(wordlist.getText()));
wordlist.setOnMouseClicked(null);
}
}
@Override
public void onDatabaseFileSet(String name) {
database.setText(name);
@ -118,7 +162,7 @@ public class MainController implements Initializable, FormView {
}
@Override
public void onWordListFileSet(String name) {
public void onWordListSet(String name) {
wordlist.setText(name);
}

View file

@ -1,18 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
/*
* Copyright 2018 William Brawner
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-->
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ProgressIndicator?>
<?import javafx.scene.layout.*?>
<AnchorPane xmlns:fx="http://javafx.com/fxml/1" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8"
fx:controller="com.wbrawner.keecrack.gui.CrackingController">
<children>
<VBox alignment="CENTER" prefHeight="200.0" prefWidth="100.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<children>
<VBox alignment="CENTER" prefHeight="200.0" prefWidth="100.0" AnchorPane.bottomAnchor="0.0"
AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<ProgressIndicator fx:id="progress"/>
<Label fx:id="timeElapsed"/>
<Label fx:id="passwordLabel" text="Trying password: "/>
<Label fx:id="password"/>
</children>
</VBox>
</children>
</AnchorPane>

View file

@ -1,25 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
/*
* Copyright 2018 William Brawner
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-->
<?import javafx.geometry.Insets?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.Cursor?>
<?import javafx.scene.layout.*?>
<AnchorPane xmlns:fx="http://javafx.com/fxml/1" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8"
fx:controller="com.wbrawner.keecrack.gui.MainController">
<children>
<VBox alignment="CENTER" prefHeight="200.0" prefWidth="100.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<children>
<GridPane alignment="CENTER" prefHeight="90.0" prefWidth="539.0">
<VBox alignment="CENTER" prefHeight="200.0" prefWidth="100.0" AnchorPane.bottomAnchor="0.0"
AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<GridPane alignment="CENTER">
<columnConstraints>
<ColumnConstraints halignment="RIGHT" minWidth="100.0" prefWidth="20.0"/>
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0"/>
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints maxHeight="-Infinity" minHeight="20.0" percentHeight="25.0" prefHeight="60.0"
vgrow="ALWAYS"/>
<RowConstraints maxHeight="-Infinity" minHeight="20.0" percentHeight="25.0" prefHeight="60.0"
vgrow="ALWAYS"/>
<RowConstraints maxHeight="-Infinity" minHeight="20.0" percentHeight="25.0" prefHeight="60.0"
vgrow="ALWAYS"/>
<RowConstraints maxHeight="-Infinity" minHeight="20.0" percentHeight="25.0" prefHeight="60.0"
vgrow="ALWAYS"/>
</rowConstraints>
<children>
<padding>
<Insets left="20.0" right="20.0"/>
</padding>
<Label alignment="CENTER_RIGHT" text="Database"/>
<TextField fx:id="database" editable="false" promptText="Select" GridPane.columnIndex="1">
<cursor>
@ -39,8 +62,26 @@
<Insets left="20.0"/>
</GridPane.margin>
</TextField>
<TextField fx:id="wordlist" editable="false" promptText="Select" GridPane.columnIndex="1"
<Label alignment="CENTER_RIGHT" text="Word List Type" GridPane.rowIndex="2"/>
<HBox alignment="CENTER_LEFT" prefHeight="100.0" prefWidth="200.0" GridPane.columnIndex="1"
GridPane.rowIndex="2">
<RadioButton fx:id="wordlistPattern" text="From Pattern">
<HBox.margin>
<Insets left="20.0"/>
</HBox.margin>
<toggleGroup>
<ToggleGroup fx:id="wordlistType"/>
</toggleGroup>
</RadioButton>
<RadioButton fx:id="wordlistFile" selected="true" text="From File"
toggleGroup="wordlistType">
<HBox.margin>
<Insets left="20.0"/>
</HBox.margin>
</RadioButton>
</HBox>
<TextField fx:id="wordlist" editable="false" promptText="Select" GridPane.columnIndex="1"
GridPane.rowIndex="3">
<cursor>
<Cursor fx:constant="HAND"/>
</cursor>
@ -48,14 +89,8 @@
<Insets left="20.0"/>
</GridPane.margin>
</TextField>
<Label text="Word List" GridPane.rowIndex="2" />
</children>
<padding>
<Insets left="20.0" right="20.0" />
</padding>
<Label text="Word List" GridPane.rowIndex="3"/>
</GridPane>
<Button fx:id="crackButton" mnemonicParsing="false" prefHeight="25.0" prefWidth="111.0" text="Crack"/>
</children>
</VBox>
</children>
</AnchorPane>

View file

@ -1,13 +1,30 @@
/*
* Copyright 2018 William Brawner
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
apply plugin: 'jacoco'
dependencies {
implementation group: 'commons-io', name: 'commons-io', version: '2.6'
implementation group: 'com.github.mifmif', name: 'generex', version: '1.0.2'
implementation group: 'org.linguafranca.pwdb', name: 'KeePassJava2-kdbx', version: '2.1.4'
}
jacocoTestReport {
reports {
xml.enabled true
html.enabled true
xml.enabled = true
html.enabled = true
html.destination file("${buildDir}/jacoco")
}
}

View file

@ -1,3 +1,18 @@
/*
* Copyright 2018 William Brawner
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.wbrawner.keecrack.lib;
/**
@ -7,6 +22,7 @@ public enum Code {
ERROR_FILE_READ,
ERROR_CRACKING_INTERRUPTED,
ERROR_CRACKING_IN_PROGRESS,
ERROR_MISSING_DATABASE_FILE,
ERROR_MISSING_WORD_LIST_FILE
ERROR_INVALID_DATABASE_FILE,
ERROR_INVALID_WORD_LIST
}

View file

@ -1,34 +1,70 @@
/*
* Copyright 2018 William Brawner
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.wbrawner.keecrack.lib;
import com.wbrawner.keecrack.lib.view.CrackingView;
import com.wbrawner.keecrack.lib.view.FormView;
import com.wbrawner.keecrack.lib.wordlist.WordList;
import org.linguafranca.pwdb.kdbx.KdbxCreds;
import org.linguafranca.pwdb.kdbx.stream_3_1.KdbxHeader;
import org.linguafranca.pwdb.kdbx.stream_3_1.KdbxSerializer;
import java.io.*;
import java.lang.ref.WeakReference;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
import java.time.Duration;
import java.time.Instant;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
/**
* The main class responsible for handling the brute forcing of the KeePass database. You do not contruct the
* KeeCrack instance directly, but rather call {@link #getInstance()}. To begin, you should set the form view and
* cracking view with {@link #setFormView(FormView)} and {@link #setCrackingView(CrackingView)} respectively. These
* views will be responsible for displaying information like error messages and status updates. The cracking will
* work without these, though it's highly recommended to set them prior to beginning. The database and wordlist must
* be set, while the key file is also optional. If either of the required parameters are missing, the
* {@link #attack()} operation will abort, sending either {@link Code#ERROR_INVALID_DATABASE_FILE} or
* {@link Code#ERROR_INVALID_WORD_LIST}, respectively. The word list can either be a pattern, in which case
* incremental guessing will take place, or a file, in which case each line of the file will be considered a password
* to guess. Use {@link #setWordListPattern(String)} or {@link #setWordListFile(File)} respectively to achieve the
* desired guess strategy. If you need to interrupt the attack for any reason, you can call {@link #abort} and the
* cracking will stop on the next guess, sending {@link Code#ERROR_CRACKING_INTERRUPTED} to the views, provided they
* are set. For each guess, {@link CrackingView#onPasswordGuess(String)} is called, in case you'd like to do
* something with the passwords that have already been attempted. Upon a successful password guess, the
* {@link CrackingView#onResult(String, int, Duration)} method will be called with the first parameter as the correct
* password. If the password cannot be guessed with the words provided, then the same method will be called but the
* first parameter will be null.
*/
public class KeeCrack {
private static final AtomicReference<KeeCrack> singleton = new AtomicReference<>(null);
private final Object keyFileLock = new Object();
private WeakReference<FormView> formView = new WeakReference<>(null);
private WeakReference<CrackingView> crackingView = new WeakReference<>(null);
private final AtomicBoolean isCracking = new AtomicBoolean(false);
private File databaseFile;
private File keyFile;
private File wordlistFile;
/**
* This is used to abort cracking
*/
private final AtomicBoolean abort = new AtomicBoolean(false);
private final AtomicReference<FormView> formView = new AtomicReference<>(null);
private final AtomicReference<CrackingView> crackingView = new AtomicReference<>(null);
private File databaseFile;
private File keyFile;
private byte[] databaseBytes;
private byte[] keyBytes;
private WordList wordList;
private int guessCount = 0;
private KeeCrack() {
@ -49,7 +85,7 @@ public class KeeCrack {
public void reset() {
setDatabaseFile(null);
setKeyFile(null);
setWordlistFile(null);
setWordListFile(null);
setCrackingView(null);
setFormView(null);
}
@ -63,12 +99,12 @@ public class KeeCrack {
*/
public void attack() {
if (databaseFile == null || !databaseFile.exists() || !databaseFile.canRead()) {
sendErrorCode(Code.ERROR_MISSING_DATABASE_FILE);
sendErrorCode(Code.ERROR_INVALID_DATABASE_FILE);
return;
}
if (wordlistFile == null || !wordlistFile.exists() || !wordlistFile.canRead()) {
sendErrorCode(Code.ERROR_MISSING_WORD_LIST_FILE);
if (wordList == null) {
sendErrorCode(Code.ERROR_INVALID_WORD_LIST);
return;
}
@ -81,56 +117,73 @@ public class KeeCrack {
guessCount = 0;
Instant startTime = Instant.now();
try (BufferedReader wordReader = new BufferedReader(new FileReader(wordlistFile))) {
String line = null;
boolean haveCorrectPassword = false;
while (!haveCorrectPassword && (line = wordReader.readLine()) != null) {
prepareByteArrays();
while (!haveCorrectPassword && wordList.hasNext()) {
if (abort.get()) {
sendErrorCode(Code.ERROR_CRACKING_INTERRUPTED);
isCracking.set(false);
abort.set(false);
return;
}
CrackingView view = crackingView.get();
if (view != null)
view.onPasswordGuess(line);
line = wordList.nextWord();
try {
//noinspection ConstantConditions
crackingView.get().onPasswordGuess(line);
} catch (NullPointerException ignored) {
}
haveCorrectPassword = guessPassword(line);
}
CrackingView view = crackingView.get();
if (view != null) {
Duration duration = Duration.between(startTime, Instant.now());
String password = null;
if (haveCorrectPassword) {
password = line;
}
view.onResult(password, guessCount, duration);
try {
//noinspection ConstantConditions
crackingView.get().onResult(password, guessCount, duration);
} catch (NullPointerException ignored) {
}
} catch (IOException e) {
e.printStackTrace();
sendErrorCode(Code.ERROR_FILE_READ);
} finally {
isCracking.set(false);
}
@SuppressWarnings("ResultOfMethodCallIgnored")
private void prepareByteArrays() {
databaseBytes = Utils.getFileBytes(databaseFile);
synchronized (keyFileLock) {
if (keyFile == null) {
return;
}
keyBytes = Utils.getFileBytes(keyFile);
}
}
private boolean guessPassword(String password) {
guessCount++;
try (InputStream inputStream = new FileInputStream(databaseFile)) {
InputStream databaseInput = null;
InputStream keyFileInput = null;
try {
databaseInput = new ByteArrayInputStream(databaseBytes);
KdbxHeader databaseHeader = new KdbxHeader();
KdbxCreds credentials;
synchronized (keyFileLock) {
if (keyFile == null) {
if (keyBytes == null || keyBytes.length == 0) {
credentials = new KdbxCreds(password.getBytes());
} else {
credentials = new KdbxCreds(password.getBytes(), new FileInputStream(keyFile));
keyFileInput = new ByteArrayInputStream(keyBytes);
credentials = new KdbxCreds(password.getBytes(), keyFileInput);
}
}
KdbxSerializer.createUnencryptedInputStream(credentials, databaseHeader, inputStream);
KdbxSerializer.createUnencryptedInputStream(credentials, databaseHeader, databaseInput);
return true;
} catch (IllegalStateException ignored) {
// This happens when an incorrect guess occurs. Expected behavior, so we ignore it
} catch (Exception e) {
e.printStackTrace();
} finally {
Utils.closeQuietly(databaseInput);
Utils.closeQuietly(keyFileInput);
}
return false;
}
@ -178,26 +231,63 @@ public class KeeCrack {
}
}
public File getWordlistFile() {
return wordlistFile;
}
public void setWordlistFile(File wordlistFile) {
this.wordlistFile = wordlistFile;
public void setWordListFile(File wordlistFile) {
String response = null;
if (wordlistFile == null) {
this.wordList = null;
} else {
if (!wordlistFile.exists() || !wordlistFile.canRead()) {
try {
String response = (wordlistFile == null) ? null : wordlistFile.getName();
//noinspection ConstantConditions
formView.get().onWordListFileSet(response);
crackingView.get().onError(Code.ERROR_INVALID_WORD_LIST);
} catch (NullPointerException ignored) {
}
this.wordList = null;
return;
}
this.wordList = new WordList(wordlistFile);
response = wordlistFile.getName();
}
try {
//noinspection ConstantConditions
formView.get().onWordListSet(response);
} catch (NullPointerException ignored) {
}
}
public void setWordListPattern(String pattern) {
if (pattern == null) {
this.wordList = null;
} else {
try {
this.wordList = new WordList(pattern);
} catch (IllegalArgumentException ignored) {
// This can be thrown if the user has entered an invalid regular expression
}
}
try {
//noinspection ConstantConditions
formView.get().onWordListSet(pattern);
} catch (NullPointerException ignored) {
}
}
WordList getWordList() {
return this.wordList;
}
public String getWordListName() {
if (this.wordList == null)
return null;
return this.wordList.getName();
}
public void setFormView(FormView formView) {
this.formView = new WeakReference<>(formView);
this.formView.set(formView);
}
public void setCrackingView(CrackingView crackingView) {
this.crackingView = new WeakReference<>(crackingView);
this.crackingView.set(crackingView);
}
public boolean isCracking() {

View file

@ -0,0 +1,44 @@
/*
* Copyright 2018 William Brawner
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.wbrawner.keecrack.lib;
import java.io.*;
class Utils {
static void closeQuietly(Closeable closeable) {
if (closeable == null) {
return;
}
try {
closeable.close();
} catch (IOException ignored) {
}
}
@SuppressWarnings("ResultOfMethodCallIgnored")
static byte[] getFileBytes(File file) {
byte[] fileBytes;
try (InputStream inputStream = new FileInputStream(file)) {
fileBytes = new byte[inputStream.available()];
inputStream.read(fileBytes);
return fileBytes;
} catch (IOException e) {
e.printStackTrace();
return new byte[]{};
}
}
}

View file

@ -1,3 +1,18 @@
/*
* Copyright 2018 William Brawner
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.wbrawner.keecrack.lib.view;
import com.wbrawner.keecrack.lib.Code;

View file

@ -1,3 +1,18 @@
/*
* Copyright 2018 William Brawner
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.wbrawner.keecrack.lib.view;
import java.time.Duration;
@ -5,12 +20,14 @@ import java.time.Duration;
public interface CrackingView extends BaseView {
/**
* Called prior to each guess
*
* @param password The password that will be guessed
*/
void onPasswordGuess(final String password);
/**
* Called when the password has been successfully guessed, or there are no more passwords to guess
*
* @param password The password, if successfully guessed, or null if no passwords were successful
* @param guessCount The number of passwords guessed
* @param timeElapsed The time taken to guess the password

View file

@ -1,7 +1,24 @@
/*
* Copyright 2018 William Brawner
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.wbrawner.keecrack.lib.view;
public interface FormView extends BaseView {
void onDatabaseFileSet(String name);
void onKeyFileSet(String name);
void onWordListFileSet(String name);
void onWordListSet(String name);
}

View file

@ -0,0 +1,57 @@
/*
* Copyright 2018 William Brawner
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.wbrawner.keecrack.lib.wordlist;
import com.mifmif.common.regex.Generex;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
public class WordList {
private WordListIterator iterator;
private String name;
public WordList(File wordFile) {
java.util.Iterator<String> wordListIterator = null;
this.name = wordFile.getName();
try {
wordListIterator = FileUtils.lineIterator(wordFile);
} catch (IOException e) {
e.printStackTrace();
}
iterator = new WordListIterator(wordListIterator);
}
public WordList(String pattern) {
Generex generex = new Generex(pattern);
this.name = pattern;
iterator = new WordListIterator(generex.iterator());
}
public boolean hasNext() {
return iterator.hasNext();
}
public String nextWord() {
return iterator.next();
}
public String getName() {
return this.name;
}
}

View file

@ -0,0 +1,54 @@
/*
* Copyright 2018 William Brawner
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.wbrawner.keecrack.lib.wordlist;
/**
* Since Generex uses their own Iterator interface, that complicates things a bit for us, so this class serves as
* nothing more than a wrapper around both interfaces to prevent complication in other areas of the code;
*/
public class WordListIterator {
private com.mifmif.common.regex.util.Iterator generexIterator;
private java.util.Iterator<String> standardIterator;
WordListIterator(com.mifmif.common.regex.util.Iterator generexIterator) {
this.generexIterator = generexIterator;
}
WordListIterator(java.util.Iterator<String> standardIterator) {
this.standardIterator = standardIterator;
}
public boolean hasNext() {
if (generexIterator != null) {
return generexIterator.hasNext();
}
return standardIterator != null && standardIterator.hasNext();
}
public String next() {
if (generexIterator != null) {
return generexIterator.next();
}
if (standardIterator != null) {
return standardIterator.next();
}
return null;
}
}

View file

@ -1,3 +1,18 @@
/*
* Copyright 2018 William Brawner
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.wbrawner.keecrack.lib;
import com.wbrawner.keecrack.lib.view.CrackingView;
@ -10,6 +25,7 @@ import java.io.File;
import java.io.IOException;
import java.time.Duration;
import static junit.framework.TestCase.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.anyString;
@ -38,26 +54,30 @@ public class KeeCrackTest {
public void resetTest() {
keeCrack.setDatabaseFile(new File("Database"));
keeCrack.setKeyFile(new File("Keyfile"));
keeCrack.setWordlistFile(new File("WordList"));
keeCrack.setWordListPattern("Some pattern");
assertNotNull(keeCrack.getDatabaseFile());
assertNotNull(keeCrack.getKeyFile());
assertNotNull(keeCrack.getWordlistFile());
assertNotNull(keeCrack.getWordList());
keeCrack.reset();
assertNull(keeCrack.getDatabaseFile());
assertNull(keeCrack.getKeyFile());
assertNull(keeCrack.getWordlistFile());
assertNull(keeCrack.getWordList());
assertFalse(keeCrack.isCracking());
}
@Test
public void abortTest() throws IOException {
keeCrack.setDatabaseFile(Utils.getDatabase("123456.kdbx"));
keeCrack.setWordlistFile(Utils.getWordList("valid-words.txt"));
keeCrack.setWordListFile(Utils.getWordList("valid-words.txt"));
keeCrack.abort();
keeCrack.setCrackingView(mockCrackingView);
keeCrack.attack();
verify(mockCrackingView, times(1)).onError(Code.ERROR_CRACKING_INTERRUPTED);
}
/**
* This ensures that both views receive any errors sent
*/
@Test
public void sendErrorTest() {
keeCrack.setCrackingView(mockCrackingView);
@ -67,6 +87,9 @@ public class KeeCrackTest {
verify(mockFormView, times(1)).onError(Code.ERROR_CRACKING_IN_PROGRESS);
}
/**
* This ensures that the form view still receives the error if the cracking view is null
*/
@Test
public void sendErrorWithoutCrackingViewTest() {
keeCrack.setFormView(mockFormView);
@ -74,6 +97,9 @@ public class KeeCrackTest {
verify(mockFormView, times(1)).onError(Code.ERROR_CRACKING_IN_PROGRESS);
}
/**
* This ensures that the cracking view still receives the error if the form view is null
*/
@Test
public void sendErrorWithoutFormViewTest() {
keeCrack.setCrackingView(mockCrackingView);
@ -83,10 +109,10 @@ public class KeeCrackTest {
@Test
public void errorWithoutDatabaseFileTest() throws IOException {
keeCrack.setWordlistFile(Utils.getWordList("valid-words.txt"));
keeCrack.setWordListFile(Utils.getWordList("valid-words.txt"));
keeCrack.setCrackingView(mockCrackingView);
keeCrack.attack();
verify(mockCrackingView, times(1)).onError(Code.ERROR_MISSING_DATABASE_FILE);
verify(mockCrackingView, times(1)).onError(Code.ERROR_INVALID_DATABASE_FILE);
}
@Test
@ -94,14 +120,14 @@ public class KeeCrackTest {
keeCrack.setDatabaseFile(Utils.getDatabase("123456.kdbx"));
keeCrack.setCrackingView(mockCrackingView);
keeCrack.attack();
verify(mockCrackingView, times(1)).onError(Code.ERROR_MISSING_WORD_LIST_FILE);
verify(mockCrackingView, times(1)).onError(Code.ERROR_INVALID_WORD_LIST);
}
@Test
public void guessCorrectPasswordTest() throws IOException {
public void guessCorrectPasswordFromFileTest() throws IOException {
keeCrack.setDatabaseFile(Utils.getDatabase("123456.kdbx"));
keeCrack.setWordlistFile(Utils.getWordList("valid-words.txt"));
keeCrack.setWordListFile(Utils.getWordList("valid-words.txt"));
keeCrack.setCrackingView(mockCrackingView);
keeCrack.attack();
verify(mockCrackingView, times(1)).onPasswordGuess("123456");
@ -109,20 +135,42 @@ public class KeeCrackTest {
}
@Test
public void guessCorrectPasswordAndKeyTest() throws IOException {
public void guessCorrectPasswordFromFileAndKeyTest() throws IOException {
keeCrack.setDatabaseFile(Utils.getDatabase("123456-key.kdbx"));
keeCrack.setKeyFile(Utils.getKeyFile());
keeCrack.setWordlistFile(Utils.getWordList("valid-words.txt"));
keeCrack.setWordListFile(Utils.getWordList("valid-words.txt"));
keeCrack.setCrackingView(mockCrackingView);
keeCrack.attack();
verify(mockCrackingView, times(1)).onPasswordGuess("123456");
verify(mockCrackingView, times(1)).onResult(eq("123456"), eq(1), any(Duration.class));
}
@Test
public void keepGuessingUntilCorrectPasswordTest() throws IOException {
public void guessCorrectPasswordFromPatternTest() throws IOException {
keeCrack.setDatabaseFile(Utils.getDatabase("0000.kdbx"));
keeCrack.setWordListPattern("[0-9]{4}");
keeCrack.setCrackingView(mockCrackingView);
keeCrack.attack();
verify(mockCrackingView, times(1)).onPasswordGuess(anyString());
verify(mockCrackingView, times(1)).onResult(eq("0000"), eq(1), any(Duration.class));
}
@Test
public void guessCorrectPasswordFromPatternAndKeyTest() throws IOException {
keeCrack.setDatabaseFile(Utils.getDatabase("0000-key.kdbx"));
keeCrack.setWordListPattern("[0-9]{4}");
keeCrack.setKeyFile(Utils.getKeyFile());
keeCrack.setCrackingView(mockCrackingView);
keeCrack.attack();
verify(mockCrackingView, times(1)).onPasswordGuess(anyString());
verify(mockCrackingView, times(1)).onResult(eq("0000"), eq(1), any(Duration.class));
}
@Test
public void keepGuessingUntilCorrectPasswordFromFileTest() throws IOException {
keeCrack.setDatabaseFile(Utils.getDatabase("redwings.kdbx"));
keeCrack.setWordlistFile(Utils.getWordList("valid-words.txt"));
keeCrack.setWordListFile(Utils.getWordList("valid-words.txt"));
keeCrack.setCrackingView(mockCrackingView);
keeCrack.attack();
verify(mockCrackingView, times(2)).onPasswordGuess(anyString());
@ -130,20 +178,41 @@ public class KeeCrackTest {
}
@Test
public void keepGuessingUntilCorrectPasswordAndKeyTest() throws IOException {
public void keepGuessingUntilCorrectPasswordFromFileAndKeyTest() throws IOException {
keeCrack.setDatabaseFile(Utils.getDatabase("redwings-key.kdbx"));
keeCrack.setKeyFile(Utils.getKeyFile());
keeCrack.setWordlistFile(Utils.getWordList("valid-words.txt"));
keeCrack.setWordListFile(Utils.getWordList("valid-words.txt"));
keeCrack.setCrackingView(mockCrackingView);
keeCrack.attack();
verify(mockCrackingView, times(2)).onPasswordGuess(anyString());
verify(mockCrackingView, times(1)).onResult(eq("redwings"), eq(2), any(Duration.class));
}
@Test
public void keepGuessingUntilCorrectPasswordFromPatternTest() throws IOException {
keeCrack.setDatabaseFile(Utils.getDatabase("ab.kdbx"));
keeCrack.setWordListPattern("[a-z]{2}");
keeCrack.setCrackingView(mockCrackingView);
keeCrack.attack();
verify(mockCrackingView, times(2)).onPasswordGuess(anyString());
verify(mockCrackingView, times(1)).onResult(eq("ab"), eq(2), any(Duration.class));
}
@Test
public void keepGuessingUntilCorrectPasswordFromPatternAndKeyTest() throws IOException {
keeCrack.setDatabaseFile(Utils.getDatabase("ab-key.kdbx"));
keeCrack.setWordListPattern("[a-z]{2}");
keeCrack.setKeyFile(Utils.getKeyFile());
keeCrack.setCrackingView(mockCrackingView);
keeCrack.attack();
verify(mockCrackingView, times(2)).onPasswordGuess(anyString());
verify(mockCrackingView, times(1)).onResult(eq("ab"), eq(2), any(Duration.class));
}
@Test
public void failToCrackPasswordTest() throws IOException {
keeCrack.setDatabaseFile(Utils.getDatabase("123456.kdbx"));
keeCrack.setWordlistFile(Utils.getWordList("invalid-words.txt"));
keeCrack.setWordListFile(Utils.getWordList("invalid-words.txt"));
keeCrack.setCrackingView(mockCrackingView);
keeCrack.attack();
verify(mockCrackingView, times(3)).onPasswordGuess(anyString());
@ -154,7 +223,7 @@ public class KeeCrackTest {
public void failToCrackInvalidPasswordAndValidKeyTest() throws IOException {
keeCrack.setDatabaseFile(Utils.getDatabase("123456-key.kdbx"));
keeCrack.setKeyFile(Utils.getKeyFile());
keeCrack.setWordlistFile(Utils.getWordList("invalid-words.txt"));
keeCrack.setWordListFile(Utils.getWordList("invalid-words.txt"));
keeCrack.setCrackingView(mockCrackingView);
keeCrack.attack();
verify(mockCrackingView, times(3)).onPasswordGuess(anyString());
@ -165,7 +234,7 @@ public class KeeCrackTest {
public void failToCrackValidPasswordAndInvalidKeyTest() throws IOException {
keeCrack.setDatabaseFile(Utils.getDatabase("123456-key.kdbx"));
keeCrack.setKeyFile(Utils.getInvalidKeyFile());
keeCrack.setWordlistFile(Utils.getWordList("valid-words.txt"));
keeCrack.setWordListFile(Utils.getWordList("valid-words.txt"));
keeCrack.setCrackingView(mockCrackingView);
keeCrack.attack();
verify(mockCrackingView, times(3)).onPasswordGuess(anyString());

View file

@ -1,3 +1,18 @@
/*
* Copyright 2018 William Brawner
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.wbrawner.keecrack.lib;
import java.io.*;

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1 +1,16 @@
/*
* Copyright 2018 William Brawner
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
include 'keecrack-lib', 'keecrack-gui', 'keecrack-cli'