diff --git a/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java b/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java index b74752f24..9e4edf2f6 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/commandInjection/CommandInjection.java @@ -37,19 +37,48 @@ public class CommandInjection { private static final Pattern IP_ADDRESS_PATTERN = Pattern.compile("\\b((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\\.|$)){4}\\b"); + /** + * Defense-in-depth guard applied inside {@link #getResponseFromPingCommand} itself, on top of + * whatever each level's own {@code isValid} check decided. Restricting the character set to + * letters, digits, dots and hyphens means no shell metacharacter, whitespace, or control + * character can ever reach the ping command - regardless of which per-level validator (or + * bypass of one) let a value through. + */ + private static final Pattern SHELL_SAFE_TARGET_PATTERN = + Pattern.compile("[A-Za-z0-9][A-Za-z0-9.-]{0,252}"); + + /** + * Blocklisting individual metacharacters/encodings (as LEVEL_1-5 originally did) is + * inherently incomplete - there is always another separator/encoding the blocklist forgot + * about. The only sound check is a strict allowlist validating the exact value that will be + * concatenated into the shell command: either a dotted-quad IPv4 address or the literal + * string "localhost". This mirrors the approach already used by the LEVEL_6 secure + * reference below. + */ + private static boolean isSafePingTarget(String ipAddress) { + return StringUtils.isNotBlank(ipAddress) + && (IP_ADDRESS_PATTERN.matcher(ipAddress).matches() + || ipAddress.contentEquals("localhost")); + } + StringBuilder getResponseFromPingCommand(String ipAddress, boolean isValid) throws IOException { boolean isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows"); StringBuilder stringBuilder = new StringBuilder(); - if (isValid) { + if (isValid + && ipAddress != null + && SHELL_SAFE_TARGET_PATTERN.matcher(ipAddress).matches()) { Process process; + // Passing the target as its own argv element (rather than concatenating it into a + // "sh -c" string) means there is no shell left to parse metacharacters out of it in + // the first place - the allowlist above is a second, independent line of defense. if (!isWindows) { process = - new ProcessBuilder(new String[] {"sh", "-c", "ping -c 2 " + ipAddress}) + new ProcessBuilder("ping", "-c", "2", ipAddress) .redirectErrorStream(true) .start(); } else { process = - new ProcessBuilder(new String[] {"cmd", "/c", "ping -n 2 " + ipAddress}) + new ProcessBuilder("ping", "-n", "2", ipAddress) .redirectErrorStream(true) .start(); } @@ -133,14 +162,7 @@ public ResponseEntity> getVulnerablePay @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws ServiceApplicationException, IOException { - Supplier validator = - () -> - StringUtils.isNotBlank(ipAddress) - && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN - .matcher(requestEntity.getUrl().toString()) - .find() - && !requestEntity.getUrl().toString().toUpperCase().contains("%26") - && !requestEntity.getUrl().toString().toUpperCase().contains("%3B"); + Supplier validator = () -> isSafePingTarget(ipAddress); return new ResponseEntity>( new GenericVulnerabilityResponseBean( this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), @@ -157,15 +179,7 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel5( @RequestParam(IP_ADDRESS) String ipAddress, RequestEntity requestEntity) throws IOException { - Supplier validator = - () -> - StringUtils.isNotBlank(ipAddress) - && !SEMICOLON_SPACE_LOGICAL_AND_PATTERN - .matcher(requestEntity.getUrl().toString()) - .find() - && !requestEntity.getUrl().toString().toUpperCase().contains("%26") - && !requestEntity.getUrl().toString().toUpperCase().contains("%3B") - && !requestEntity.getUrl().toString().toUpperCase().contains("%7C"); + Supplier validator = () -> isSafePingTarget(ipAddress); return new ResponseEntity>( new GenericVulnerabilityResponseBean( this.getResponseFromPingCommand(ipAddress, validator.get()).toString(), diff --git a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java index 7c854f3c1..7515ed89e 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/CryptographicFailuresVulnerability.java @@ -43,6 +43,16 @@ public CryptographicFailuresVulnerability( private static final String PASSWORD_PARAM = "password"; + /** + * Levels 2-4 used to publish the real secret run through a reversible transform (Base64, + * Caesar, a custom cipher), which is exactly what let an attacker undo the transform and + * recover the real password. Now that the vault only ever holds a BCrypt digest, there is + * nothing reversible left to show for those levels - so the flavour text instead shows that + * same transform applied to this fixed, non-secret sample word. Decoding it recovers only + * this sample, never the real password. + */ + private static final String DECOY_SAMPLE = "opensesame42"; + // Level 1: Plaintext storage — password leaked in response (CWE-326) @AttackVector( vulnerabilityExposed = VulnerabilityType.INSECURE_CRYPTOGRAPHIC_STORAGE, @@ -58,29 +68,36 @@ public ResponseEntity> getVulnerablePay String password = queryParams.get(PASSWORD_PARAM); if (password == null || password.isEmpty()) { - // Vulnerable: password is exposed in plaintext in the API response + // Fixed: the vault never stores the raw password anymore, only a salted BCrypt + // hash of it. The hash itself is still shown (same as the LEVEL_11 secure + // reference) - unlike plaintext, publishing a BCrypt digest does not disclose the + // password because the hash cannot be reversed back into it. return new ResponseEntity<>( new GenericVulnerabilityResponseBean<>( - "CHALLENGE: The system stores passwords in plaintext." - + " Check the database for the password to crack the challenge", + "CHALLENGE: The system used to store passwords in plaintext. It now" + + " stores only a salted BCrypt hash: " + + LEVEL_1_SECRET + + " — this digest cannot be reversed back into the password.", false), HttpStatus.OK); } // Verify the guess - if (password.equals(LEVEL_1_SECRET)) { + if (PasswordHashingUtils.isValidBcrypt(password, LEVEL_1_SECRET)) { return new ResponseEntity<>( new GenericVulnerabilityResponseBean<>( - "Correct! The password '" - + LEVEL_1_SECRET - + "' was stored in plaintext with no encryption or hashing." - + " Anyone with access to the storage can read it directly.", + "Correct! The password was '" + + password + + "'. The vault now stores a salted BCrypt hash instead of the" + + " plaintext value, so reading the storage no longer discloses the" + + " secret.", true), HttpStatus.OK); } else { return new ResponseEntity<>( new GenericVulnerabilityResponseBean<>( - "Incorrect. Hint: Check the database for plaintext storage", false), + "Incorrect. The stored value is now a salted BCrypt hash, not plaintext.", + false), HttpStatus.OK); } } @@ -95,38 +112,41 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel2( @RequestParam Map queryParams) { - String LEVEL_2_ENCODED = repo.findPasswordByLevelName(LevelConstants.LEVEL_2); + String LEVEL_2_HASH = repo.findPasswordByLevelName(LevelConstants.LEVEL_2); String password = queryParams.get(PASSWORD_PARAM); if (password == null || password.isEmpty()) { + // Fixed: the vault no longer stores a reversible Base64 encoding of the real + // password, only a salted BCrypt hash. The hint keeps showing a Base64 string for + // flavour, but it now encodes only the fixed DECOY_SAMPLE, never the real secret - + // decoding it teaches nothing about the actual password. return new ResponseEntity<>( new GenericVulnerabilityResponseBean<>( - "CHALLENGE: The system 'encodes' passwords." - + "The stored password is: " - + LEVEL_2_ENCODED - + " — Decode it and enter the original password!", + "CHALLENGE: The system 'encodes' passwords with Base64. A sample encoded" + + " value looks like: " + + EncodingUtils.encodeBase64(DECOY_SAMPLE) + + " — but the real password is stored only as a salted BCrypt" + + " hash, which cannot be decoded back to the original value.", false), HttpStatus.OK); } // Verify the guess - String passwordGuess = EncodingUtils.encodeBase64(password); - if (passwordGuess.equals(LEVEL_2_ENCODED)) { + if (PasswordHashingUtils.isValidBcrypt(password, LEVEL_2_HASH)) { return new ResponseEntity<>( new GenericVulnerabilityResponseBean<>( "Correct! The password was '" + password - + "'. Base64 is an encoding, NOT encryption." - + " It provides zero security — anyone can decode it instantly.", + + "'. Base64 was an encoding, NOT encryption, and provided zero" + + " security. The vault now stores a salted BCrypt hash instead.", true), HttpStatus.OK); } else { return new ResponseEntity<>( new GenericVulnerabilityResponseBean<>( - "Incorrect. Your password resulted in '" - + passwordGuess - + "' . Look for the patterns in your guesses to determine the encoding and crack the password.", + "Incorrect. The stored value is now a salted BCrypt hash, not a Base64" + + " encoding, so it cannot simply be decoded.", false), HttpStatus.OK); } @@ -142,37 +162,41 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel3( @RequestParam Map queryParams) throws EncryptionException { - String LEVEL_3_CIPHERTEXT = repo.findPasswordByLevelName(LevelConstants.LEVEL_3); + String LEVEL_3_HASH = repo.findPasswordByLevelName(LevelConstants.LEVEL_3); String password = queryParams.get(PASSWORD_PARAM); if (password == null || password.isEmpty()) { + // Fixed: the vault no longer stores a Caesar-shifted (trivially reversible) + // ciphertext of the real password, only a salted BCrypt hash. The shifted sample + // shown below is computed from the fixed DECOY_SAMPLE, not the real secret. return new ResponseEntity<>( new GenericVulnerabilityResponseBean<>( - "CHALLENGE: A user's password is encrypted using an insecure cipher: " - + LEVEL_3_CIPHERTEXT - + " — Crack it and enter the original password!", + "CHALLENGE: A user's password used to be 'encrypted' with a Caesar" + + " cipher, e.g.: " + + EncryptionUtils.caesarCipher(DECOY_SAMPLE, 3) + + " — It is now stored as a salted BCrypt hash, which has no fixed" + + " shift to reverse.", false), HttpStatus.OK); } // Verify the guess - String passwordGuess = EncryptionUtils.caesarCipher(password, 3); - if (passwordGuess.equals(LEVEL_3_CIPHERTEXT)) { + if (PasswordHashingUtils.isValidBcrypt(password, LEVEL_3_HASH)) { return new ResponseEntity<>( new GenericVulnerabilityResponseBean<>( "Correct! The password was '" + password - + "'. Caesar Cipher is an insecure cipher and is trivial to crack." - + " There is both a limited number of mutations and deterministic output", + + "'. Caesar Cipher was trivial to crack since it has a limited" + + " number of shifts and deterministic output. The vault now stores" + + " a salted BCrypt hash instead.", true), HttpStatus.OK); } else { return new ResponseEntity<>( new GenericVulnerabilityResponseBean<>( - "Incorrect. The password is encrypted using an Caesar Cipher. " - + " Caesar shifts the positions of each plaintext character. " - + " — Try find the secret by reversing the character shift.", + "Incorrect. The stored value is now a salted BCrypt hash, not a Caesar" + + " cipher, so there is no shift to reverse.", false), HttpStatus.OK); } @@ -188,36 +212,42 @@ public ResponseEntity> getVulnerablePay public ResponseEntity> getVulnerablePayloadLevel4( @RequestParam Map queryParams) throws EncryptionException { - String LEVEL_4_CIPHERTEXT = repo.findPasswordByLevelName(LevelConstants.LEVEL_4); + String LEVEL_4_HASH = repo.findPasswordByLevelName(LevelConstants.LEVEL_4); String password = queryParams.get(PASSWORD_PARAM); // No password param: return the challenge hash if (password == null || password.isEmpty()) { + // Fixed: the vault no longer stores a reverse-and-Base64 "custom cipher" value of + // the real password, only a salted BCrypt hash. The sample below runs the same + // custom cipher over the fixed DECOY_SAMPLE, not the real secret. return new ResponseEntity<>( new GenericVulnerabilityResponseBean<>( - "CHALLENGE: A user's password is stored using custom logic: " - + LEVEL_4_CIPHERTEXT - + " — Crack it and enter the original password!", + "CHALLENGE: A user's password used to be stored using undisclosed custom" + + " logic, e.g.: " + + EncryptionUtils.customCipher(DECOY_SAMPLE) + + " — It is now stored as a salted BCrypt hash, a standard" + + " reviewed algorithm rather than security-by-obscurity.", false), HttpStatus.OK); } // Verify the guess - String passwordGuess = EncryptionUtils.customCipher(password); - if (passwordGuess.equals(LEVEL_4_CIPHERTEXT)) { + if (PasswordHashingUtils.isValidBcrypt(password, LEVEL_4_HASH)) { return new ResponseEntity<>( new GenericVulnerabilityResponseBean<>( "Correct! The password was '" + password + "'. Security through obscurity or custom logic is not secure." - + " Follow Kirchhoff's principle - Security of cipher is based on key secrecy, not cipher secrecy.", + + " Follow Kirchhoff's principle - Security of cipher is based on" + + " key secrecy, not cipher secrecy. The vault now stores a salted" + + " BCrypt hash instead.", true), HttpStatus.OK); } else { return new ResponseEntity<>( new GenericVulnerabilityResponseBean<>( - "Incorrect. " - + " — Try decoding the password and see if you can figure out the secret", + "Incorrect. The stored value is now a salted BCrypt hash, not the old" + + " custom cipher, so there is no undisclosed scheme to reverse.", false), HttpStatus.OK); } @@ -239,31 +269,35 @@ public ResponseEntity> getVulnerablePay // No password param: return the challenge hash if (password == null || password.isEmpty()) { + // Fixed: the vault no longer hashes with the broken, fast MD4 algorithm, only + // salted BCrypt. The hash is still shown, but a BCrypt digest is not susceptible to + // the rainbow-table/online lookups that broke the old MD4 hash. return new ResponseEntity<>( new GenericVulnerabilityResponseBean<>( - "CHALLENGE: A user's password is stored as MD4 hash: " + "CHALLENGE: A user's password used to be stored as an MD4 hash. It is" + + " now stored as a salted BCrypt hash: " + LEVEL_5_HASH - + " — Crack it and enter the original password!", + + " — which is not susceptible to rainbow tables.", false), HttpStatus.OK); } // Verify the guess - String guessHash = PasswordHashingUtils.md4Hex(password); - if (guessHash.equals(LEVEL_5_HASH)) { + if (PasswordHashingUtils.isValidBcrypt(password, LEVEL_5_HASH)) { return new ResponseEntity<>( new GenericVulnerabilityResponseBean<>( "Correct! The password was '" + password - + "'. MD4 is an insecure algorithm. Hashes can be reversed using rainbow tables and online databases.", + + "'. MD4 was an insecure algorithm reversible via rainbow tables" + + " and online databases. The vault now stores a salted BCrypt hash" + + " instead.", true), HttpStatus.OK); } else { return new ResponseEntity<>( new GenericVulnerabilityResponseBean<>( - "Incorrect. Your input hashed to: " - + guessHash - + " — Try looking up the original hash in a rainbow table!", + "Incorrect. The stored value is now a salted BCrypt hash, not an MD4" + + " hash, so rainbow tables will not help.", false), HttpStatus.OK); } @@ -285,32 +319,34 @@ public ResponseEntity> getVulnerablePay // No password param: return the challenge hash if (password == null || password.isEmpty()) { + // Fixed: the vault no longer hashes with the broken, fast MD5 algorithm, only + // salted BCrypt. The hash is still shown, but a BCrypt digest is not susceptible to + // the rainbow-table/online lookups that broke the old MD5 hash. return new ResponseEntity<>( new GenericVulnerabilityResponseBean<>( - "CHALLENGE: A user's password is stored as MD5 hash: " + "CHALLENGE: A user's password used to be stored as an MD5 hash. It is" + + " now stored as a salted BCrypt hash: " + LEVEL_6_HASH - + " — Crack it and enter the original password!", + + " — which is not susceptible to rainbow tables.", false), HttpStatus.OK); } // Verify the guess - String guessHash = PasswordHashingUtils.md5Hex(password); - if (guessHash.equals(LEVEL_6_HASH)) { + if (PasswordHashingUtils.isValidBcrypt(password, LEVEL_6_HASH)) { return new ResponseEntity<>( new GenericVulnerabilityResponseBean<>( "Correct! The password was '" + password - + "'. MD5 is insecure. Hashes can be reversed using rainbow tables and online databases." - + " using rainbow tables and online databases.", + + "'. MD5 is insecure and reversible via rainbow tables and online" + + " databases. The vault now stores a salted BCrypt hash instead.", true), HttpStatus.OK); } else { return new ResponseEntity<>( new GenericVulnerabilityResponseBean<>( - "Incorrect. Your input hashed to: " - + guessHash - + " — Try looking up the original hash in a rainbow table!", + "Incorrect. The stored value is now a salted BCrypt hash, not an MD5" + + " hash, so rainbow tables will not help.", false), HttpStatus.OK); } @@ -331,31 +367,35 @@ public ResponseEntity> getVulnerablePay String password = queryParams.get(PASSWORD_PARAM); if (password == null || password.isEmpty()) { + // Fixed: the vault no longer hashes with the deprecated SHA1 algorithm, only + // salted BCrypt. The hash is still shown, but a BCrypt digest is not susceptible to + // the collision/rainbow-table attacks that broke the old SHA1 hash. return new ResponseEntity<>( new GenericVulnerabilityResponseBean<>( - "CHALLENGE: A user's password is stored as SHA1 hash: " + "CHALLENGE: A user's password used to be stored as a SHA1 hash. It is" + + " now stored as a salted BCrypt hash: " + LEVEL_7_HASH - + " — Crack it and enter the original password!", + + " — which is not susceptible to rainbow tables.", false), HttpStatus.OK); } // Verify the guess - String guessHash = PasswordHashingUtils.sha1Hex(password); - if (guessHash.equals(LEVEL_7_HASH)) { + if (PasswordHashingUtils.isValidBcrypt(password, LEVEL_7_HASH)) { return new ResponseEntity<>( new GenericVulnerabilityResponseBean<>( "Correct! The password was '" + password - + "'. SHA1 is deprecated. it is vulnerable to collision attacks and hashes can be reversed using rainbow tables.", + + "'. SHA1 was deprecated and vulnerable to collision attacks and" + + " rainbow tables. The vault now stores a salted BCrypt hash" + + " instead.", true), HttpStatus.OK); } else { return new ResponseEntity<>( new GenericVulnerabilityResponseBean<>( - "Incorrect. Your input hashed to: " - + guessHash - + " — Try looking up the original hash in a rainbow table or use a SHA1 hash cracker!", + "Incorrect. The stored value is now a salted BCrypt hash, not a SHA1" + + " hash, so rainbow tables will not help.", false), HttpStatus.OK); } @@ -376,34 +416,37 @@ public ResponseEntity> getSecurePayload String password = queryParams.get(PASSWORD_PARAM); if (password == null || password.isEmpty()) { + // Fixed: the vault no longer stores the split-and-DES-encrypt LM hash, only + // salted BCrypt. The hash is still shown, but BCrypt is case-sensitive and not + // split into two independently-crackable halves the way the old LM hash was. return new ResponseEntity<>( new GenericVulnerabilityResponseBean<>( - "CHALLENGE: This password is hashed with LM. Hash: " + "CHALLENGE: This password used to be hashed with the legacy LM" + + " algorithm. It is now stored as a salted BCrypt hash: " + LEVEL_8_HASH - + " — Try to crack it with a LM hashing tool", + + " — which is case-sensitive and not split into two" + + " independently-crackable halves.", false), HttpStatus.OK); } // Verify the guess - String guessHash = PasswordHashingUtils.lmHash(password); - if (guessHash.equals(LEVEL_8_HASH)) { + if (PasswordHashingUtils.isValidBcrypt(password, LEVEL_8_HASH)) { return new ResponseEntity<>( new GenericVulnerabilityResponseBean<>( "Correct! The password was '" + password - + "'. LM is insecure for many reasons. Passwords are not case sensitive and max length is 14 characters. Anything shorter is NULL-padded to 14 bytes." - + " The password is split in half and a hash is calculated for each half. An attacker only needs to brute-force 7 characters twice, rather than 14 characters." - + " This makes a 14 character password only twice as strong as a 7 character one." - + " Try different capitalization to see if it makes a difference ", + + "'. LM was insecure for many reasons - it is case-insensitive and" + + " splits the password into two independently brute-forceable" + + " 7-byte halves. The vault now stores a salted BCrypt hash" + + " instead.", true), HttpStatus.OK); } else { return new ResponseEntity<>( new GenericVulnerabilityResponseBean<>( - "Incorrect. Your input hashed to: " - + guessHash - + " — Try looking up the most common passwords.", + "Incorrect. The stored value is now a salted BCrypt hash, not an LM" + + " hash, so there is no split-half shortcut.", false), HttpStatus.OK); } diff --git a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/repo/CryptographicFailuresSeeder.java b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/repo/CryptographicFailuresSeeder.java index d18824275..0f18c4dd6 100644 --- a/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/repo/CryptographicFailuresSeeder.java +++ b/src/main/java/org/sasanlabs/service/vulnerability/cryptographicFailures/repo/CryptographicFailuresSeeder.java @@ -3,7 +3,6 @@ import java.security.SecureRandom; import org.apache.commons.text.RandomStringGenerator; import org.sasanlabs.configuration.ModuleSeeder; -import org.sasanlabs.internal.utility.EncodingUtils; import org.sasanlabs.internal.utility.EncryptionUtils; import org.sasanlabs.internal.utility.PasswordHashingUtils; import org.sasanlabs.internal.utility.exception.EncryptionException; @@ -46,34 +45,47 @@ public CryptographicFailuresSeeder(CryptographicFailuresVaultRepository reposito @Transactional public void seed() throws EncryptionException { try { - // Level 1: Cleartext (Broken Cryptography) - repository.save(new VaultEntity(1, genPassword(10), "CLEARTEXT")); + // Levels 1-8 used to persist the secret using a broken/reversible scheme + // (cleartext, Base64, Caesar cipher, a "secret" custom cipher, or a fast/unsalted + // hash such as MD4/MD5/SHA-1/LM) so that anyone with read access to the vault table + // (or the hint shown by the challenge itself) could trivially recover the original + // password - CWE-326/CWE-327. Remediated the same way the app's own LEVEL_11 + // reference already recommends: only ever persist a slow, salted, one-way BCrypt + // hash of the secret. The row's stored "algorithm" label is kept for the challenge + // flavour text, but the actual persisted value is never anything but a BCrypt hash. + + // Level 1: was Cleartext (Broken Cryptography) - now BCrypt hashed at rest + repository.save( + new VaultEntity(1, PasswordHashingUtils.bCryptHash(genPassword(10)), "CLEARTEXT")); - // Level 2: Base64 Encoding (Not Encryption) + // Level 2: was Base64 Encoding (Not Encryption) - now BCrypt hashed at rest repository.save( - new VaultEntity(2, EncodingUtils.encodeBase64(genPassword(10)), "BASE64")); + new VaultEntity(2, PasswordHashingUtils.bCryptHash(genPassword(10)), "BASE64")); - // Level 3: Caesar Cipher (Weak Symmetric) + // Level 3: was Caesar Cipher (Weak Symmetric) - now BCrypt hashed at rest repository.save( new VaultEntity( - 3, EncryptionUtils.caesarCipher(genAlphaNumPassword(10), 3), "CAESAR")); + 3, PasswordHashingUtils.bCryptHash(genAlphaNumPassword(10)), "CAESAR")); - // Level 4: Custom Cipher (Security through Obscurity) + // Level 4: was Custom Cipher (Security through Obscurity) - now BCrypt hashed at rest repository.save( - new VaultEntity(4, EncryptionUtils.customCipher(genPassword(12)), "CUSTOM")); + new VaultEntity(4, PasswordHashingUtils.bCryptHash(genPassword(12)), "CUSTOM")); - // Level 5: MD4 (Broken Hash) - repository.save(new VaultEntity(5, PasswordHashingUtils.md4Hex(genPassword(5)), "MD4")); + // Level 5: was MD4 (Broken Hash) - now BCrypt hashed at rest + repository.save( + new VaultEntity(5, PasswordHashingUtils.bCryptHash(genPassword(5)), "MD4")); - // Level 6: MD5 (Broken Hash) - repository.save(new VaultEntity(6, PasswordHashingUtils.md5Hex(genPassword(5)), "MD5")); + // Level 6: was MD5 (Broken Hash) - now BCrypt hashed at rest + repository.save( + new VaultEntity(6, PasswordHashingUtils.bCryptHash(genPassword(5)), "MD5")); - // Level 7: SHA-1 (Weak Hash) + // Level 7: was SHA-1 (Weak Hash) - now BCrypt hashed at rest repository.save( - new VaultEntity(7, PasswordHashingUtils.sha1Hex(genPassword(10)), "SHA-1")); + new VaultEntity(7, PasswordHashingUtils.bCryptHash(genPassword(10)), "SHA-1")); - // Level 8: LM Hash (Legacy/Weak Windows Hash) - repository.save(new VaultEntity(8, PasswordHashingUtils.lmHash(genPassword(14)), "LM")); + // Level 8: was LM Hash (Legacy/Weak Windows Hash) - now BCrypt hashed at rest + repository.save( + new VaultEntity(8, PasswordHashingUtils.bCryptHash(genPassword(14)), "LM")); // Level 9: Unsalted SHA-256 (Fast Hash/Vulnerable to Rainbow Tables) repository.save(