Five Anti-Patterns with Secrets in Java

Most applications require some sort of secret or password to enable access: database connection info, API keys, OAuth client secrets, and JWT keys, to list some examples. Dealing with secrets in your projects is always a chore, and it’s often done wrong. In this post, I’ll describe five common problems, which you can think of as anti-patterns (the opposite of a best practice) and I’ll offer suggestions to help you avoid these issues.

If you would rather watch a video, Micah Silverman's JWTs for CSRF & Microservices (at 16:42) talk covers some of these topics too!

1. Checking secrets into source control

Checking a secret into source control is a mistake that’s easily made. This is an anti-pattern that most of us have done in error at some point, even if we don’t want to admit it. Once a secret is in your source control, it’s there forever; and can easily be found by doing a a quick history search.

If you find a secret in your source, remove it and revoke it. Of course, it’s best if secrets are never checked in to begin with! Watch out for them in code reviews and scan your code bases to ensure this doesn’t happen: GitGuardian and git-secrets.

Secrets often enter source trees by accident, usually because you temporarily typed one into a config file. When secrets are mixed in with other application properties, they become harder to spot and easier to forget about. Keep secrets out of your source entirely by using an external configuration provider like HashiCorp Vault, using environmental variables, or by keeping secrets in a separate file outside of your source tree (or one that is explicitly ignored in your .gitignore file).

Check out the spring-dotenv to use .env files with your Spring projects.

2. Conflating encoding with encryption

They are not the same! Many folks are confused about the difference between encoding and encryption; they are distinct operations and cannot be interchanged.

Encoding

Data that is encoded is transformed into another format, usually to make transferring between systems possible. An encoded value may or may not be human-readable, but it needs to be treated the same way as the original input source. The following examples show a string encoded in two common encoding schemes: URL encoding and base64 encoding.

URL encoded:
String text = "Can you still read this text?";
String encoded = URLEncoder.encode(text, UTF_8);
// encoded: Can+you+still+read+this+text%3F
String decoded = URLDecoder.decode(encoded, UTF_8);
assertThat(decoded, equalTo(decoded));
Base64 encoded (not human-readable):
String text = "Can you still read this text?";
String encoded = Base64.getEncoder().encodeToString(text.getBytes(UTF_8));
// encoded: Q2FuIHlvdSBzdGlsbCByZWFkIHRoaXMgdGV4dD8=
String decoded = new String(Base64.getDecoder().decode(encoded.getBytes(UTF_8)));
assertThat(decoded, equalTo(decoded));
In both cases, the encoded data can easily be reversed, and the encoded string must be treated the same way as plain text.

Encryption

In contrast, data that’s encrypted can only be read by authorized parties (anyone with the correct cryptographic key). When data is encrypted, the resulting value is indistinguishable from random data. Because of this, encrypted data is often also encoded to make it easier to transport. For example, in the case of JSON Web Encryption (JWE), the JSON payload is encrypted and is then transformed to a base64-encoded string.

Encryption is a complex topic and not suitable for a quick example. If you want to see a follow-up post covering common encryption use cases, leave a comment below!

If you are using Kubernetes, make sure your secrets are encrypted. By default, Kubernetes Secrets store data as base64-encoded strings, as shown above. This isn’t any better than plain text. The good news is there are a few alternatives: use HashiCorp’s Vault, AWS KMS, or Sealed Secrets.

3. Using the bytes of a string for your key

Many of us confuse the terms password and secret key. Generally speaking, passwords make poor cryptographic keys. People often choose memorable words for passwords, making them easy to guess. Even good passwords that are long and randomly generated have less entropy (the measure of the randomness) than a byte array of the same size.

To illustrate this point, think of a password of eight characters. (To keep the math simple, limit the characters to displayable ASCII.) There are 95 displayable ASCII characters, which means an eight-character password has a total of 958 = 6.6×1015 possible combinations. A random array of bytes doesn’t have this displayable character limit; each byte in the array has 256 possibilities, so a byte array of the same size has 2568 = 1.8×1019 total possible combinations!

Instead of getting the bytes directly from a String:

String secret = "password";
byte[] secretBytes = secret.getBytes(UTF_8);

Use random bytes that are base64 encoded. The example below was generated by running: openssl rand -base64 8

String secret = "JJsm0MaRPHI="; // 8 random bytes base64-encoded
byte[] secretBytes = Base64.getDecoder().decode(secret.getBytes(UTF_8));
Did you notice that I hardcoded secrets into these examples?
Picard facepalm from www.reactiongifs.com/picard-facepalm/

4. Failing to restrict access to secrets

It’s hard to keep secrets, well…​ secret. Do you know who has access to each secret used by your application? Secrets kept in environment variables can be viewed by anyone who has access to the running process on the system ps e -ww -p <pid>, or access to the Docker daemon docker inspect <container>. Even "hidden" secrets used by your build server are easy to leak. Most popular CI services will filter build logs of passwords, but it’s easy to work around filters. For example, this GitHub Actions script adds a space between each character to bypass the filtering.

steps:
- name: Not So Hidden Secret
  run: "echo ${{secrets.TEST_SECRET}} | sed 's/./& /g'"

# output: C a n   y o u   s t i l l   r e a d   t h i s   t e x t ?
I’m not suggesting you do this; doing so may get you fired. Just be aware that it can happen and treat your secrets accordingly.

5. Building your own crypto

As the old saying goes, "Don’t roll your own crypto." You are probably aware that you shouldn’t implement crypto algorithms yourself, but there’s much more to it. Knowing how the output will be used and understanding the protocol involved are just as important as the actual algorithm choice. Different algorithms have been developed for different use cases; many protocols or formats are designed with these specific use cases in mind.

Instead of cobbling together lower-level primitives, use a high-level library. This suggestion may be obvious for protocols like TLS/HTTPS, but it also applies to simple formats; a lot can go wrong when a JWT is parsed without a quality library.

Bonus: Be ready to rotate your secrets

If history has taught us anything about secrets, it’s that they will leak. In the last year there have been a few notable cases of secrets exposed: Travis-CI and Codecov. After these issues were disclosed, teams scrambled to update secrets that may have been affected, which often resulted in broken builds.

When people leave your project, any secrets they had access to must be updated.

Be aware of how your project uses secrets, and practice rotating them before the next leak or personnel change.

Learn more about application security

In this post, I’ve described a few common anti-patterns to avoid when managing secrets for Java applications. But this list of five barely scratches the surface of application security and secret management. Check out these other posts to learn more:

If you have questions, please leave a comment below. If you liked this tutorial, follow @oktadev on Twitter, follow us on LinkedIn, or subscribe to our YouTube channel.

Brian Demers is a Developer Advocate at Okta and a PMC member for the Apache Shiro project. He spends much of his day contributing to OSS projects in the form of writing code, tutorials, blogs, and answering questions. Along with typical software development, Brian also has a passion for fast builds and automation. Away from the keyboard, Brian is a beekeeper and can likely be found playing board games. You can find him on Twitter at @briandemers.

Okta Developer Blog Comment Policy

We welcome relevant and respectful comments. Off-topic comments may be removed.