1111import javax .crypto .spec .PBEKeySpec ;
1212import javax .crypto .spec .SecretKeySpec ;
1313import java .io .IOException ;
14- import java .net .NetworkInterface ;
15- import java .net .SocketException ;
1614import java .nio .file .Files ;
1715import java .nio .file .Path ;
1816import java .security .InvalidAlgorithmParameterException ;
2321import 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 */
2829public 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