-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allow use of the token returned with spam challenges as auth for the …
…challenge verification request
- Loading branch information
1 parent
ef1a8fc
commit 098b177
Showing
9 changed files
with
363 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
12 changes: 12 additions & 0 deletions
12
.../src/main/java/org/whispersystems/textsecuregcm/configuration/ChallengeConfiguration.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
/* | ||
* Copyright 2021-2022 Signal Messenger, LLC | ||
* SPDX-License-Identifier: AGPL-3.0-only | ||
*/ | ||
|
||
package org.whispersystems.textsecuregcm.configuration; | ||
|
||
import java.time.Duration; | ||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes; | ||
|
||
public record ChallengeConfiguration(SecretBytes blindingSecret, Duration tokenTtl) { | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
109 changes: 109 additions & 0 deletions
109
service/src/main/java/org/whispersystems/textsecuregcm/util/ChallengeTokenBlinder.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
/* | ||
* Copyright 2023 Signal Messenger, LLC | ||
* SPDX-License-Identifier: AGPL-3.0-only | ||
*/ | ||
|
||
package org.whispersystems.textsecuregcm.util; | ||
|
||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.fasterxml.jackson.core.JsonProcessingException; | ||
import java.io.IOException; | ||
import java.nio.ByteBuffer; | ||
import java.security.GeneralSecurityException; | ||
import java.security.ProviderException; | ||
import java.security.SecureRandom; | ||
import java.time.Clock; | ||
import java.time.Duration; | ||
import java.time.Instant; | ||
import java.util.Base64; | ||
import java.util.Optional; | ||
import java.util.UUID; | ||
import javax.crypto.AEADBadTagException; | ||
import javax.crypto.Cipher; | ||
import javax.crypto.SecretKey; | ||
import javax.crypto.spec.GCMParameterSpec; | ||
import javax.crypto.spec.SecretKeySpec; | ||
import org.whispersystems.textsecuregcm.configuration.ChallengeConfiguration; | ||
|
||
public class ChallengeTokenBlinder { | ||
|
||
private record Token( | ||
UUID uuid, | ||
Instant timestamp) { | ||
} | ||
|
||
private static final ObjectMapper mapper = SystemMapper.jsonMapper(); | ||
private final Clock clock; | ||
private final Duration tokenTtl; | ||
private final SecureRandom secureRandom = new SecureRandom(); | ||
private final SecretKey blindingKey; | ||
|
||
public ChallengeTokenBlinder(final ChallengeConfiguration config, final Clock clock) { | ||
this.blindingKey = new SecretKeySpec(config.blindingSecret().value(), "AES"); | ||
this.tokenTtl = config.tokenTtl(); | ||
this.clock = clock; | ||
} | ||
|
||
public String generateBlindedAccountToken(UUID aci) { | ||
|
||
final Token token = new Token(aci, clock.instant()); | ||
final byte[] serializedToken; | ||
try { | ||
serializedToken = mapper.writeValueAsBytes(token); | ||
} catch (IOException e) { // should really, really never happen | ||
throw new IllegalArgumentException(); | ||
} | ||
|
||
final byte[] iv = new byte[12]; | ||
secureRandom.nextBytes(iv); | ||
final GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv); | ||
|
||
try { | ||
final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); | ||
cipher.init(Cipher.ENCRYPT_MODE, blindingKey, parameterSpec); | ||
final byte[] ciphertext = cipher.doFinal(serializedToken); | ||
|
||
final ByteBuffer byteBuffer = ByteBuffer.allocate(iv.length + ciphertext.length); | ||
byteBuffer.put(iv); | ||
byteBuffer.put(ciphertext); | ||
return Base64.getUrlEncoder().withoutPadding().encodeToString(byteBuffer.array()); | ||
} catch (GeneralSecurityException e) { | ||
throw new IllegalArgumentException(e); | ||
} | ||
} | ||
|
||
public Optional<UUID> unblindAccountToken(String token) { | ||
final byte[] ciphertext; | ||
try { | ||
ciphertext = Base64.getUrlDecoder().decode(token); | ||
} catch (IllegalArgumentException e) { | ||
return Optional.empty(); | ||
} | ||
|
||
final Token parsedToken; | ||
|
||
try { | ||
final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); | ||
final GCMParameterSpec parameterSpec = new GCMParameterSpec(128, ciphertext, 0, 12); | ||
cipher.init(Cipher.DECRYPT_MODE, blindingKey, parameterSpec); | ||
|
||
parsedToken = mapper.readValue(cipher.doFinal(ciphertext, 12, ciphertext.length - 12), Token.class); | ||
} catch (ProviderException | AEADBadTagException | JsonProcessingException e) { | ||
// the token doesn't successfully decrypt with this key, it's bogus (or from an older server version or before a key rotation) | ||
return Optional.empty(); | ||
} catch (IOException | GeneralSecurityException e) { // should never happen | ||
throw new IllegalArgumentException(); | ||
} | ||
|
||
Instant now = clock.instant(); | ||
Instant intervalStart = now.minus(tokenTtl); | ||
Instant tokenTime = parsedToken.timestamp(); | ||
if (tokenTime.isAfter(now) || tokenTime.isBefore(intervalStart)) { | ||
// expired or fraudulently-future token | ||
return Optional.empty(); | ||
} | ||
|
||
return Optional.of(parsedToken.uuid()); | ||
} | ||
|
||
} |
Oops, something went wrong.