-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
Copy pathNtlmEngine.java
1586 lines (1405 loc) · 60.2 KB
/
NtlmEngine.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
// fork from Apache HttpComponents
package org.asynchttpclient.ntlm;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.Nullable;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
import java.util.Locale;
import static java.nio.charset.StandardCharsets.US_ASCII;
/**
* Provides an implementation for NTLMv1, NTLMv2, and NTLM2 Session forms of the NTLM
* authentication protocol.
*
* @since 4.1
*/
@SuppressWarnings("unused")
public final class NtlmEngine {
public static final NtlmEngine INSTANCE = new NtlmEngine();
/**
* Unicode encoding
*/
private static final Charset UNICODE_LITTLE_UNMARKED = StandardCharsets.UTF_16LE;
private static final byte[] MAGIC_CONSTANT = "KGS!@#$%".getBytes(US_ASCII);
// Flags we use; descriptions according to:
// http://davenport.sourceforge.net/ntlm.html
// and
// http://msdn.microsoft.com/en-us/library/cc236650%28v=prot.20%29.aspx
private static final int FLAG_REQUEST_UNICODE_ENCODING = 0x00000001; // Unicode string encoding requested
private static final int FLAG_REQUEST_TARGET = 0x00000004; // Requests target field
private static final int FLAG_REQUEST_SIGN = 0x00000010; // Requests all messages have a signature attached, in NEGOTIATE message.
private static final int FLAG_REQUEST_SEAL = 0x00000020; // Request key exchange for message confidentiality in NEGOTIATE message. MUST be used in conjunction with 56BIT.
private static final int FLAG_REQUEST_LAN_MANAGER_KEY = 0x00000080; // Request Lan Manager key instead of user session key
private static final int FLAG_REQUEST_NTLMv1 = 0x00000200; // Request NTLMv1 security. MUST be set in NEGOTIATE and CHALLENGE both
private static final int FLAG_DOMAIN_PRESENT = 0x00001000; // Domain is present in message
private static final int FLAG_WORKSTATION_PRESENT = 0x00002000; // Workstation is present in message
private static final int FLAG_REQUEST_ALWAYS_SIGN = 0x00008000; // Requests a signature block on all messages. Overridden by REQUEST_SIGN and REQUEST_SEAL.
private static final int FLAG_REQUEST_NTLM2_SESSION = 0x00080000; // From server in challenge, requesting NTLM2 session security
private static final int FLAG_REQUEST_VERSION = 0x02000000; // Request protocol version
private static final int FLAG_TARGETINFO_PRESENT = 0x00800000; // From server in challenge message, indicating targetinfo is present
private static final int FLAG_REQUEST_128BIT_KEY_EXCH = 0x20000000; // Request explicit 128-bit key exchange
private static final int FLAG_REQUEST_EXPLICIT_KEY_EXCH = 0x40000000; // Request explicit key exchange
private static final int FLAG_REQUEST_56BIT_ENCRYPTION = 0x80000000; // Must be used in conjunction with SEAL
/**
* Secure random generator
*/
private static final @Nullable SecureRandom RND_GEN;
static {
SecureRandom rnd = null;
try {
rnd = SecureRandom.getInstance("SHA1PRNG");
} catch (final Exception ignore) {
}
RND_GEN = rnd;
}
/**
* The signature string as bytes in the default encoding
*/
private static final byte[] SIGNATURE;
static {
final byte[] bytesWithoutNull = "NTLMSSP".getBytes(US_ASCII);
SIGNATURE = new byte[bytesWithoutNull.length + 1];
System.arraycopy(bytesWithoutNull, 0, SIGNATURE, 0, bytesWithoutNull.length);
SIGNATURE[bytesWithoutNull.length] = 0x00;
}
private static final String TYPE_1_MESSAGE = new Type1Message().getResponse();
/**
* Creates the type 3 message using the given server nonce. The type 3
* message includes all the information for authentication, host, domain,
* username and the result of encrypting the nonce sent by the server using
* the user's password as the key.
*
* @param user The username. This should not include the domain name.
* @param password The password.
* @param host The host that is originating the authentication request.
* @param domain The domain to authenticate within.
* @param nonce the 8 byte array the server sent.
* @return The type 3 message.
* @throws NtlmEngineException If {@encrypt(byte[],byte[])} fails.
*/
private static String getType3Message(final String user, final String password, final String host, final String domain, final byte[] nonce,
final int type2Flags, final @Nullable String target, final byte @Nullable [] targetInformation) {
return new Type3Message(domain, host, user, password, nonce, type2Flags, target, targetInformation).getResponse();
}
/**
* Strip dot suffix from a name
*/
private static String stripDotSuffix(final String value) {
final int index = value.indexOf('.');
if (index != -1) {
return value.substring(0, index);
}
return value;
}
/**
* Convert host to standard form
*/
@Contract(value = "!null -> !null", pure = true)
private static @Nullable String convertHost(final String host) {
return host != null ? stripDotSuffix(host).toUpperCase() : null;
}
/**
* Convert domain to standard form
*/
@Contract(value = "!null -> !null", pure = true)
private static @Nullable String convertDomain(final String domain) {
return domain != null ? stripDotSuffix(domain).toUpperCase() : null;
}
private static int readULong(final byte[] src, final int index) {
if (src.length < index + 4) {
throw new NtlmEngineException("NTLM authentication - buffer too small for DWORD");
}
return src[index] & 0xff | (src[index + 1] & 0xff) << 8 | (src[index + 2] & 0xff) << 16 | (src[index + 3] & 0xff) << 24;
}
private static int readUShort(final byte[] src, final int index) {
if (src.length < index + 2) {
throw new NtlmEngineException("NTLM authentication - buffer too small for WORD");
}
return src[index] & 0xff | (src[index + 1] & 0xff) << 8;
}
private static byte[] readSecurityBuffer(final byte[] src, final int index) {
final int length = readUShort(src, index);
final int offset = readULong(src, index + 4);
if (src.length < offset + length) {
throw new NtlmEngineException("NTLM authentication - buffer too small for data item");
}
final byte[] buffer = new byte[length];
System.arraycopy(src, offset, buffer, 0, length);
return buffer;
}
/**
* Calculate a challenge block
*/
private static byte[] makeRandomChallenge() {
if (RND_GEN == null) {
throw new NtlmEngineException("Random generator not available");
}
final byte[] rval = new byte[8];
synchronized (RND_GEN) {
RND_GEN.nextBytes(rval);
}
return rval;
}
/**
* Calculate a 16-byte secondary key
*/
private static byte[] makeSecondaryKey() {
if (RND_GEN == null) {
throw new NtlmEngineException("Random generator not available");
}
final byte[] rval = new byte[16];
synchronized (RND_GEN) {
RND_GEN.nextBytes(rval);
}
return rval;
}
private static class CipherGen {
protected final String domain;
protected final String user;
protected final String password;
protected final byte[] challenge;
protected final @Nullable String target;
protected final byte @Nullable [] targetInformation;
// Information we can generate but may be passed in (for testing)
protected byte @Nullable [] clientChallenge;
protected byte @Nullable [] clientChallenge2;
protected byte @Nullable [] secondaryKey;
protected byte @Nullable [] timestamp;
// Stuff we always generate
protected byte @Nullable [] lmHash;
protected byte @Nullable [] lmResponse;
protected byte @Nullable [] ntlmHash;
protected byte @Nullable [] ntlmResponse;
protected byte @Nullable [] ntlmv2Hash;
protected byte @Nullable [] lmv2Hash;
protected byte @Nullable [] lmv2Response;
protected byte @Nullable [] ntlmv2Blob;
protected byte @Nullable [] ntlmv2Response;
protected byte @Nullable [] ntlm2SessionResponse;
protected byte @Nullable [] lm2SessionResponse;
protected byte @Nullable [] lmUserSessionKey;
protected byte @Nullable [] ntlmUserSessionKey;
protected byte @Nullable [] ntlmv2UserSessionKey;
protected byte @Nullable [] ntlm2SessionResponseUserSessionKey;
protected byte @Nullable [] lanManagerSessionKey;
CipherGen(final String domain, final String user, final String password, final byte[] challenge, final @Nullable String target,
final byte @Nullable [] targetInformation, final byte @Nullable [] clientChallenge, final byte @Nullable [] clientChallenge2,
final byte @Nullable [] secondaryKey, final byte @Nullable [] timestamp) {
this.domain = domain;
this.target = target;
this.user = user;
this.password = password;
this.challenge = challenge;
this.targetInformation = targetInformation;
this.clientChallenge = clientChallenge;
this.clientChallenge2 = clientChallenge2;
this.secondaryKey = secondaryKey;
this.timestamp = timestamp;
}
CipherGen(final String domain, final String user, final String password, final byte[] challenge, final @Nullable String target,
final byte @Nullable [] targetInformation) {
this(domain, user, password, challenge, target, targetInformation, null, null, null, null);
}
/**
* Calculate and return client challenge
*/
public byte[] getClientChallenge() {
if (clientChallenge == null) {
clientChallenge = makeRandomChallenge();
}
return clientChallenge;
}
/**
* Calculate and return second client challenge
*/
public byte[] getClientChallenge2() {
if (clientChallenge2 == null) {
clientChallenge2 = makeRandomChallenge();
}
return clientChallenge2;
}
/**
* Calculate and return random secondary key
*/
public byte[] getSecondaryKey() {
if (secondaryKey == null) {
secondaryKey = makeSecondaryKey();
}
return secondaryKey;
}
/**
* Calculate and return the LMHash
*/
public byte[] getLMHash() {
if (lmHash == null) {
lmHash = lmHash(password);
}
return lmHash;
}
/**
* Calculate and return the LMResponse
*/
public byte[] getLMResponse() {
if (lmResponse == null) {
lmResponse = lmResponse(getLMHash(), challenge);
}
return lmResponse;
}
/**
* Calculate and return the NTLMHash
*/
public byte[] getNTLMHash() {
if (ntlmHash == null) {
ntlmHash = ntlmHash(password);
}
return ntlmHash;
}
/**
* Calculate and return the NTLMResponse
*/
public byte[] getNTLMResponse() {
if (ntlmResponse == null) {
ntlmResponse = lmResponse(getNTLMHash(), challenge);
}
return ntlmResponse;
}
/**
* Calculate the LMv2 hash
*/
public byte[] getLMv2Hash() {
if (lmv2Hash == null) {
lmv2Hash = lmv2Hash(domain, user, getNTLMHash());
}
return lmv2Hash;
}
/**
* Calculate the NTLMv2 hash
*/
public byte[] getNTLMv2Hash() {
if (ntlmv2Hash == null) {
ntlmv2Hash = ntlmv2Hash(domain, user, getNTLMHash());
}
return ntlmv2Hash;
}
/**
* Calculate a timestamp
*/
public byte[] getTimestamp() {
if (timestamp == null) {
long time = System.currentTimeMillis();
time += 11644473600000L; // milliseconds from January 1, 1601 -> epoch.
time *= 10000; // tenths of a microsecond.
// convert to little-endian byte array.
timestamp = new byte[8];
for (int i = 0; i < 8; i++) {
timestamp[i] = (byte) time;
time >>>= 8;
}
}
return timestamp;
}
/**
* Calculate the NTLMv2Blob
*
* @param targetInformation this parameter is the same object as the field targetInformation,
* but guaranteed to be not null. This is done to satisfy NullAway requirements
*/
public byte[] getNTLMv2Blob(byte[] targetInformation) {
if (ntlmv2Blob == null) {
ntlmv2Blob = createBlob(getClientChallenge2(), targetInformation, getTimestamp());
}
return ntlmv2Blob;
}
/**
* Calculate the NTLMv2Response
*
* @param targetInformation this parameter is the same object as the field targetInformation,
* but guaranteed to be not null. This is done to satisfy NullAway requirements
*/
public byte[] getNTLMv2Response(byte[] targetInformation) {
if (ntlmv2Response == null) {
ntlmv2Response = lmv2Response(getNTLMv2Hash(), challenge, getNTLMv2Blob(targetInformation));
}
return ntlmv2Response;
}
/**
* Calculate the LMv2Response
*/
public byte[] getLMv2Response() {
if (lmv2Response == null) {
lmv2Response = lmv2Response(getLMv2Hash(), challenge, getClientChallenge());
}
return lmv2Response;
}
/**
* Get NTLM2SessionResponse
*/
public byte[] getNTLM2SessionResponse() {
if (ntlm2SessionResponse == null) {
ntlm2SessionResponse = ntlm2SessionResponse(getNTLMHash(), challenge, getClientChallenge());
}
return ntlm2SessionResponse;
}
/**
* Calculate and return LM2 session response
*/
public byte[] getLM2SessionResponse() {
if (lm2SessionResponse == null) {
final byte[] clntChallenge = getClientChallenge();
lm2SessionResponse = new byte[24];
System.arraycopy(clntChallenge, 0, lm2SessionResponse, 0, clntChallenge.length);
Arrays.fill(lm2SessionResponse, clntChallenge.length, lm2SessionResponse.length, (byte) 0x00);
}
return lm2SessionResponse;
}
/**
* Get LMUserSessionKey
*/
public byte[] getLMUserSessionKey() {
if (lmUserSessionKey == null) {
lmUserSessionKey = new byte[16];
System.arraycopy(getLMHash(), 0, lmUserSessionKey, 0, 8);
Arrays.fill(lmUserSessionKey, 8, 16, (byte) 0x00);
}
return lmUserSessionKey;
}
/**
* Get NTLMUserSessionKey
*/
public byte[] getNTLMUserSessionKey() {
if (ntlmUserSessionKey == null) {
final MD4 md4 = new MD4();
md4.update(getNTLMHash());
ntlmUserSessionKey = md4.getOutput();
}
return ntlmUserSessionKey;
}
/**
* GetNTLMv2UserSessionKey
*
* @param targetInformation this parameter is the same object as the field targetInformation,
* but guaranteed to be not null. This is done to satisfy NullAway requirements
*/
public byte[] getNTLMv2UserSessionKey(byte[] targetInformation) {
if (ntlmv2UserSessionKey == null) {
final byte[] ntlmv2hash = getNTLMv2Hash();
final byte[] truncatedResponse = new byte[16];
System.arraycopy(getNTLMv2Response(targetInformation), 0, truncatedResponse, 0, 16);
ntlmv2UserSessionKey = hmacMD5(truncatedResponse, ntlmv2hash);
}
return ntlmv2UserSessionKey;
}
/**
* Get NTLM2SessionResponseUserSessionKey
*/
public byte[] getNTLM2SessionResponseUserSessionKey() {
if (ntlm2SessionResponseUserSessionKey == null) {
final byte[] ntlm2SessionResponseNonce = getLM2SessionResponse();
final byte[] sessionNonce = new byte[challenge.length + ntlm2SessionResponseNonce.length];
System.arraycopy(challenge, 0, sessionNonce, 0, challenge.length);
System.arraycopy(ntlm2SessionResponseNonce, 0, sessionNonce, challenge.length, ntlm2SessionResponseNonce.length);
ntlm2SessionResponseUserSessionKey = hmacMD5(sessionNonce, getNTLMUserSessionKey());
}
return ntlm2SessionResponseUserSessionKey;
}
/**
* Get LAN Manager session key
*/
public byte[] getLanManagerSessionKey() {
if (lanManagerSessionKey == null) {
try {
final byte[] keyBytes = new byte[14];
System.arraycopy(getLMHash(), 0, keyBytes, 0, 8);
Arrays.fill(keyBytes, 8, keyBytes.length, (byte) 0xbd);
final Key lowKey = createDESKey(keyBytes, 0);
final Key highKey = createDESKey(keyBytes, 7);
final byte[] truncatedResponse = new byte[8];
System.arraycopy(getLMResponse(), 0, truncatedResponse, 0, truncatedResponse.length);
Cipher des = Cipher.getInstance("DES/ECB/NoPadding");
des.init(Cipher.ENCRYPT_MODE, lowKey);
final byte[] lowPart = des.doFinal(truncatedResponse);
des = Cipher.getInstance("DES/ECB/NoPadding");
des.init(Cipher.ENCRYPT_MODE, highKey);
final byte[] highPart = des.doFinal(truncatedResponse);
lanManagerSessionKey = new byte[16];
System.arraycopy(lowPart, 0, lanManagerSessionKey, 0, lowPart.length);
System.arraycopy(highPart, 0, lanManagerSessionKey, lowPart.length, highPart.length);
} catch (final Exception e) {
throw new NtlmEngineException(e.getMessage(), e);
}
}
return lanManagerSessionKey;
}
}
/**
* Calculates HMAC-MD5
*/
private static byte[] hmacMD5(final byte[] value, final byte[] key) {
final HMACMD5 hmacMD5 = new HMACMD5(key);
hmacMD5.update(value);
return hmacMD5.getOutput();
}
/**
* Calculates RC4
*/
private static byte[] RC4(final byte[] value, final byte[] key) {
try {
final Cipher rc4 = Cipher.getInstance("RC4");
rc4.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "RC4"));
return rc4.doFinal(value);
} catch (final Exception e) {
throw new NtlmEngineException(e.getMessage(), e);
}
}
/**
* Calculates the NTLM2 Session Response for the given challenge, using the
* specified password and client challenge.
*
* @return The NTLM2 Session Response. This is placed in the NTLM response
* field of the Type 3 message; the LM response field contains the
* client challenge, null-padded to 24 bytes.
*/
private static byte[] ntlm2SessionResponse(final byte[] ntlmHash, final byte[] challenge, final byte[] clientChallenge) {
try {
final MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(challenge);
md5.update(clientChallenge);
final byte[] digest = md5.digest();
final byte[] sessionHash = new byte[8];
System.arraycopy(digest, 0, sessionHash, 0, 8);
return lmResponse(ntlmHash, sessionHash);
} catch (final Exception e) {
if (e instanceof NtlmEngineException) {
throw (NtlmEngineException) e;
}
throw new NtlmEngineException(e.getMessage(), e);
}
}
/**
* Creates the LM Hash of the user's password.
*
* @param password The password.
* @return The LM Hash of the given password, used in the calculation of the
* LM Response.
*/
private static byte[] lmHash(final String password) {
try {
final byte[] oemPassword = password.toUpperCase(Locale.ROOT).getBytes(US_ASCII);
final int length = Math.min(oemPassword.length, 14);
final byte[] keyBytes = new byte[14];
System.arraycopy(oemPassword, 0, keyBytes, 0, length);
final Key lowKey = createDESKey(keyBytes, 0);
final Key highKey = createDESKey(keyBytes, 7);
final Cipher des = Cipher.getInstance("DES/ECB/NoPadding");
des.init(Cipher.ENCRYPT_MODE, lowKey);
final byte[] lowHash = des.doFinal(MAGIC_CONSTANT);
des.init(Cipher.ENCRYPT_MODE, highKey);
final byte[] highHash = des.doFinal(MAGIC_CONSTANT);
final byte[] lmHash = new byte[16];
System.arraycopy(lowHash, 0, lmHash, 0, 8);
System.arraycopy(highHash, 0, lmHash, 8, 8);
return lmHash;
} catch (final Exception e) {
throw new NtlmEngineException(e.getMessage(), e);
}
}
/**
* Creates the NTLM Hash of the user's password.
*
* @param password The password.
* @return The NTLM Hash of the given password, used in the calculation of
* the NTLM Response and the NTLMv2 and LMv2 Hashes.
*/
private static byte[] ntlmHash(final String password) {
final byte[] unicodePassword = password.getBytes(UNICODE_LITTLE_UNMARKED);
final MD4 md4 = new MD4();
md4.update(unicodePassword);
return md4.getOutput();
}
/**
* Creates the LMv2 Hash of the user's password.
*
* @return The LMv2 Hash, used in the calculation of the NTLMv2 and LMv2
* Responses.
*/
private static byte[] lmv2Hash(final String domain, final String user, final byte[] ntlmHash) {
final HMACMD5 hmacMD5 = new HMACMD5(ntlmHash);
// Upper case username, upper case domain!
hmacMD5.update(user.toUpperCase(Locale.ROOT).getBytes(UNICODE_LITTLE_UNMARKED));
if (domain != null) {
hmacMD5.update(domain.toUpperCase(Locale.ROOT).getBytes(UNICODE_LITTLE_UNMARKED));
}
return hmacMD5.getOutput();
}
/**
* Creates the NTLMv2 Hash of the user's password.
*
* @return The NTLMv2 Hash, used in the calculation of the NTLMv2 and LMv2
* Responses.
*/
private static byte[] ntlmv2Hash(final String domain, final String user, final byte[] ntlmHash) {
final HMACMD5 hmacMD5 = new HMACMD5(ntlmHash);
// Upper case username, mixed case target!!
hmacMD5.update(user.toUpperCase(Locale.ROOT).getBytes(UNICODE_LITTLE_UNMARKED));
if (domain != null) {
hmacMD5.update(domain.getBytes(UNICODE_LITTLE_UNMARKED));
}
return hmacMD5.getOutput();
}
/**
* Creates the LM Response from the given hash and Type 2 challenge.
*
* @param hash The LM or NTLM Hash.
* @param challenge The server challenge from the Type 2 message.
* @return The response (either LM or NTLM, depending on the provided hash).
*/
private static byte[] lmResponse(final byte[] hash, final byte[] challenge) {
try {
final byte[] keyBytes = new byte[21];
System.arraycopy(hash, 0, keyBytes, 0, 16);
final Key lowKey = createDESKey(keyBytes, 0);
final Key middleKey = createDESKey(keyBytes, 7);
final Key highKey = createDESKey(keyBytes, 14);
final Cipher des = Cipher.getInstance("DES/ECB/NoPadding");
des.init(Cipher.ENCRYPT_MODE, lowKey);
final byte[] lowResponse = des.doFinal(challenge);
des.init(Cipher.ENCRYPT_MODE, middleKey);
final byte[] middleResponse = des.doFinal(challenge);
des.init(Cipher.ENCRYPT_MODE, highKey);
final byte[] highResponse = des.doFinal(challenge);
final byte[] lmResponse = new byte[24];
System.arraycopy(lowResponse, 0, lmResponse, 0, 8);
System.arraycopy(middleResponse, 0, lmResponse, 8, 8);
System.arraycopy(highResponse, 0, lmResponse, 16, 8);
return lmResponse;
} catch (final Exception e) {
throw new NtlmEngineException(e.getMessage(), e);
}
}
/**
* Creates the LMv2 Response from the given hash, client data, and Type 2
* challenge.
*
* @param hash The NTLMv2 Hash.
* @param clientData The client data (blob or client challenge).
* @param challenge The server challenge from the Type 2 message.
* @return The response (either NTLMv2 or LMv2, depending on the client
* data).
*/
private static byte[] lmv2Response(final byte[] hash, final byte[] challenge, final byte[] clientData) {
final HMACMD5 hmacMD5 = new HMACMD5(hash);
hmacMD5.update(challenge);
hmacMD5.update(clientData);
final byte[] mac = hmacMD5.getOutput();
final byte[] lmv2Response = new byte[mac.length + clientData.length];
System.arraycopy(mac, 0, lmv2Response, 0, mac.length);
System.arraycopy(clientData, 0, lmv2Response, mac.length, clientData.length);
return lmv2Response;
}
/**
* Creates the NTLMv2 blob from the given target information block and
* client challenge.
*
* @param targetInformation The target information block from the Type 2 message.
* @param clientChallenge The random 8-byte client challenge.
* @return The blob, used in the calculation of the NTLMv2 Response.
*/
private static byte[] createBlob(final byte[] clientChallenge, final byte[] targetInformation, final byte[] timestamp) {
final byte[] blobSignature = {(byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00};
final byte[] reserved = {(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00};
final byte[] unknown1 = {(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00};
final byte[] unknown2 = {(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00};
final byte[] blob = new byte[blobSignature.length + reserved.length + timestamp.length + 8 + unknown1.length
+ targetInformation.length + unknown2.length];
int offset = 0;
System.arraycopy(blobSignature, 0, blob, offset, blobSignature.length);
offset += blobSignature.length;
System.arraycopy(reserved, 0, blob, offset, reserved.length);
offset += reserved.length;
System.arraycopy(timestamp, 0, blob, offset, timestamp.length);
offset += timestamp.length;
System.arraycopy(clientChallenge, 0, blob, offset, 8);
offset += 8;
System.arraycopy(unknown1, 0, blob, offset, unknown1.length);
offset += unknown1.length;
System.arraycopy(targetInformation, 0, blob, offset, targetInformation.length);
offset += targetInformation.length;
System.arraycopy(unknown2, 0, blob, offset, unknown2.length);
return blob;
}
/**
* Creates a DES encryption key from the given key material.
*
* @param bytes A byte array containing the DES key material.
* @param offset The offset in the given byte array at which the 7-byte key
* material starts.
* @return A DES encryption key created from the key material starting at
* the specified offset in the given byte array.
*/
private static Key createDESKey(final byte[] bytes, final int offset) {
final byte[] keyBytes = new byte[7];
System.arraycopy(bytes, offset, keyBytes, 0, 7);
final byte[] material = new byte[8];
material[0] = keyBytes[0];
material[1] = (byte) (keyBytes[0] << 7 | (keyBytes[1] & 0xff) >>> 1);
material[2] = (byte) (keyBytes[1] << 6 | (keyBytes[2] & 0xff) >>> 2);
material[3] = (byte) (keyBytes[2] << 5 | (keyBytes[3] & 0xff) >>> 3);
material[4] = (byte) (keyBytes[3] << 4 | (keyBytes[4] & 0xff) >>> 4);
material[5] = (byte) (keyBytes[4] << 3 | (keyBytes[5] & 0xff) >>> 5);
material[6] = (byte) (keyBytes[5] << 2 | (keyBytes[6] & 0xff) >>> 6);
material[7] = (byte) (keyBytes[6] << 1);
oddParity(material);
return new SecretKeySpec(material, "DES");
}
/**
* Applies odd parity to the given byte array.
*
* @param bytes The data whose parity bits are to be adjusted for odd parity.
*/
private static void oddParity(final byte[] bytes) {
for (int i = 0; i < bytes.length; i++) {
final byte b = bytes[i];
final boolean needsParity = ((b >>> 7 ^ b >>> 6 ^ b >>> 5 ^ b >>> 4 ^ b >>> 3 ^ b >>> 2 ^ b >>> 1) & 0x01) == 0;
if (needsParity) {
bytes[i] |= 0x01;
} else {
bytes[i] &= (byte) 0xfe;
}
}
}
/**
* NTLM message generation, base class
*/
private static class NTLMMessage {
private static final byte[] EMPTY_BYTE_ARRAY = {};
/**
* The current response
*/
private byte[] messageContents = EMPTY_BYTE_ARRAY;
/**
* The current output position
*/
private int currentOutputPosition;
/**
* Constructor to use when message contents are not yet known
*/
NTLMMessage() {
}
/**
* Constructor to use when message contents are known
*/
NTLMMessage(final String messageBody, final int expectedType) {
messageContents = Base64.getDecoder().decode(messageBody);
// Look for NTLM message
if (messageContents.length < SIGNATURE.length) {
throw new NtlmEngineException("NTLM message decoding error - packet too short");
}
int i = 0;
while (i < SIGNATURE.length) {
if (messageContents[i] != SIGNATURE[i]) {
throw new NtlmEngineException("NTLM message expected - instead got unrecognized bytes");
}
i++;
}
// Check to be sure there's a type 2 message indicator next
final int type = readULong(SIGNATURE.length);
if (type != expectedType) {
throw new NtlmEngineException("NTLM type " + expectedType + " message expected - instead got type "
+ type);
}
currentOutputPosition = messageContents.length;
}
/**
* Get the length of the signature and flags, so calculations can adjust
* offsets accordingly.
*/
protected int getPreambleLength() {
return SIGNATURE.length + 4;
}
/**
* Get the message length
*/
protected final int getMessageLength() {
return currentOutputPosition;
}
/**
* Read a byte from a position within the message buffer
*/
protected byte readByte(final int position) {
if (messageContents.length < position + 1) {
throw new NtlmEngineException("NTLM: Message too short");
}
return messageContents[position];
}
/**
* Read a bunch of bytes from a position in the message buffer
*/
protected final void readBytes(final byte[] buffer, final int position) {
if (messageContents.length < position + buffer.length) {
throw new NtlmEngineException("NTLM: Message too short");
}
System.arraycopy(messageContents, position, buffer, 0, buffer.length);
}
/**
* Read an ushort from a position within the message buffer
*/
protected int readUShort(final int position) {
return NtlmEngine.readUShort(messageContents, position);
}
/**
* Read an ulong from a position within the message buffer
*/
protected final int readULong(final int position) {
return NtlmEngine.readULong(messageContents, position);
}
/**
* Read a security buffer from a position within the message buffer
*/
protected final byte[] readSecurityBuffer(final int position) {
return NtlmEngine.readSecurityBuffer(messageContents, position);
}
/**
* Prepares the object to create a response of the given length.
*
* @param maxlength the maximum length of the response to prepare, not
* including the type and the signature (which this method
* adds).
*/
protected void prepareResponse(final int maxlength, final int messageType) {
messageContents = new byte[maxlength];
currentOutputPosition = 0;
addBytes(SIGNATURE);
addULong(messageType);
}
/**
* Adds the given byte to the response.
*
* @param b the byte to add.
*/
protected void addByte(final byte b) {
messageContents[currentOutputPosition] = b;
currentOutputPosition++;
}
/**
* Adds the given bytes to the response.
*
* @param bytes the bytes to add.
*/
protected void addBytes(final byte @Nullable [] bytes) {
if (bytes == null) {
return;
}
for (final byte b : bytes) {
messageContents[currentOutputPosition] = b;
currentOutputPosition++;
}
}
/**
* Adds a USHORT to the response
*/
protected void addUShort(final int value) {
addByte((byte) (value & 0xff));
addByte((byte) (value >> 8 & 0xff));
}
/**
* Adds a ULong to the response
*/
protected void addULong(final int value) {
addByte((byte) (value & 0xff));
addByte((byte) (value >> 8 & 0xff));
addByte((byte) (value >> 16 & 0xff));
addByte((byte) (value >> 24 & 0xff));
}
/**
* Returns the response that has been generated after shrinking the
* array if required and base64 encodes the response.
*
* @return The response as above.
*/
String getResponse() {
final byte[] resp;
if (messageContents.length > currentOutputPosition) {
final byte[] tmp = new byte[currentOutputPosition];
System.arraycopy(messageContents, 0, tmp, 0, currentOutputPosition);
resp = tmp;
} else {
resp = messageContents;
}
return Base64.getEncoder().encodeToString(resp);
}
}
/**
* Type 1 message assembly class
*/
private static class Type1Message extends NTLMMessage {
/**
* Getting the response involves building the message before returning
* it
*/
@Override
String getResponse() {
// Now, build the message. Calculate its length first, including
// signature or type.
final int finalLength = 32 + 8;
// Set up the response. This will initialize the signature, message
// type, and flags.
prepareResponse(finalLength, 1);
// Flags. These are the complete set of flags we support.
addULong(
//FLAG_WORKSTATION_PRESENT |
//FLAG_DOMAIN_PRESENT |
// Required flags
//FLAG_REQUEST_LAN_MANAGER_KEY |
FLAG_REQUEST_NTLMv1 | FLAG_REQUEST_NTLM2_SESSION |
// Protocol version request
FLAG_REQUEST_VERSION |
// Recommended privacy settings
FLAG_REQUEST_ALWAYS_SIGN |
//FLAG_REQUEST_SEAL |
//FLAG_REQUEST_SIGN |
// These must be set according to documentation, based on use of SEAL above
FLAG_REQUEST_128BIT_KEY_EXCH | FLAG_REQUEST_56BIT_ENCRYPTION |
//FLAG_REQUEST_EXPLICIT_KEY_EXCH |
FLAG_REQUEST_UNICODE_ENCODING);
// Domain length (two times).
addUShort(0);
addUShort(0);
// Domain offset.
addULong(finalLength);
// Host length (two times).
addUShort(0);
addUShort(0);
// Host offset (always 32 + 8).
addULong(finalLength);