Skip to content

Commit 880608d

Browse files
Merge branch 'dev'
2 parents 4f74626 + 596fb6c commit 880608d

1 file changed

Lines changed: 75 additions & 81 deletions

File tree

subprocess/src/main/java/dev/ftb/app/storage/CredentialStorage.java

Lines changed: 75 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111
import javax.crypto.spec.PBEKeySpec;
1212
import javax.crypto.spec.SecretKeySpec;
1313
import java.io.IOException;
14-
import java.net.NetworkInterface;
15-
import java.net.SocketException;
1614
import java.nio.file.Files;
1715
import java.nio.file.Path;
1816
import java.security.InvalidAlgorithmParameterException;
@@ -23,31 +21,50 @@
2321
import java.util.*;
2422

2523
/**
26-
* A non-ideal credential storage system backed by the users mac address.
24+
* A non-secure storage for user credentials. The key is generated from consistent system information
25+
* to effectively fingerprint the file to the user's machine. This is primarily to prevent relatively
26+
* simple grab and dump style attacks. Ultimately, this information should be store on the users systems
27+
* keychain, windows credential manager, or equivalent but this is a decent stop gap.
2728
*/
2829
public class CredentialStorage {
2930
private static final Logger LOGGER = LoggerFactory.getLogger(CredentialStorage.class);
3031
private static CredentialStorage INSTANCE;
3132

33+
private final String encryptionKey;
34+
3235
private HashMap<String, String> credentials = new HashMap<>();
33-
private final byte[] macAddress;
3436

3537
private CredentialStorage() {
36-
macAddress = getMacAddress();
38+
encryptionKey = generateEncryptionKey();
3739
LOGGER.info("Loading credentials for user");
3840
try {
39-
// If it fails, it shouldn't be fatal. It will be a bit annoying for the user
41+
// If it fails, it shouldn't be fatal. It will be a bit annoying for the user,
4042
// but it's not the end of the world.
4143
load();
42-
} catch (Exception e) {
43-
LOGGER.error("Failed to load credentials", e);
44+
} catch (NoSuchPaddingException | IllegalBlockSizeException | NoSuchAlgorithmException |
45+
InvalidKeySpecException | BadPaddingException | InvalidAlgorithmParameterException |
46+
InvalidKeyException | IOException e) {
47+
// In some cases, we want to just delete the file to reset the credentials.
48+
// In others, it's a legitimate error we can't recover from.
49+
if (e instanceof BadPaddingException) {
50+
LOGGER.warn("Failed to load credentials, deleting credentials file to reset", e);
51+
try {
52+
Files.deleteIfExists(AppMain.paths().credentialsFiles());
53+
} catch (IOException ioException) {
54+
// RIP.
55+
LOGGER.error("Failed to delete credentials file", ioException);
56+
}
57+
} else {
58+
LOGGER.error("Failed to load credentials", e);
59+
}
4460
}
4561
}
4662

4763
public static CredentialStorage getInstance() {
4864
if (CredentialStorage.INSTANCE == null) {
4965
CredentialStorage.INSTANCE = new CredentialStorage();
5066
}
67+
5168
return CredentialStorage.INSTANCE;
5269
}
5370

@@ -77,19 +94,14 @@ public HashMap<String, String> getCredentials() {
7794
/**
7895
* Use the systems mac address to encrypt the users credentials.
7996
*/
80-
private boolean save() {
81-
if (macAddress == null) {
82-
LOGGER.error("Failed to save credentials, mac address is null");
83-
return false;
84-
}
85-
97+
private void save() {
8698
// Convert the credentials to a json string
8799
String credentialsJson = "";
88100
try {
89101
credentialsJson = new Gson().toJson(credentials);
90102
} catch (Exception e) {
91103
LOGGER.error("Failed to convert credentials to json", e);
92-
return false;
104+
return;
93105
}
94106

95107
// Encrypt the credentials
@@ -113,96 +125,78 @@ private boolean save() {
113125

114126
// Save the data to the file system
115127
Files.writeString(AppMain.paths().credentialsFiles(), encryptedCredentials);
116-
return true;
117128
} catch (Exception e) {
118129
LOGGER.error("Failed to encrypt credentials", e);
119-
return false;
120130
}
121131
}
122132

123-
private boolean load() {
124-
if (macAddress == null) {
125-
LOGGER.error("Failed to load credentials, mac address is null");
126-
return false;
127-
}
128-
133+
private void load() throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, IOException {
129134
// Load the encrypted credentials from the file system
130135
Path credentials = AppMain.paths().credentialsFiles();
131136
if (!Files.exists(credentials)) {
132137
LOGGER.warn("Failed to load credentials, credentials file does not exist");
133-
return false;
138+
return;
134139
}
135140

136141
// Decrypt the credentials
137-
try {
138-
String encryptedCredentials = Files.readString(credentials);
142+
String encryptedCredentials = Files.readString(credentials);
139143

140-
var userHome = System.getProperty("user.home");
141-
// Use the user home directory as the IV
142-
byte[] iv = userHome.getBytes();
143-
144-
GCMParameterSpec spec = new GCMParameterSpec(128, iv);
145-
146-
var key = generateKey();
147-
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
148-
cipher.init(Cipher.DECRYPT_MODE, key, spec);
149-
150-
byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedCredentials));
151-
String decryptedCredentials = new String(decryptedBytes);
152-
153-
// Convert the decrypted credentials to a HashMap
154-
this.credentials = new Gson().fromJson(decryptedCredentials, HashMap.class);
155-
} catch (IOException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeySpecException |
156-
InvalidKeyException | IllegalBlockSizeException | BadPaddingException |
157-
InvalidAlgorithmParameterException e) {
158-
throw new RuntimeException(e);
159-
}
144+
var userHome = System.getProperty("user.home");
145+
// Use the user home directory as the IV
146+
byte[] iv = userHome.getBytes();
160147

161-
return true;
148+
GCMParameterSpec spec = new GCMParameterSpec(128, iv);
149+
150+
var key = generateKey();
151+
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
152+
cipher.init(Cipher.DECRYPT_MODE, key, spec);
153+
154+
byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedCredentials));
155+
String decryptedCredentials = new String(decryptedBytes);
156+
157+
// Convert the decrypted credentials to a HashMap
158+
this.credentials = new Gson().fromJson(decryptedCredentials, HashMap.class);
162159
}
163160

