| // Copyright (C) 2020 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.gerrit.server.mail; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.gerrit.testing.GerritJUnit.assertThrows; |
| |
| import java.util.Random; |
| import java.util.regex.Pattern; |
| import org.junit.Before; |
| import org.junit.Test; |
| |
| public class SignedTokenTest { |
| |
| private static final Pattern URL_UNSAFE_CHARS = Pattern.compile("(\\+|/)"); |
| private static final String REGISTER_EMAIL_PRIVATE_KEY = "TGMv3/bTC42jUKQndTQrXyHhHYMP0t69i/4="; |
| private static final int maxAge = 5; |
| private static final String TEXT = "This is a text"; |
| private static final String FORGED_TEXT = "This is a forged text"; |
| private static final String FORGED_TOKEN = String.format("Zm9yZ2VkJTIwa2V5$%s", TEXT); |
| |
| private SignedToken signedToken; |
| |
| @Before |
| public void setUp() throws Exception { |
| signedToken = new SignedToken(maxAge, REGISTER_EMAIL_PRIVATE_KEY); |
| } |
| |
| /** Test new token: the key is a normal BASE64 string that can be used for URL safely */ |
| @Test |
| public void newTokenKeyDoesNotContainUnsafeChar() throws Exception { |
| assertThat(signedToken.newToken(TEXT)).doesNotContainMatch(URL_UNSAFE_CHARS); |
| } |
| |
| /** Test new token: the key is an URL unsafe BASE64 string with index of '62'(+) */ |
| @Test |
| public void newTokenWithUrlUnsafeBase64Plus() throws Exception { |
| String token = "+" + signedToken.newToken(TEXT); |
| CheckTokenException thrown = |
| assertThrows(CheckTokenException.class, () -> signedToken.checkToken(token, TEXT)); |
| |
| assertThat(thrown).hasMessageThat().contains("decoding failed"); |
| |
| assertThat(thrown) |
| .hasCauseThat() |
| .hasMessageThat() |
| .isEqualTo( |
| "com.google.common.io.BaseEncoding$DecodingException: Unrecognized character: +"); |
| } |
| |
| /** Test new token: the key is an URL unsafe BASE64 string with '63'(/) */ |
| @Test |
| public void newTokenWithUrlUnsafeBase64Slash() throws Exception { |
| String token = "/" + signedToken.newToken(TEXT); |
| CheckTokenException thrown = |
| assertThrows(CheckTokenException.class, () -> signedToken.checkToken(token, TEXT)); |
| |
| assertThat(thrown).hasMessageThat().contains("decoding failed"); |
| |
| assertThat(thrown) |
| .hasCauseThat() |
| .hasMessageThat() |
| .isEqualTo( |
| "com.google.common.io.BaseEncoding$DecodingException: Unrecognized character: /"); |
| } |
| |
| /** Test check token: BASE64 encoding and decoding in a safe URL way */ |
| @Test |
| public void checkToken() throws Exception { |
| String token = signedToken.newToken(TEXT); |
| ValidToken validToken = signedToken.checkToken(token, TEXT); |
| assertThat(validToken).isNotNull(); |
| assertThat(validToken.getData()).isEqualTo(TEXT); |
| } |
| |
| /** Test check token: input token string is null */ |
| @Test |
| public void checkTokenInputTokenNull() throws Exception { |
| CheckTokenException thrown = |
| assertThrows(CheckTokenException.class, () -> signedToken.checkToken(null, TEXT)); |
| |
| assertThat(thrown).hasMessageThat().isEqualTo("Empty token"); |
| } |
| |
| /** Test check token: input token string is empty */ |
| @Test |
| public void checkTokenInputTokenEmpty() throws Exception { |
| CheckTokenException thrown = |
| assertThrows(CheckTokenException.class, () -> signedToken.checkToken("", TEXT)); |
| |
| assertThat(thrown).hasMessageThat().isEqualTo("Empty token"); |
| } |
| |
| /** Test check token: token string is not illegal with no '$' character */ |
| @Test |
| public void checkTokenInputTokenNoDollarSplitChar() throws Exception { |
| String token = signedToken.newToken(TEXT).replace("$", "¥"); |
| CheckTokenException thrown = |
| assertThrows(CheckTokenException.class, () -> signedToken.checkToken(token, TEXT)); |
| |
| assertThat(thrown).hasMessageThat().isEqualTo("Token does not contain character '$'"); |
| } |
| |
| /** Test check token: token string length is match but is not a legal BASE64 string */ |
| @Test |
| public void checkTokenInputTokenKeyBase64DecodeFail() throws Exception { |
| String token = signedToken.newToken(TEXT); |
| String key = randomString(token.indexOf("$") + 1); |
| String illegalBase64Token = key + "$" + TEXT; |
| CheckTokenException thrown = |
| assertThrows( |
| CheckTokenException.class, () -> signedToken.checkToken(illegalBase64Token, TEXT)); |
| |
| assertThat(thrown).hasMessageThat().isEqualTo("Base64 decoding failed"); |
| } |
| |
| /** Test check token: token is illegal with a forged key */ |
| @Test |
| public void checkTokenForgedKey() throws Exception { |
| CheckTokenException thrown = |
| assertThrows(CheckTokenException.class, () -> signedToken.checkToken(FORGED_TOKEN, TEXT)); |
| |
| assertThat(thrown).hasMessageThat().isEqualTo("Token length mismatch"); |
| } |
| |
| /** Test check token: token is illegal with a forged text */ |
| @Test |
| public void checkTokenForgedText() throws Exception { |
| CheckTokenException thrown = |
| assertThrows( |
| CheckTokenException.class, |
| () -> { |
| String token = signedToken.newToken(TEXT); |
| signedToken.checkToken(token, FORGED_TEXT); |
| }); |
| |
| assertThat(thrown).hasMessageThat().isEqualTo("Token text mismatch"); |
| } |
| |
| private static String randomString(int length) { |
| String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; |
| Random random = new Random(); |
| StringBuffer sb = new StringBuffer(); |
| for (int i = 0; i < length; i++) { |
| int number = random.nextInt(62); |
| sb.append(str.charAt(number)); |
| } |
| return sb.toString(); |
| } |
| } |