Skip to content

Commit

Permalink
Add support for username case sensitivity (#24)
Browse files Browse the repository at this point in the history
* use expireAfterWrite; set secs=0 in tests

* switch back to refreshAfterWrite for cache, use 1 sec delay in tests; invalidate verify cache on credential delete; switch Salt from Base64 encoding to Hex encoding as KeyStore aliases are always lowercase (no guarantees of case sensitivity)

* username case sensitivity

add test, remove unused code, perform encoding only once in saveKey()

initial commit for hex encoding whole alias

* add loading user0 to usernameCaseSensitivityTest()

* only hex encode username, not whole alias

* remove unused import in EntryAliasString

* remove unused import in Salt
  • Loading branch information
eemhu authored Jun 6, 2024
1 parent dd7cda2 commit 44cfd8e
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 16 deletions.
34 changes: 21 additions & 13 deletions src/main/java/com/teragrep/jai_02/keystore/KeyStoreAccessImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@

import com.teragrep.jai_02.entry.EntryAliasFactory;
import com.teragrep.jai_02.entry.EntryAliasString;
import com.teragrep.jai_02.password.DecodedHex;
import com.teragrep.jai_02.password.EncodedHex;
import com.teragrep.jai_02.password.PasswordEntry;
import com.teragrep.jai_02.password.PasswordEntryFactory;
import com.teragrep.jai_02.user.UserToAliasMapping;
Expand Down Expand Up @@ -88,13 +90,14 @@ public KeyStoreAccessImpl(final KeyStore keyStore, final String keyStorePath, fi
}

public PasswordEntry loadKey(final String username) throws UnrecoverableEntryException, KeyStoreException, InvalidKeyException {
// TODO create a cache of requests -> success/fail
if (!userToAliasMapping.has(username)) {
throw new InvalidKeyException("Username <[" + username + "]> was not found in the map!");
}
// get encoded alias from map
final String alias = userToAliasMapping.get(username);

PasswordEntryFactory keyWithSecret = new PasswordEntryFactory(new EntryAliasString(alias, entryAliasFactory.split()).toEntryAlias());
PasswordEntryFactory keyWithSecret = new PasswordEntryFactory(
new EntryAliasString(alias, entryAliasFactory.split()).toEntryAlias());

final KeyStore.SecretKeyEntry ske;
try {
Expand All @@ -109,8 +112,9 @@ public PasswordEntry loadKey(final String username) throws UnrecoverableEntryExc
throw new InvalidKeyException("Could not find SecretKey in KeyStore for username <[" + username + "]>");
}

// return with human readable username
return new PasswordEntry(
keyWithSecret.asEntryAlias(),
entryAliasFactory.build(new DecodedHex(keyWithSecret.asEntryAlias().userName().toString()).decodeString(), keyWithSecret.asEntryAlias().salt()),
new SecretKeySpec(ske.getSecretKey().getEncoded(), keyWithSecret.keyAlgorithm().get().toString())
);
}
Expand All @@ -122,7 +126,8 @@ public void saveKey(final String username, final char[] password) throws KeyStor
throw new IllegalArgumentException("Alias for username <[" + username + "]> already exists in KeyStore!");
}

PasswordEntryFactory keyWithSecret = new PasswordEntryFactory(entryAliasFactory.build(username));
// Hex encoded username
PasswordEntryFactory keyWithSecret = new PasswordEntryFactory(entryAliasFactory.build(new EncodedHex(username).encode()));
try {
keyStore.setEntry(keyWithSecret.asEntryAlias().toString(), new KeyStore.SecretKeyEntry(keyWithSecret.build(password).secretKey()),
new KeyStore.PasswordProtection(keyStorePassword));
Expand All @@ -137,28 +142,31 @@ public void saveKey(final String username, final char[] password) throws KeyStor
throw new RuntimeException("I/O error storing keyStore: ", e);
}

// Put user->user:alias mapping and store keyStore in file
userToAliasMapping.put(keyWithSecret.asEntryAlias().userName().toString(), keyWithSecret.asEntryAlias().toString());
// Put user(decoded)->user:alias(encoded) mapping and store keyStore in file
userToAliasMapping.put(username, keyWithSecret.asEntryAlias().toString());
}

public boolean verifyKey(final String username, final char[] password) throws InvalidKeySpecException,
public boolean verifyKey(final String username, final char[] password) throws
UnrecoverableEntryException, KeyStoreException, InvalidKeyException {
// Get stored SecretKey and compare to newly generated key with same salt
final PasswordEntry storedKeyPair = loadKey(username);
final SecretKey newKey = new PasswordEntryFactory(entryAliasFactory.build(username, storedKeyPair.entryAlias().salt())).build(password).secretKey();
final SecretKey newKey = new PasswordEntryFactory(entryAliasFactory.build(new EncodedHex(username).encode(), storedKeyPair.entryAlias().salt())).build(password).secretKey();
return storedKeyPair.secretKey().equals(newKey);
}

