Skip to content

Commit

Permalink
Include standalone HKDF implementation for v1.local tokens (#6)
Browse files Browse the repository at this point in the history
* Add standalone Java v1 local tokens implementation

* Add BaseV1LocalCryptoProvider

Now that we have more than one option for V1 Local tokens HKDF and BC, a `BaseV1LocalCryptoProvider` has been added.
This reduces what is needed to implement a V1LocalCryptoProvider to a single method.

Co-authored-by: Brian Demers <[email protected]>
  • Loading branch information
zbiljic and bdemers authored Apr 27, 2020
1 parent ec5f808 commit d028cd6
Show file tree
Hide file tree
Showing 17 changed files with 354 additions and 100 deletions.
1 change: 1 addition & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ This project includes:
ASM Tree under BSD
ASM Util under BSD
Gson under Apache 2.0
HKDF-RFC5869 under Apache License, Version 2.0
jffi under The Apache Software License, Version 2.0
jnr-a64asm under The Apache Software License, Version 2.0
jnr-ffi under The Apache Software License, Version 2.0
Expand Down
32 changes: 21 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,26 +202,34 @@ If you're building a (non-Android) JDK project, you will want to define the foll
<dependency>
<groupId>dev.paseto</groupId>
<artifactId>jpaseto-api</artifactId>
<version>0.1.0</version>
<version>0.5.0</version>
</dependency>
<dependency>
<groupId>dev.paseto</groupId>
<artifactId>jpaseto-impl</artifactId>
<version>0.1.0</version>
<version>0.5.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>dev.paseto</groupId>
<artifactId>jpaseto-jackson</artifactId>
<version>0.1.0</version>
<version>0.5.0</version>
<scope>runtime</scope>
</dependency>
<!-- Uncomment the next lines if you want to use v1.local tokens -->
<!--
<dependency>
<groupId>dev.paseto</groupId>
<artifactId>jpaseto-bouncy-castle</artifactId>
<version>0.1.0</version>
<version>0.5.0</version>
<scope>runtime</scope>
</dependency> -->
<!-- or this (only 'v1.local' tokens) for smaller dependency (~11 KB for HKDF vs. ~4.3 MB for Bouncy Castle) -->
<!--
<dependency>
<groupId>dev.paseto</groupId>
<artifactId>jpaseto-hkdf</artifactId>
<version>0.5.0</version>
<scope>runtime</scope>
</dependency> -->
<!-- Uncomment the next lines if you want to use v2 tokens -->
Expand All @@ -230,7 +238,7 @@ If you're building a (non-Android) JDK project, you will want to define the foll
<dependency>
<groupId>dev.paseto</groupId>
<artifactId>jpaseto-sodium</artifactId>
<version>0.1.0</version>
<version>0.5.0</version>
<scope>runtime</scope>
</dependency> -->
```
Expand All @@ -240,22 +248,24 @@ If you're building a (non-Android) JDK project, you will want to define the foll

```groovy
dependencies {
compile 'dev.paseto:jpaseto-api:0.1.0'
runtime 'dev.paseto:jpaseto-impl:0.1.0',
compile 'dev.paseto:jpaseto-api:0.5.0'
runtime 'dev.paseto:jpaseto-impl:0.5.0',
// Uncomment the next lines if you want to use v1.local tokens
// 'dev.paseto:jpaseto-bouncy-castle:0.1.0',
// 'dev.paseto:jpaseto-bouncy-castle:0.5.0',
// or this (only 'v1.local' tokens) for smaller dependency (~11 KB for HKDF vs. ~4.3 MB for Bouncy Castle)
// 'dev.paseto:jpaseto-hkdf:0.5.0',
// Uncomment the next lines if you want to use v2 tokens
// NOTE: this requires the native lib sodium library installed on your system see below
// 'dev.paseto:jpaseto-sodium:0.1.0',
'dev.paseto:jpaseto-jackson:0.1.0'
// 'dev.paseto:jpaseto-sodium:0.5.0',
'dev.paseto:jpaseto-jackson:0.5.0'
}
```
<a name="install-sodium"></a>
#### libsodium

Installation the a native library [libsodium](https://github.com/jedisct1/libsodium) is required when creating or parseing "v2.local" tokens.

**NOTE:** `public` tokens can be used with the `jpaseto-bouncy-castle` dependency or Java 11+. `v1.local` tokens require `jpaseto-bouncy-castle`.
**NOTE:** `public` tokens can be used with the `jpaseto-bouncy-castle` dependency or Java 11+. `v1.local` tokens require `jpaseto-bouncy-castle` or `jpaseto-hkdf`.

- MacOS - Can install libsodium using brew:

Expand Down
2 changes: 1 addition & 1 deletion api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<parent>
<groupId>dev.paseto</groupId>
<artifactId>jpaseto-root</artifactId>
<version>0.4.1-SNAPSHOT</version>
<version>0.5.0-SNAPSHOT</version>
</parent>

<artifactId>jpaseto-api</artifactId>
Expand Down
6 changes: 5 additions & 1 deletion coverage/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<parent>
<groupId>dev.paseto</groupId>
<artifactId>jpaseto-root</artifactId>
<version>0.4.1-SNAPSHOT</version>
<version>0.5.0-SNAPSHOT</version>
</parent>

<artifactId>jpaseto-coverage</artifactId>
Expand Down Expand Up @@ -49,6 +49,10 @@
<groupId>dev.paseto</groupId>
<artifactId>jpaseto-bouncy-castle</artifactId>
</dependency>
<dependency>
<groupId>dev.paseto</groupId>
<artifactId>jpaseto-hkdf</artifactId>
</dependency>
<dependency>
<groupId>dev.paseto</groupId>
<artifactId>jpaseto-its-sodium-jackson</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion extensions/crypto/bouncy-castle/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<parent>
<groupId>dev.paseto</groupId>
<artifactId>jpaseto-root</artifactId>
<version>0.4.1-SNAPSHOT</version>
<version>0.5.0-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,93 +16,23 @@
package dev.paseto.jpaseto.crypto.bouncycastle;

import com.google.auto.service.AutoService;
import dev.paseto.jpaseto.InvalidMacException;
import dev.paseto.jpaseto.impl.crypto.Hmacs;
import dev.paseto.jpaseto.impl.crypto.PreAuthEncoder;
import dev.paseto.jpaseto.impl.crypto.BaseV1LocalCryptoProvider;
import dev.paseto.jpaseto.impl.crypto.V1LocalCryptoProvider;
import dev.paseto.jpaseto.impl.lang.Bytes;
import org.bouncycastle.crypto.generators.HKDFBytesGenerator;
import org.bouncycastle.crypto.params.HKDFParameters;
import org.bouncycastle.crypto.util.DigestFactory;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import java.security.MessageDigest;
import java.util.Arrays;

import static java.nio.charset.StandardCharsets.UTF_8;

@AutoService(V1LocalCryptoProvider.class)
public class BouncyCastleV1LocalCryptoProvider implements V1LocalCryptoProvider {

private static final byte[] HEADER_BYTES = "v1.local.".getBytes(UTF_8);
public class BouncyCastleV1LocalCryptoProvider extends BaseV1LocalCryptoProvider {

@Override
public byte[] encrypt(byte[] payload, byte[] footer, byte[] nonce, SecretKey sharedSecret) {

byte[] salt = Arrays.copyOf(nonce, 16);
byte[] rightNonce = Arrays.copyOfRange(nonce, 16, nonce.length);

// 4
byte[] encryptionKey = encryptionKey(sharedSecret, salt);
byte[] authenticationKey = authenticationKey(sharedSecret, salt);

// 5
byte[] cipherText = V1LocalCryptoProvider.doCipher(Cipher.ENCRYPT_MODE, encryptionKey, rightNonce, payload);

//6
byte[] preAuth = PreAuthEncoder.encode(HEADER_BYTES, nonce, cipherText, footer);

//7
byte[] calculatedMac = Hmacs.hmacSha384(authenticationKey, preAuth);

// 8
return Bytes.concat(nonce, cipherText, calculatedMac);
}

@Override
public byte[] decrypt(byte[] encryptedBytes, byte[] footer, byte[] nonce, SecretKey sharedSecret) {

// 3
byte[] salt = Arrays.copyOf(nonce, 16);
byte[] rightNonce = Arrays.copyOfRange(nonce, 16, nonce.length);

byte[] cipherText = Arrays.copyOfRange(encryptedBytes, 32, encryptedBytes.length - 48);
byte[] mac = Arrays.copyOfRange(encryptedBytes, encryptedBytes.length - 48, encryptedBytes.length);

// 4
byte[] encryptionKey = encryptionKey(sharedSecret, salt);
byte[] authenticationKey = authenticationKey(sharedSecret, salt);

// 5
byte[] preAuth = PreAuthEncoder.encode(HEADER_BYTES, nonce, cipherText, footer);

// 6
byte[] calculatedMac = Hmacs.hmacSha384(authenticationKey, preAuth);

// 7
if (!MessageDigest.isEqual(calculatedMac, mac)) {
throw new InvalidMacException("Failed to validate mac in token");
}

// 8
return V1LocalCryptoProvider.doCipher(Cipher.DECRYPT_MODE, encryptionKey, rightNonce, cipherText);
}

private byte[] hkdfSha384(SecretKey sharedSecret, byte[] salt, String info) {

protected byte[] hkdfSha384(SecretKey sharedSecret, byte[] salt, byte[] info) {
HKDFBytesGenerator hkdfBytesGenerator = new HKDFBytesGenerator(DigestFactory.createSHA384());
hkdfBytesGenerator.init(new HKDFParameters(sharedSecret.getEncoded(), salt, info.getBytes(UTF_8)));
hkdfBytesGenerator.init(new HKDFParameters(sharedSecret.getEncoded(), salt, info));
byte[] result = new byte[32];
hkdfBytesGenerator.generateBytes(result, 0, result.length);
return result;
}

private byte[] encryptionKey(SecretKey sharedSecret, byte[] salt) {
return hkdfSha384(sharedSecret, salt, "paseto-encryption-key");
}

private byte[] authenticationKey(SecretKey sharedSecret, byte[] salt) {
return hkdfSha384(sharedSecret, salt, "paseto-auth-key-for-aead");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2020-Present paseto.dev
*
* 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 dev.paseto.jpaseto.crypto.bouncycastle

import dev.paseto.jpaseto.impl.crypto.V1LocalCryptoProvider
import dev.paseto.jpaseto.lang.Keys
import dev.paseto.jpaseto.lang.Services
import org.testng.annotations.Test

import javax.crypto.SecretKey
import java.nio.charset.StandardCharsets

import static org.hamcrest.MatcherAssert.assertThat
import static org.hamcrest.Matchers.equalTo
import static org.hamcrest.Matchers.instanceOf
class BouncyCastleV1LocalCryptoProviderTest {

@Test
void loadServiceTest() {
assertThat Services.loadFirst(V1LocalCryptoProvider), instanceOf(BouncyCastleV1LocalCryptoProvider)
}

@Test
void hkdfSha384Test() {
SecretKey secretKey = Keys.secretKey(decode("3nQBDXcLZRTcVZF0NS/6yZ3JO03i/Yv+C1CQRvPgmJk"))
byte[] salt = decode("/bvrxpG04bMH2j98Sgm5ug")
byte[] info = "test-info".getBytes(StandardCharsets.UTF_8)
String expectedResult = "PtiIWzWkNywvjlnyv60Rtz2Zr7vQsgZivlj0Ys9HDy4"

byte[] result = new BouncyCastleV1LocalCryptoProvider().hkdfSha384(secretKey, salt, info)
assertThat encodeToString(result), equalTo(expectedResult)
}

private static String encodeToString(byte[] bytes) {
return Base64.getEncoder().withoutPadding().encodeToString(bytes)
}

private static byte[] decode(String input) {
return Base64.getDecoder().decode(input)
}
}
49 changes: 49 additions & 0 deletions extensions/crypto/hkdf/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2019-Present paseto.dev, Inc.
~
~ 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>dev.paseto</groupId>
<artifactId>jpaseto-root</artifactId>
<version>0.5.0-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>

<artifactId>jpaseto-hkdf</artifactId>
<name>JPaseto :: Crypto :: HKDF</name>

<dependencies>
<dependency>
<groupId>dev.paseto</groupId>
<artifactId>jpaseto-impl</artifactId>
</dependency>

<dependency>
<groupId>at.favre.lib</groupId>
<artifactId>hkdf</artifactId>
</dependency>

<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<scope>provided</scope>
<optional>true</optional>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2020-Present paseto.dev
*
* 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 dev.paseto.jpaseto.crypto.hkdf;

import at.favre.lib.crypto.HKDF;
import at.favre.lib.crypto.HkdfMacFactory;
import com.google.auto.service.AutoService;
import dev.paseto.jpaseto.impl.crypto.BaseV1LocalCryptoProvider;
import dev.paseto.jpaseto.impl.crypto.V1LocalCryptoProvider;

import javax.crypto.SecretKey;

/**
* @since 0.5.0
*/
@AutoService(V1LocalCryptoProvider.class)
public class HKDFV1LocalCryptoProvider extends BaseV1LocalCryptoProvider {

@Override
protected byte[] hkdfSha384(SecretKey sharedSecret, byte[] salt, byte[] info) {
HKDF hkdfSha384 = HKDF.from(new HkdfMacFactory.Default("HmacSHA384"));
return hkdfSha384.extractAndExpand(salt, sharedSecret.getEncoded(), info, 32);
}
}
Loading

0 comments on commit d028cd6

Please sign in to comment.