164161
private SecretKeySpec generateKey() throws NoSuchAlgorithmException, InvalidKeySpecException {
165162
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
166-
KeySpec spec = new PBEKeySpec(new String(macAddress).toCharArray(), "FTBAPP".getBytes(), 65536, 256);
163+
KeySpec spec = new PBEKeySpec(encryptionKey.toCharArray(), "FTBAPP".getBytes(), 65536, 256);
167164
return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");
168165
}
169166

170-
public static byte[] getMacAddress() {
171-
try {
172-
List<NetworkInterface> networkInterfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
173-
Optional<NetworkInterface> sortedNetworks = networkInterfaces.stream()
174-
.filter(CredentialStorage::networkIsValid)
175-
.min(Comparator.comparing(NetworkInterface::getName));
176-
177-
if (sortedNetworks.isPresent()) {
178-
var network = sortedNetworks.get();
179-
var mac = network.getHardwareAddress();
180-
LOGGER.debug("Interface: {} : {}", network.getDisplayName(), network.getName());
181-
var address = new byte[mac.length * 10];
182-
for (int i = 0; i < address.length; i++) {
183-
address[i] = mac[i - (Math.round(i / mac.length) * mac.length)];
184-
}
185-
186-
return address;
187-
}
188-
} catch (SocketException e) {
189-
LOGGER.warn("Exception getting MAC address", e);
167+
private String generateEncryptionKey() {
168+
String appName = "FTBApp";
169+
String storageName = "CredentialStorageV1";
170+
171+
String userHome = System.getProperty("user.home");
172+
String osName = System.getProperty("os.name");
173+
String osArch = System.getProperty("os.arch");
174+
String timeZone = System.getProperty("user.timezone");
175+
176+
List<String> components = shuffleBasedOnUserHome(userHome, appName, storageName, userHome, osName, osArch, timeZone);
177+
StringBuilder keyBuilder = new StringBuilder();
178+
for (String component : components) {
179+
keyBuilder.append(component).append("|");
190180
}
191-
192-
LOGGER.warn("Failed to get MAC address, using default logindata key");
193-
return new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 };
181+
182+
return keyBuilder.toString();
194183
}
195-
196-
private static boolean networkIsValid(NetworkInterface network) {
197-
try {
198-
var mac = network.getHardwareAddress();
199-
var name = network.getName();
200184

201-
return mac != null && mac.length > 0 && !network.isLoopback() && !network.isVirtual() && !network.isPointToPoint() && !name.startsWith("ham") && !name.startsWith("vir") && !name.startsWith("docker") && !name.startsWith("br-");
202-
} catch (Throwable error) {
203-
LOGGER.error("Failed to check if network is valid", error);
185+
/**
186+
* This should create a seed that produces a consistent shuffle based on the user's home directory.
187+
* This way the order of the components is unique to the user but consistent across runs.
188+
*
189+
* @param userHome The user's home directory
190+
* @param components The components to shuffle
191+
* @return A shuffled list of components
192+
*/
193+
private List<String> shuffleBasedOnUserHome(String userHome, String... components) {
194+
List<String> componentList = new ArrayList<>(Arrays.asList(components));
195+
long seed = 0;
196+
for (char c : userHome.toCharArray()) {
197+
seed += c;
204198
}
205-
206-
return false;
199+
Collections.shuffle(componentList, new Random(seed));
200+
return componentList;
207201
}
208202
}

0 commit comments

Comments
 (0)