public int deleteKey(final String usernameToRemove) throws KeyStoreException, IOException {
final Enumeration<String> aliases = keyStore.aliases();
final List<String> aliasesToRemove = new ArrayList<>();
while (aliases.hasMoreElements()) {
final String alias = aliases.nextElement();
final EntryAliasString entryAliasString = new EntryAliasString(alias, entryAliasFactory.split());
// original alias will be with hex encoded username
final String originalAlias = aliases.nextElement();
final EntryAliasString entryAliasString = new EntryAliasString(originalAlias, entryAliasFactory.split());

final String username = entryAliasString.toEntryAlias().userName().toString();
// decode alias to match plain text username
final String username = new DecodedHex(entryAliasString.toEntryAlias().userName().toString()).decodeString();
if (username.equals(usernameToRemove)) {
aliasesToRemove.add(alias);
// add original alias so right entry will be removed
aliasesToRemove.add(originalAlias);
}
}

Expand Down Expand Up @@ -189,7 +197,7 @@ public boolean checkForExistingAlias(final String usernameToCheck) throws KeySto
final String alias = aliases.nextElement();
final EntryAliasString entryAliasString = new EntryAliasString(alias, entryAliasFactory.split());

final String username = entryAliasString.toEntryAlias().userName().toString();
final String username = new DecodedHex(entryAliasString.toEntryAlias().userName().toString()).decodeString();
if (username.equals(usernameToCheck)) {
exists = true;
break;
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/com/teragrep/jai_02/password/DecodedHex.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
package com.teragrep.jai_02.password;

import java.math.BigInteger;
import java.nio.charset.StandardCharsets;

public class DecodedHex {
private final String hexString;
Expand All @@ -65,4 +66,8 @@ public byte[] decode() {
}
return byteArray;
}

public String decodeString() {
return new String(decode(), StandardCharsets.US_ASCII);
}
}
60 changes: 60 additions & 0 deletions src/main/java/com/teragrep/jai_02/password/EncodedHex.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Java Authentication Info jai_02
* Copyright (C) 2021 Suomen Kanuuna Oy
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://github.com/teragrep/teragrep/blob/main/LICENSE>.
*
*
* Additional permission under GNU Affero General Public License version 3
* section 7
*
* If you modify this Program, or any covered work, by linking or combining it
* with other code, such other code is not for that reason alone subject to any
* of the requirements of the GNU Affero GPL version 3 as long as this Program
* is the same Program as licensed from Suomen Kanuuna Oy without any additional
* modifications.
*
* Supplemented terms under GNU Affero General Public License version 3
* section 7
*
* Origin of the software must be attributed to Suomen Kanuuna Oy. Any modified
* versions must be marked as "Modified version of" The Program.
*
* Names of the licensors and authors may not be used for publicity purposes.
*
* No rights are granted for use of trade names, trademarks, or service marks
* which are in The Program if any.
*
* Licensee must indemnify licensors and authors for any liability that these
* contractual assumptions impose on licensors and authors.
*
* To the extent this program is licensed as part of the Commercial versions of
* Teragrep, the applicable Commercial License may apply to this file if you as
* a licensee so wish it.
*/
package com.teragrep.jai_02.password;

import java.math.BigInteger;
import java.nio.charset.StandardCharsets;

public class EncodedHex {
private final String nonEncodedString;
public EncodedHex(String nonEncodedString) {
this.nonEncodedString = nonEncodedString;
}

public String encode() {
return String.format("%040x", new BigInteger(1, nonEncodedString.getBytes(StandardCharsets.US_ASCII)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import com.teragrep.jai_02.entry.EntryAlias;
import com.teragrep.jai_02.entry.EntryAliasString;
import com.teragrep.jai_02.entry.Split;
import com.teragrep.jai_02.password.DecodedHex;

import java.security.KeyStore;
import java.security.KeyStoreException;
Expand Down Expand Up @@ -75,9 +76,10 @@ public UserToAliasMapping(KeyStore ks, Split split) {
}

while (aliases.hasMoreElements()) {
final String alias = aliases.nextElement();
final EntryAlias k = new EntryAliasString(alias, split).toEntryAlias();
this.internalMap.put(k.userName().toString(), alias);
final String originalAlias = aliases.nextElement();
final EntryAlias k = new EntryAliasString(originalAlias, split).toEntryAlias();

this.internalMap.put(new DecodedHex(k.userName().toString()).decodeString(), originalAlias);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,32 @@ public void loadNonExistingEntryTest() {
Assertions.assertEquals("Username <[" + userName + "]> was not found in the map!", ike.getMessage());
}

@Test
public void usernameCaseSensitivityTest() {
String user0 = "userNAME";
String user1 = "username";
// Make sure existing entries do not exist by deleting them
// Save user0 to KeyStore
Assertions.assertDoesNotThrow(() -> {
ksa.deleteKey(user0);
ksa.deleteKey(user1);
ksa.saveKey(user0, "password".toCharArray());
});

// Try loading user1 from KeyStore, should fail as they are in different cases
InvalidKeyException ike = Assertions.assertThrows(InvalidKeyException.class, () -> {
ksa.loadKey(user1);
});

Assertions.assertEquals("Username <[" + user1 + "]> was not found in the map!", ike.getMessage());

// Make sure that user0 can be loaded
Assertions.assertDoesNotThrow(() -> {
PasswordEntry pe = ksa.loadKey(user0);
Assertions.assertEquals(user0, pe.entryAlias().userName().toString());
});
}

@Test
public void externalModificationAddEntryTest() {
// One keyStoreAccess reads the key and one saves it
Expand Down

0 comments on commit 44cfd8e

Please sign in to comment.