Skip to content

Commit d028cd6

Browse files
zbiljicbdemers
andauthored
Include standalone HKDF implementation for v1.local tokens (#6)
* 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 <bdemers@apache.org>
1 parent ec5f808 commit d028cd6

17 files changed

Lines changed: 354 additions & 100 deletions

File tree

NOTICE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ This project includes:
1919
ASM Tree under BSD
2020
ASM Util under BSD
2121
Gson under Apache 2.0
22+
HKDF-RFC5869 under Apache License, Version 2.0
2223
jffi under The Apache Software License, Version 2.0
2324
jnr-a64asm under The Apache Software License, Version 2.0
2425
jnr-ffi under The Apache Software License, Version 2.0

README.md

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -202,26 +202,34 @@ If you're building a (non-Android) JDK project, you will want to define the foll
202202
<dependency>
203203
<groupId>dev.paseto</groupId>
204204
<artifactId>jpaseto-api</artifactId>
205-
<version>0.1.0</version>
205+
<version>0.5.0</version>
206206
</dependency>
207207
<dependency>
208208
<groupId>dev.paseto</groupId>
209209
<artifactId>jpaseto-impl</artifactId>
210-
<version>0.1.0</version>
210+
<version>0.5.0</version>
211211
<scope>runtime</scope>
212212
</dependency>
213213
<dependency>
214214
<groupId>dev.paseto</groupId>
215215
<artifactId>jpaseto-jackson</artifactId>
216-
<version>0.1.0</version>
216+
<version>0.5.0</version>
217217
<scope>runtime</scope>
218218
</dependency>
219219
<!-- Uncomment the next lines if you want to use v1.local tokens -->
220220
<!--
221221
<dependency>
222222
<groupId>dev.paseto</groupId>
223223
<artifactId>jpaseto-bouncy-castle</artifactId>
224-
<version>0.1.0</version>
224+
<version>0.5.0</version>
225+
<scope>runtime</scope>
226+
</dependency> -->
227+
<!-- or this (only 'v1.local' tokens) for smaller dependency (~11 KB for HKDF vs. ~4.3 MB for Bouncy Castle) -->
228+
<!--
229+
<dependency>
230+
<groupId>dev.paseto</groupId>
231+
<artifactId>jpaseto-hkdf</artifactId>
232+
<version>0.5.0</version>
225233
<scope>runtime</scope>
226234
</dependency> -->
227235
<!-- Uncomment the next lines if you want to use v2 tokens -->
@@ -230,7 +238,7 @@ If you're building a (non-Android) JDK project, you will want to define the foll
230238
<dependency>
231239
<groupId>dev.paseto</groupId>
232240
<artifactId>jpaseto-sodium</artifactId>
233-
<version>0.1.0</version>
241+
<version>0.5.0</version>
234242
<scope>runtime</scope>
235243
</dependency> -->
236244
```
@@ -240,22 +248,24 @@ If you're building a (non-Android) JDK project, you will want to define the foll
240248

241249
```groovy
242250
dependencies {
243-
compile 'dev.paseto:jpaseto-api:0.1.0'
244-
runtime 'dev.paseto:jpaseto-impl:0.1.0',
251+
compile 'dev.paseto:jpaseto-api:0.5.0'
252+
runtime 'dev.paseto:jpaseto-impl:0.5.0',
245253
// Uncomment the next lines if you want to use v1.local tokens
246-
// 'dev.paseto:jpaseto-bouncy-castle:0.1.0',
254+
// 'dev.paseto:jpaseto-bouncy-castle:0.5.0',
255+
// or this (only 'v1.local' tokens) for smaller dependency (~11 KB for HKDF vs. ~4.3 MB for Bouncy Castle)
256+
// 'dev.paseto:jpaseto-hkdf:0.5.0',
247257
// Uncomment the next lines if you want to use v2 tokens
248258
// NOTE: this requires the native lib sodium library installed on your system see below
249-
// 'dev.paseto:jpaseto-sodium:0.1.0',
250-
'dev.paseto:jpaseto-jackson:0.1.0'
259+
// 'dev.paseto:jpaseto-sodium:0.5.0',
260+
'dev.paseto:jpaseto-jackson:0.5.0'
251261
}
252262
```
253263
<a name="install-sodium"></a>
254264
#### libsodium
255265

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

258-
**NOTE:** `public` tokens can be used with the `jpaseto-bouncy-castle` dependency or Java 11+. `v1.local` tokens require `jpaseto-bouncy-castle`.
268+
**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`.
259269

260270
- MacOS - Can install libsodium using brew:
261271

api/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
<parent>
2121
<groupId>dev.paseto</groupId>
2222
<artifactId>jpaseto-root</artifactId>
23-
<version>0.4.1-SNAPSHOT</version>
23+
<version>0.5.0-SNAPSHOT</version>
2424
</parent>
2525

2626
<artifactId>jpaseto-api</artifactId>

coverage/pom.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
<parent>
2222
<groupId>dev.paseto</groupId>
2323
<artifactId>jpaseto-root</artifactId>
24-
<version>0.4.1-SNAPSHOT</version>
24+
<version>0.5.0-SNAPSHOT</version>
2525
</parent>
2626

2727
<artifactId>jpaseto-coverage</artifactId>
@@ -49,6 +49,10 @@
4949
<groupId>dev.paseto</groupId>
5050
<artifactId>jpaseto-bouncy-castle</artifactId>
5151
</dependency>
52+
<dependency>
53+
<groupId>dev.paseto</groupId>
54+
<artifactId>jpaseto-hkdf</artifactId>
55+
</dependency>
5256
<dependency>
5357
<groupId>dev.paseto</groupId>
5458
<artifactId>jpaseto-its-sodium-jackson</artifactId>

extensions/crypto/bouncy-castle/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
<parent>
2121
<groupId>dev.paseto</groupId>
2222
<artifactId>jpaseto-root</artifactId>
23-
<version>0.4.1-SNAPSHOT</version>
23+
<version>0.5.0-SNAPSHOT</version>
2424
<relativePath>../../../pom.xml</relativePath>
2525
</parent>
2626

extensions/crypto/bouncy-castle/src/main/java/dev/paseto/jpaseto/crypto/bouncycastle/BouncyCastleV1LocalCryptoProvider.java

Lines changed: 4 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -16,93 +16,23 @@
1616
package dev.paseto.jpaseto.crypto.bouncycastle;
1717

1818
import com.google.auto.service.AutoService;
19-
import dev.paseto.jpaseto.InvalidMacException;
20-
import dev.paseto.jpaseto.impl.crypto.Hmacs;
21-
import dev.paseto.jpaseto.impl.crypto.PreAuthEncoder;
19+
import dev.paseto.jpaseto.impl.crypto.BaseV1LocalCryptoProvider;
2220
import dev.paseto.jpaseto.impl.crypto.V1LocalCryptoProvider;
23-
import dev.paseto.jpaseto.impl.lang.Bytes;
2421
import org.bouncycastle.crypto.generators.HKDFBytesGenerator;
2522
import org.bouncycastle.crypto.params.HKDFParameters;
2623
import org.bouncycastle.crypto.util.DigestFactory;
2724

28-
import javax.crypto.Cipher;
2925
import javax.crypto.SecretKey;
30-
import java.security.MessageDigest;
31-
import java.util.Arrays;
32-
33-
import static java.nio.charset.StandardCharsets.UTF_8;
3426

3527
@AutoService(V1LocalCryptoProvider.class)
36-
public class BouncyCastleV1LocalCryptoProvider implements V1LocalCryptoProvider {
37-
38-
private static final byte[] HEADER_BYTES = "v1.local.".getBytes(UTF_8);
28+
public class BouncyCastleV1LocalCryptoProvider extends BaseV1LocalCryptoProvider {
3929

4030
@Override
41-
public byte[] encrypt(byte[] payload, byte[] footer, byte[] nonce, SecretKey sharedSecret) {
42-
43-
byte[] salt = Arrays.copyOf(nonce, 16);
44-
byte[] rightNonce = Arrays.copyOfRange(nonce, 16, nonce.length);
45-
46-
// 4
47-
byte[] encryptionKey = encryptionKey(sharedSecret, salt);
48-
byte[] authenticationKey = authenticationKey(sharedSecret, salt);
49-
50-
// 5
51-
byte[] cipherText = V1LocalCryptoProvider.doCipher(Cipher.ENCRYPT_MODE, encryptionKey, rightNonce, payload);
52-
53-
//6
54-
byte[] preAuth = PreAuthEncoder.encode(HEADER_BYTES, nonce, cipherText, footer);
55-
56-
//7
57-
byte[] calculatedMac = Hmacs.hmacSha384(authenticationKey, preAuth);
58-
59-
// 8
60-
return Bytes.concat(nonce, cipherText, calculatedMac);
61-
}
62-
63-
@Override
64-
public byte[] decrypt(byte[] encryptedBytes, byte[] footer, byte[] nonce, SecretKey sharedSecret) {
65-
66-
// 3
67-
byte[] salt = Arrays.copyOf(nonce, 16);
68-
byte[] rightNonce = Arrays.copyOfRange(nonce, 16, nonce.length);
69-
70-
byte[] cipherText = Arrays.copyOfRange(encryptedBytes, 32, encryptedBytes.length - 48);
71-
byte[] mac = Arrays.copyOfRange(encryptedBytes, encryptedBytes.length - 48, encryptedBytes.length);
72-
73-
// 4
74-
byte[] encryptionKey = encryptionKey(sharedSecret, salt);
75-
byte[] authenticationKey = authenticationKey(sharedSecret, salt);
76-
77-
// 5
78-
byte[] preAuth = PreAuthEncoder.encode(HEADER_BYTES, nonce, cipherText, footer);
79-
80-
// 6
81-
byte[] calculatedMac = Hmacs.hmacSha384(authenticationKey, preAuth);
82-
83-
// 7
84-
if (!MessageDigest.isEqual(calculatedMac, mac)) {
85-
throw new InvalidMacException("Failed to validate mac in token");
86-
}
87-
88-
// 8
89-
return V1LocalCryptoProvider.doCipher(Cipher.DECRYPT_MODE, encryptionKey, rightNonce, cipherText);
90-
}
91-
92-
private byte[] hkdfSha384(SecretKey sharedSecret, byte[] salt, String info) {
93-
31+
protected byte[] hkdfSha384(SecretKey sharedSecret, byte[] salt, byte[] info) {
9432
HKDFBytesGenerator hkdfBytesGenerator = new HKDFBytesGenerator(DigestFactory.createSHA384());
95-
hkdfBytesGenerator.init(new HKDFParameters(sharedSecret.getEncoded(), salt, info.getBytes(UTF_8)));
33+
hkdfBytesGenerator.init(new HKDFParameters(sharedSecret.getEncoded(), salt, info));
9634
byte[] result = new byte[32];
9735
hkdfBytesGenerator.generateBytes(result, 0, result.length);
9836
return result;
9937
}
100-
101-
private byte[] encryptionKey(SecretKey sharedSecret, byte[] salt) {
102-
return hkdfSha384(sharedSecret, salt, "paseto-encryption-key");
103-
}
104-
105-
private byte[] authenticationKey(SecretKey sharedSecret, byte[] salt) {
106-
return hkdfSha384(sharedSecret, salt, "paseto-auth-key-for-aead");
107-
}
10838
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2020-Present paseto.dev
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package dev.paseto.jpaseto.crypto.bouncycastle
17+
18+
import dev.paseto.jpaseto.impl.crypto.V1LocalCryptoProvider
19+
import dev.paseto.jpaseto.lang.Keys
20+
import dev.paseto.jpaseto.lang.Services
21+
import org.testng.annotations.Test
22+
23+
import javax.crypto.SecretKey
24+
import java.nio.charset.StandardCharsets
25+
26+
import static org.hamcrest.MatcherAssert.assertThat
27+
import static org.hamcrest.Matchers.equalTo
28+
import static org.hamcrest.Matchers.instanceOf
29+
class BouncyCastleV1LocalCryptoProviderTest {
30+
31+
@Test
32+
void loadServiceTest() {
33+
assertThat Services.loadFirst(V1LocalCryptoProvider), instanceOf(BouncyCastleV1LocalCryptoProvider)
34+
}
35+
36+
@Test
37+
void hkdfSha384Test() {
38+
SecretKey secretKey = Keys.secretKey(decode("3nQBDXcLZRTcVZF0NS/6yZ3JO03i/Yv+C1CQRvPgmJk"))
39+
byte[] salt = decode("/bvrxpG04bMH2j98Sgm5ug")
40+
byte[] info = "test-info".getBytes(StandardCharsets.UTF_8)
41+
String expectedResult = "PtiIWzWkNywvjlnyv60Rtz2Zr7vQsgZivlj0Ys9HDy4"
42+
43+
byte[] result = new BouncyCastleV1LocalCryptoProvider().hkdfSha384(secretKey, salt, info)
44+
assertThat encodeToString(result), equalTo(expectedResult)
45+
}
46+
47+
private static String encodeToString(byte[] bytes) {
48+
return Base64.getEncoder().withoutPadding().encodeToString(bytes)
49+
}
50+
51+
private static byte[] decode(String input) {
52+
return Base64.getDecoder().decode(input)
53+
}
54+
}

extensions/crypto/hkdf/pom.xml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
~ Copyright 2019-Present paseto.dev, Inc.
4+
~
5+
~ Licensed under the Apache License, Version 2.0 (the "License");
6+
~ you may not use this file except in compliance with the License.
7+
~ You may obtain a copy of the License at
8+
~
9+
~ http://www.apache.org/licenses/LICENSE-2.0
10+
~
11+
~ Unless required by applicable law or agreed to in writing, software
12+
~ distributed under the License is distributed on an "AS IS" BASIS,
13+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
~ See the License for the specific language governing permissions and
15+
~ limitations under the License.
16+
-->
17+
<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">
18+
<modelVersion>4.0.0</modelVersion>
19+
20+
<parent>
21+
<groupId>dev.paseto</groupId>
22+
<artifactId>jpaseto-root</artifactId>
23+
<version>0.5.0-SNAPSHOT</version>
24+
<relativePath>../../../pom.xml</relativePath>
25+
</parent>
26+
27+
<artifactId>jpaseto-hkdf</artifactId>
28+
<name>JPaseto :: Crypto :: HKDF</name>
29+
30+
<dependencies>
31+
<dependency>
32+
<groupId>dev.paseto</groupId>
33+
<artifactId>jpaseto-impl</artifactId>
34+
</dependency>
35+
36+
<dependency>
37+
<groupId>at.favre.lib</groupId>
38+
<artifactId>hkdf</artifactId>
39+
</dependency>
40+
41+
<dependency>
42+
<groupId>com.google.auto.service</groupId>
43+
<artifactId>auto-service</artifactId>
44+
<scope>provided</scope>
45+
<optional>true</optional>
46+
</dependency>
47+
</dependencies>
48+
49+
</project>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2020-Present paseto.dev
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package dev.paseto.jpaseto.crypto.hkdf;
17+
18+
import at.favre.lib.crypto.HKDF;
19+
import at.favre.lib.crypto.HkdfMacFactory;
20+
import com.google.auto.service.AutoService;
21+
import dev.paseto.jpaseto.impl.crypto.BaseV1LocalCryptoProvider;
22+
import dev.paseto.jpaseto.impl.crypto.V1LocalCryptoProvider;
23+
24+
import javax.crypto.SecretKey;
25+
26+
/**
27+
* @since 0.5.0
28+
*/
29+
@AutoService(V1LocalCryptoProvider.class)
30+
public class HKDFV1LocalCryptoProvider extends BaseV1LocalCryptoProvider {
31+
32+
@Override
33+
protected byte[] hkdfSha384(SecretKey sharedSecret, byte[] salt, byte[] info) {
34+
HKDF hkdfSha384 = HKDF.from(new HkdfMacFactory.Default("HmacSHA384"));
35+
return hkdfSha384.extractAndExpand(salt, sharedSecret.getEncoded(), info, 32);
36+
}
37+
}

0 commit comments

Comments
 (0)