11package cwms .cda .data .dao ;
22
33import com .google .common .flogger .FluentLogger ;
4+ import com .password4j .Hash ;
5+ import com .password4j .HashUpdate ;
46import cwms .cda .ApiServlet ;
57import cwms .cda .data .dto .auth .ApiKey ;
68import cwms .cda .datasource .ConnectionPreparer ;
1012import cwms .cda .datasource .SessionOfficePreparer ;
1113import cwms .cda .datasource .SessionTimeZonePreparer ;
1214import cwms .cda .helpers .ResourceHelper ;
15+ import cwms .cda .features .CdaFeatures ;
1316import cwms .cda .security .CwmsAuthException ;
1417import cwms .cda .security .DataApiPrincipal ;
1518import cwms .cda .security .MissingRolesException ;
1619import cwms .cda .security .Role ;
1720import io .javalin .core .security .RouteRole ;
1821import io .javalin .http .Context ;
1922import io .javalin .http .HttpCode ;
23+ import org .togglz .core .context .FeatureContext ;
2024
25+ import com .password4j .Password ;
2126import java .security .NoSuchAlgorithmException ;
2227import java .security .SecureRandom ;
2328import java .sql .Connection ;
@@ -56,6 +61,8 @@ public class AuthDao extends Dao<DataApiPrincipal> {
5661 + "23.03.16 or later to handle authorization operations." ;
5762 public static final String DATA_API_PRINCIPAL = "DataApiPrincipal" ;
5863 public static final String AUTH_ERROR_MSG = "Authentication failed. The API Key may be invalid or no longer active." ;
64+ private static final String API_KEY_V1_PREFIX = "ak1_" ;
65+ private static final int API_KEY_ID_LENGTH = 12 ;
5966 // At this level we just care that the user has permissions in *any* office
6067 private static final String RETRIEVE_GROUPS_OF_USER =
6168 ResourceHelper .getResourceAsString ("/cwms/data/sql/user_groups.sql" , AuthDao .class );
@@ -68,7 +75,8 @@ public class AuthDao extends Dao<DataApiPrincipal> {
6875 + "cwms_env.set_session_user_direct(upper(?),upper(?)); end;" ;
6976
7077 private static final String CHECK_API_KEY =
71- "select userid from cwms_20.at_api_keys where apikey = ? and (expires is null or expires >= systimestamp)" ;
78+ "select userid, apikey, key_name, expires from cwms_20.at_api_keys where (expires is null or expires >= systimestamp) " +
79+ "and apikey like ?" ;
7280
7381 private static final String USER_FOR_EDIPI =
7482 "select userid from cwms_20.at_sec_cwms_users where edipi = ?" ;
@@ -194,30 +202,82 @@ static void setSessionForAuthCheck(Connection conn) throws SQLException {
194202 }
195203 }
196204
205+ private static String hashApiKey (String apiKey ) {
206+ return Password .hash (apiKey )
207+ .withArgon2 ()
208+ .getResult ();
209+ }
210+
197211 private String checkKey (String key ) throws CwmsAuthException {
198212 try {
199213 return dsl .connectionResult (c -> {
200214 setSessionForAuthCheck (c );
201- try (PreparedStatement checkForKey = c .prepareStatement (CHECK_API_KEY )) {
202- checkForKey .setString (1 ,key );
203- try (ResultSet rs = checkForKey .executeQuery ()) {
204- if (rs .next ()) {
205- return rs .getString (1 );
206- } else {
207- throw new CwmsAuthException (AUTH_ERROR_MSG );
215+
216+ boolean legacySupport = FeatureContext .getFeatureManager ()
217+ .isActive (CdaFeatures .AUTH_RE_ENABLE_NON_HASH_KEY_SUPPORT );
218+
219+ if (key .startsWith (API_KEY_V1_PREFIX ) && key .length () > API_KEY_ID_LENGTH ) {
220+ try (PreparedStatement checkForKey = c .prepareStatement (CHECK_API_KEY )) {
221+ String keyId = key .substring (0 , API_KEY_ID_LENGTH );
222+ checkForKey .setString (1 , keyId + "%" );
223+ try (ResultSet rs = checkForKey .executeQuery ()) {
224+ if (rs .next ()) {
225+ String secretKey = key .substring (API_KEY_ID_LENGTH );
226+ String persistentHash = rs .getString (2 ).substring (API_KEY_ID_LENGTH );
227+ HashUpdate hashUpdate = checkKey (secretKey , persistentHash );
228+ if (hashUpdate .isVerified ()) {
229+ String userId = rs .getString (1 );
230+ if (hashUpdate .isUpdated ()) {
231+ Hash newHash = hashUpdate .getHash ();
232+ String newHashedApiKey = keyId + newHash .getResult ();
233+ String keyName = rs .getString (3 );
234+ Date expires = rs .getDate (4 );
235+ updateHash (c , userId , keyName , expires , newHashedApiKey );
236+ }
237+ return userId ;
238+ }
239+ }
240+ }
241+ }
242+ } else if (legacySupport ) {
243+ try (PreparedStatement checkForKey = c .prepareStatement (CHECK_API_KEY )) {
244+ checkForKey .setString (1 , key );
245+ try (ResultSet rs = checkForKey .executeQuery ()) {
246+ if (rs .next ()) {
247+ return rs .getString (1 );
248+ }
208249 }
209250 }
210- } catch (SQLException ex ) {
211- throw new CwmsAuthException ("Failed API key check" ,ex );
212251 }
252+ throw new CwmsAuthException (AUTH_ERROR_MSG );
213253 });
214- } catch (DataAccessException ex ) {
215- Throwable t = ex .getCause ();
216- if (t instanceof CwmsAuthException ) {
217- throw (CwmsAuthException )t ;
218- } else {
219- throw ex ;
220- }
254+ } catch (RuntimeException ex ) {
255+ // Don't expose internal database errors
256+ logger .atWarning ().withCause (ex ).log ("Error verifying API key." );
257+ throw new CwmsAuthException (AUTH_ERROR_MSG );
258+ }
259+ }
260+
261+ private static HashUpdate checkKey (String keyFromClient , String persistentHash ) {
262+ return Password .check (keyFromClient , persistentHash )
263+ .andUpdate ()
264+ .withArgon2 ();
265+ }
266+
267+ private void updateHash (Connection c , String userId , String keyName , Date expires , String apiKey )
268+ throws SQLException {
269+ //Schema does not allow for row updates. Delete + recreate
270+ try (PreparedStatement deleteKey = c .prepareStatement (REMOVE_API_KEY )) {
271+ deleteKey .setString (1 , userId );
272+ deleteKey .setString (2 , keyName );
273+ deleteKey .execute ();
274+ }
275+ try (PreparedStatement createKey = c .prepareStatement (CREATE_API_KEY )) {
276+ createKey .setString (1 , userId );
277+ createKey .setString (2 , keyName );
278+ createKey .setString (3 , apiKey );
279+ createKey .setDate (4 , expires );
280+ createKey .execute ();
221281 }
222282 }
223283
@@ -412,15 +472,13 @@ public ApiKey createApiKey(DataApiPrincipal p, ApiKey sourceData) throws CwmsAut
412472 throw new CwmsAuthException (ONLY_OWN_KEY_MESSAGE , HttpCode .UNAUTHORIZED .getStatus ());
413473 }
414474 SecureRandom randomSource = SecureRandom .getInstanceStrong ();
415- String key = randomSource .ints ((char )'0' ,(char )'z' ) // allow a-zA-Z0-9
416- .filter (i -> (i <= 57 || i >= 65 ) && (i <= 90 || i >= 97 )) // actually filter to above
417- .limit (256 )
418- .collect (StringBuilder ::new ,StringBuilder ::appendCodePoint , StringBuilder ::append )
419- .toString ();
475+ String secretKey = generateSecretKey (randomSource );
476+ String keyId = generateKeyId (randomSource );
477+ String fullApiKey = keyId + secretKey ;
420478 final ApiKey newKey = new ApiKey (
421479 sourceData .getUserId ().toUpperCase (),
422480 sourceData .getKeyName (),
423- key ,
481+ fullApiKey ,
424482 ZonedDateTime .now (ZoneId .of ("UTC" )),
425483 sourceData .getExpires ()
426484 );
@@ -429,7 +487,7 @@ public ApiKey createApiKey(DataApiPrincipal p, ApiKey sourceData) throws CwmsAut
429487 try (PreparedStatement createKey = c .prepareStatement (CREATE_API_KEY )) {
430488 createKey .setString (1 , newKey .getUserId ());
431489 createKey .setString (2 , newKey .getKeyName ());
432- createKey .setString (3 , newKey . getApiKey ( ));
490+ createKey .setString (3 , keyId + hashApiKey ( secretKey ));
433491 createKey .setDate (4 , new Date (newKey .getCreated ().toInstant ().toEpochMilli ()),
434492 Calendar .getInstance (TimeZone .getTimeZone ("UTC" )));
435493 if (newKey .getExpires () != null ) {
@@ -453,6 +511,22 @@ public ApiKey createApiKey(DataApiPrincipal p, ApiKey sourceData) throws CwmsAut
453511
454512 }
455513
514+ private static String generateSecretKey (SecureRandom randomSource ) {
515+ return randomSource .ints ('0' , 'z' ) // allow a-zA-Z0-9
516+ .filter (i -> (i <= 57 || i >= 65 ) && (i <= 90 || i >= 97 )) // actually filter to above
517+ .limit (256 )
518+ .collect (StringBuilder ::new , StringBuilder ::appendCodePoint , StringBuilder ::append )
519+ .toString ();
520+ }
521+
522+ private static String generateKeyId (SecureRandom randomSource ) {
523+ return API_KEY_V1_PREFIX + randomSource .ints ('0' , 'z' ) // allow a-zA-Z0-9
524+ .filter (i -> (i <= 57 || i >= 65 ) && (i <= 90 || i >= 97 )) // actually filter to above
525+ .limit (API_KEY_ID_LENGTH - API_KEY_V1_PREFIX .length ())
526+ .collect (StringBuilder ::new , StringBuilder ::appendCodePoint , StringBuilder ::append )
527+ .toString ();
528+ }
529+
456530 /**
457531 * Return all API Keys for a given user.
458532 * @param p User for which we want the keys
0 commit comments