Skip to content

Commit 4d29db6

Browse files
committed
Add support for HFS tokens and pattern modifiers
1 parent 4de5aef commit 4d29db6

File tree

3 files changed

+232
-10
lines changed

3 files changed

+232
-10
lines changed

src/main/java/com/eatthepath/noise/HandshakePattern.java

Lines changed: 101 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,21 @@ class HandshakePattern {
340340
}
341341

342342
record MessagePattern(NoiseHandshake.Role sender, Token[] tokens) {
343+
344+
MessagePattern withAddedToken(final Token token, final int insertionIndex) {
345+
if (insertionIndex < 0 || insertionIndex >= this.tokens().length + 1) {
346+
throw new IllegalArgumentException("Illegal insertion index");
347+
}
348+
349+
final Token[] modifiedTokens = new Token[this.tokens().length + 1];
350+
System.arraycopy(this.tokens(), 0, modifiedTokens, 0, insertionIndex);
351+
modifiedTokens[insertionIndex] = token;
352+
System.arraycopy(this.tokens(), insertionIndex, modifiedTokens,
353+
insertionIndex + 1, this.tokens().length - insertionIndex);
354+
355+
return new MessagePattern(this.sender(), modifiedTokens);
356+
}
357+
343358
@Override
344359
public String toString() {
345360
final String prefix = switch (sender()) {
@@ -375,18 +390,24 @@ enum Token {
375390
ES,
376391
SE,
377392
SS,
378-
PSK;
393+
PSK,
394+
E1,
395+
EKEM1;
379396

380397
static Token fromString(final String string) {
381-
return switch (string) {
382-
case "e", "E" -> E;
383-
case "s", "S" -> S;
384-
case "ee", "EE" -> EE;
385-
case "es", "ES" -> ES;
386-
case "se", "SE" -> SE;
387-
case "ss", "SS" -> SS;
388-
case "psk", "PSK" -> PSK;
389-
default -> throw new IllegalArgumentException("Unrecognized token: " + string);
398+
for (final Token token : Token.values()) {
399+
if (token.name().equalsIgnoreCase(string)) {
400+
return token;
401+
}
402+
}
403+
404+
throw new IllegalArgumentException("Unrecognized token: " + string);
405+
}
406+
407+
boolean isKeyAgreementToken() {
408+
return switch (this) {
409+
case EE, ES, SE, SS -> true;
410+
default -> false;
390411
};
391412
}
392413
}
@@ -482,6 +503,8 @@ HandshakePattern withModifier(final String modifier) {
482503
modifiedMessagePatterns = getPatternsWithFallbackModifier();
483504
} else if (modifier.startsWith("psk")) {
484505
modifiedMessagePatterns = getPatternsWithPskModifier(modifier);
506+
} else if ("hfs".equals(modifier)) {
507+
modifiedMessagePatterns = getPatternsWithHfsModifier();
485508
} else {
486509
throw new IllegalArgumentException("Unrecognized modifier: " + modifier);
487510
}
@@ -538,6 +561,74 @@ private MessagePattern[][] getPatternsWithPskModifier(final String modifier) {
538561
return new MessagePattern[][] { modifiedPreMessagePatterns, modifiedHandshakeMessagePatterns };
539562
}
540563

564+
private MessagePattern[][] getPatternsWithHfsModifier() {
565+
// Temporarily combine the pre-messages and "normal" messages to make iteration/state management easier
566+
final MessagePattern[] messagePatterns =
567+
new MessagePattern[getPreMessagePatterns().length + getHandshakeMessagePatterns().length];
568+
569+
System.arraycopy(getPreMessagePatterns(), 0, messagePatterns, 0, getPreMessagePatterns().length);
570+
System.arraycopy(getHandshakeMessagePatterns(), 0, messagePatterns,
571+
getPreMessagePatterns().length, getHandshakeMessagePatterns().length);
572+
573+
boolean insertedE1Token = false;
574+
boolean insertedEkem1Token = false;
575+
576+
for (int i = 0; i < messagePatterns.length; i++) {
577+
if (!insertedE1Token && Arrays.stream(messagePatterns[i].tokens()).anyMatch(token -> token == Token.E)) {
578+
// We haven't inserted an E1 token yet, and this message pattern needs one. Exactly where it should go depends
579+
// on whether this message pattern also contains a key agreement token, but either way, this pattern will wind
580+
// up one token longer than it was when it started.
581+
int insertionIndex = -1;
582+
583+
for (int t = 0; t < messagePatterns[i].tokens().length; t++) {
584+
final Token token = messagePatterns[i].tokens()[t];
585+
586+
// TODO Prove that E must come before key agreement tokens
587+
if (token == Token.E || token.isKeyAgreementToken()) {
588+
insertionIndex = t + 1;
589+
590+
if (token.isKeyAgreementToken()) {
591+
break;
592+
}
593+
}
594+
}
595+
596+
messagePatterns[i] = messagePatterns[i].withAddedToken(Token.E1, insertionIndex);
597+
insertedE1Token = true;
598+
}
599+
600+
if (!insertedEkem1Token && Arrays.stream(messagePatterns[i].tokens()).anyMatch(token -> token == Token.EE)) {
601+
// We haven't inserted an EKEM1 token yet, and this pattern needs one. EKEM1 tokens always go after the first
602+
// EE token.
603+
int insertionIndex = -1;
604+
605+
for (int t = 0; t < messagePatterns[i].tokens().length; t++) {
606+
if (messagePatterns[i].tokens()[t] == Token.EE) {
607+
insertionIndex = t + 1;
608+
break;
609+
}
610+
}
611+
612+
messagePatterns[i] = messagePatterns[i].withAddedToken(Token.EKEM1, insertionIndex);
613+
insertedEkem1Token = true;
614+
}
615+
616+
if (insertedE1Token && insertedEkem1Token) {
617+
// No need to inspect the rest of the message patterns if we've already inserted both of the HFS tokens
618+
break;
619+
}
620+
}
621+
622+
final MessagePattern[] modifiedPreMessagePatterns = new MessagePattern[getPreMessagePatterns().length];
623+
final MessagePattern[] modifiedHandshakeMessagePatterns = new MessagePattern[getHandshakeMessagePatterns().length];
624+
625+
System.arraycopy(messagePatterns, 0, modifiedPreMessagePatterns, 0, getPreMessagePatterns().length);
626+
System.arraycopy(messagePatterns, getPreMessagePatterns().length,
627+
modifiedHandshakeMessagePatterns, 0, getHandshakeMessagePatterns().length);
628+
629+
return new MessagePattern[][] { modifiedPreMessagePatterns, modifiedHandshakeMessagePatterns };
630+
}
631+
541632
private String getModifiedName(final String modifier) {
542633
final String modifiedName;
543634

src/main/java/com/eatthepath/noise/NoiseHandshake.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ public enum Role {
349349
}
350350
case EE, ES, SE, SS, PSK ->
351351
throw new IllegalArgumentException("Key-mixing tokens must not appear in pre-messages");
352+
case E1, EKEM1 -> throw new UnsupportedOperationException();
352353
}))
353354
.forEach(publicKey -> mixHash(keyAgreement.serializePublicKey(publicKey)));
354355
}

src/test/java/com/eatthepath/noise/HandshakePatternTest.java

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,111 @@ void withPskModifier() throws NoSuchPatternException {
169169
}
170170
}
171171

172+
@ParameterizedTest
173+
@MethodSource
174+
void withHfsModifier(final HandshakePattern expectedHfsPattern) throws NoSuchPatternException {
175+
final String fundamentalPatternName = HandshakePattern.getFundamentalPatternName(expectedHfsPattern.getName());
176+
177+
assertEquals(expectedHfsPattern, HandshakePattern.getInstance(fundamentalPatternName).withModifier("hfs"));
178+
}
179+
180+
private static List<HandshakePattern> withHfsModifier() {
181+
return List.of(
182+
HandshakePattern.fromString("""
183+
NNhfs:
184+
-> e, e1
185+
<- e, ee, ekem1
186+
"""),
187+
188+
HandshakePattern.fromString("""
189+
NKhfs:
190+
<- s
191+
...
192+
-> e, es, e1
193+
<- e, ee, ekem1
194+
"""),
195+
196+
HandshakePattern.fromString("""
197+
NXhfs:
198+
-> e, e1
199+
<- e, ee, ekem1, s, es
200+
"""),
201+
202+
HandshakePattern.fromString("""
203+
XNhfs:
204+
-> e, e1
205+
<- e, ee, ekem1
206+
-> s, se
207+
"""),
208+
209+
HandshakePattern.fromString("""
210+
XKhfs:
211+
<- s
212+
...
213+
-> e, es, e1
214+
<- e, ee, ekem1
215+
-> s, se
216+
"""),
217+
218+
HandshakePattern.fromString("""
219+
XXhfs:
220+
-> e, e1
221+
<- e, ee, ekem1, s, es
222+
-> s, se
223+
"""),
224+
225+
HandshakePattern.fromString("""
226+
KNhfs:
227+
-> s
228+
...
229+
-> e, e1
230+
<- e, ee, ekem1, se
231+
"""),
232+
233+
// Note that this is different from what's listed at https://github.com/noiseprotocol/noise_hfs_spec/blob/025f0f60cb3b94ad75b68e3a4158b9aac234f8cb/noise_hfs.md?plain=1#L130-L135;
234+
// the specification (at the time of writing) appears to have a typo. Please see
235+
// https://github.com/noiseprotocol/noise_hfs_spec/pull/3.
236+
HandshakePattern.fromString("""
237+
KKhfs:
238+
-> s
239+
<- s
240+
...
241+
-> e, es, e1, ss
242+
<- e, ee, ekem1, se
243+
"""),
244+
245+
HandshakePattern.fromString("""
246+
KXhfs:
247+
-> s
248+
...
249+
-> e, e1
250+
<- e, ee, ekem1, se, s, es
251+
"""),
252+
253+
// This also deviates from the latest version of the spec to fix a typo (the `ee` token is missing in the
254+
// current draft of the spec). Please see https://github.com/noiseprotocol/noise_hfs_spec/pull/4.
255+
HandshakePattern.fromString("""
256+
INhfs:
257+
-> e, e1, s
258+
<- e, ee, ekem1, se
259+
"""),
260+
261+
HandshakePattern.fromString("""
262+
IKhfs:
263+
<- s
264+
...
265+
-> e, es, e1, s, ss
266+
<- e, ee, ekem1, se
267+
"""),
268+
269+
HandshakePattern.fromString("""
270+
IXhfs:
271+
-> e, e1, s
272+
<- e, ee, ekem1, se, s, es
273+
""")
274+
);
275+
}
276+
172277
@Test
173278
void withModifierUnrecognized() {
174279
assertThrows(IllegalArgumentException.class, () -> HandshakePattern.getInstance("XX").withModifier("fancy"));
@@ -253,4 +358,29 @@ void requiresRemoteStaticPublicKey() throws NoSuchPatternException {
253358
assertTrue(HandshakePattern.getInstance("KN").requiresRemoteStaticPublicKey(Role.RESPONDER));
254359
assertFalse(HandshakePattern.getInstance("KN").requiresRemoteStaticPublicKey(Role.INITIATOR));
255360
}
361+
362+
@Test
363+
void messagePatternWithAddedToken() {
364+
final MessagePattern originalPattern = new HandshakePattern.MessagePattern(Role.INITIATOR,
365+
new Token[] { Token.E, Token.EE, Token.SE });
366+
367+
assertEquals(new HandshakePattern.MessagePattern(Role.INITIATOR,
368+
new Token[] { Token.E1, Token.E, Token.EE, Token.SE }),
369+
originalPattern.withAddedToken(Token.E1, 0));
370+
371+
assertEquals(new HandshakePattern.MessagePattern(Role.INITIATOR,
372+
new Token[] { Token.E, Token.E1, Token.EE, Token.SE }),
373+
originalPattern.withAddedToken(Token.E1, 1));
374+
375+
assertEquals(new HandshakePattern.MessagePattern(Role.INITIATOR,
376+
new Token[] { Token.E, Token.EE, Token.E1, Token.SE }),
377+
originalPattern.withAddedToken(Token.E1, 2));
378+
379+
assertEquals(new HandshakePattern.MessagePattern(Role.INITIATOR,
380+
new Token[] { Token.E, Token.EE, Token.SE, Token.E1 }),
381+
originalPattern.withAddedToken(Token.E1, 3));
382+
383+
assertThrows(IllegalArgumentException.class, () -> originalPattern.withAddedToken(Token.E1, -1));
384+
assertThrows(IllegalArgumentException.class, () -> originalPattern.withAddedToken(Token.E1, 4));
385+
}
256386
}

0 commit comments

Comments
 (0)