/
KeyUtils.java
1287 lines (1152 loc) · 55.5 KB
/
KeyUtils.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.
*/
package org.apache.sshd.common.config.keys;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.DSAKey;
import java.security.interfaces.DSAParams;
import java.security.interfaces.DSAPrivateKey;
import java.security.interfaces.DSAPublicKey;
import java.security.interfaces.ECKey;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAKey;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.DSAPublicKeySpec;
import java.security.spec.ECParameterSpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NavigableSet;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.sshd.common.Factory;
import org.apache.sshd.common.cipher.ECCurves;
import org.apache.sshd.common.config.keys.impl.DSSPublicKeyEntryDecoder;
import org.apache.sshd.common.config.keys.impl.ECDSAPublicKeyEntryDecoder;
import org.apache.sshd.common.config.keys.impl.OpenSSHCertificateDecoder;
import org.apache.sshd.common.config.keys.impl.RSAPublicKeyDecoder;
import org.apache.sshd.common.config.keys.impl.SkECDSAPublicKeyEntryDecoder;
import org.apache.sshd.common.config.keys.impl.SkED25519PublicKeyEntryDecoder;
import org.apache.sshd.common.config.keys.u2f.SkED25519PublicKey;
import org.apache.sshd.common.config.keys.u2f.SkEcdsaPublicKey;
import org.apache.sshd.common.digest.BuiltinDigests;
import org.apache.sshd.common.digest.Digest;
import org.apache.sshd.common.digest.DigestFactory;
import org.apache.sshd.common.digest.DigestUtils;
import org.apache.sshd.common.keyprovider.KeyPairProvider;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.MapEntryUtils.MapBuilder;
import org.apache.sshd.common.util.MapEntryUtils.NavigableMapBuilder;
import org.apache.sshd.common.util.OsUtils;
import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.common.util.buffer.Buffer;
import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
import org.apache.sshd.common.util.io.IoUtils;
import org.apache.sshd.common.util.security.SecurityUtils;
/**
* Utility class for keys
*
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
public final class KeyUtils {
/**
* Name of algorithm for RSA keys to be used when calling security provider
*/
public static final String RSA_ALGORITHM = "RSA";
/**
* The most commonly used RSA public key exponent
*/
public static final BigInteger DEFAULT_RSA_PUBLIC_EXPONENT = BigInteger.valueOf(65537);
/**
* Name of algorithm for DSS keys to be used when calling security provider
*/
public static final String DSS_ALGORITHM = "DSA";
/**
* Name of algorithm for EC keys to be used when calling security provider
*/
public static final String EC_ALGORITHM = "EC";
/**
* The {@link Set} of {@link PosixFilePermission} <U>not</U> allowed if strict permissions are enforced on key files
*/
public static final Set<PosixFilePermission> STRICTLY_PROHIBITED_FILE_PERMISSION = Collections.unmodifiableSet(
EnumSet.of(
PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE, PosixFilePermission.GROUP_EXECUTE,
PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_WRITE, PosixFilePermission.OTHERS_EXECUTE));
/**
* System property that can be used to control the default fingerprint factory used for keys. If not set the
* {@link #DEFAULT_FINGERPRINT_DIGEST_FACTORY} is used
*/
public static final String KEY_FINGERPRINT_FACTORY_PROP = "org.apache.sshd.keyFingerprintFactory";
/**
* The default {@link Factory} of {@link Digest}s initialized as the value of
* {@link #getDefaultFingerPrintFactory()} if not overridden by {@link #KEY_FINGERPRINT_FACTORY_PROP} or
* {@link #setDefaultFingerPrintFactory(DigestFactory)}
*/
public static final DigestFactory DEFAULT_FINGERPRINT_DIGEST_FACTORY = BuiltinDigests.sha256;
/** @see <A HREF="">https://tools.ietf.org/html/rfc8332#section-3</A> */
public static final String RSA_SHA256_KEY_TYPE_ALIAS = "rsa-sha2-256";
public static final String RSA_SHA512_KEY_TYPE_ALIAS = "rsa-sha2-512";
public static final String RSA_SHA256_CERT_TYPE_ALIAS = "rsa-sha2-256-cert-v01@openssh.com";
public static final String RSA_SHA512_CERT_TYPE_ALIAS = "rsa-sha2-512-cert-v01@openssh.com";
private static final AtomicReference<DigestFactory> DEFAULT_DIGEST_HOLDER = new AtomicReference<>();
private static final Map<String, PublicKeyEntryDecoder<?, ?>> BY_KEY_TYPE_DECODERS_MAP
= new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
// order matters here, when, for example, SkED25519PublicKeyEntryDecoder is used, it registers the PrivateKey
// type as java.security.PrivateKey, and all PrivateKey types are assignable to this, so it must be consulted last
private static final Map<Class<?>, PublicKeyEntryDecoder<?, ?>> BY_KEY_CLASS_DECODERS_MAP = new LinkedHashMap<>();
private static final Map<String, String> KEY_TYPE_ALIASES
= NavigableMapBuilder.<String, String> builder(String.CASE_INSENSITIVE_ORDER)
.put(RSA_SHA256_KEY_TYPE_ALIAS, KeyPairProvider.SSH_RSA)
.put(RSA_SHA512_KEY_TYPE_ALIAS, KeyPairProvider.SSH_RSA)
.put(RSA_SHA256_CERT_TYPE_ALIAS, KeyPairProvider.SSH_RSA_CERT)
.put(RSA_SHA512_CERT_TYPE_ALIAS, KeyPairProvider.SSH_RSA_CERT)
.build();
private static final Map<String, String> SIGNATURE_ALGORITHM_MAP
= MapBuilder.<String, String> builder()
.put(RSA_SHA256_CERT_TYPE_ALIAS, RSA_SHA256_KEY_TYPE_ALIAS)
.put(RSA_SHA512_CERT_TYPE_ALIAS, RSA_SHA512_KEY_TYPE_ALIAS)
.put(KeyPairProvider.SSH_RSA_CERT, KeyPairProvider.SSH_RSA)
.put(KeyPairProvider.SSH_DSS_CERT, KeyPairProvider.SSH_DSS)
.put(KeyPairProvider.SSH_ED25519_CERT, KeyPairProvider.SSH_ED25519)
.put(KeyPairProvider.SSH_ECDSA_SHA2_NISTP256_CERT, KeyPairProvider.ECDSA_SHA2_NISTP256)
.put(KeyPairProvider.SSH_ECDSA_SHA2_NISTP384_CERT, KeyPairProvider.ECDSA_SHA2_NISTP384)
.put(KeyPairProvider.SSH_ECDSA_SHA2_NISTP521_CERT, KeyPairProvider.ECDSA_SHA2_NISTP521)
.build();
static {
// order matters here, when, for example, SkED25519PublicKeyEntryDecoder is used, it registers the PrivateKey
// type as java.security.PrivateKey, and all PrivateKey types are assignable to this, so it must be consulted last
registerPublicKeyEntryDecoder(OpenSSHCertificateDecoder.INSTANCE);
registerPublicKeyEntryDecoder(RSAPublicKeyDecoder.INSTANCE);
registerPublicKeyEntryDecoder(DSSPublicKeyEntryDecoder.INSTANCE);
if (SecurityUtils.isECCSupported()) {
registerPublicKeyEntryDecoder(ECDSAPublicKeyEntryDecoder.INSTANCE);
}
if (SecurityUtils.isEDDSACurveSupported()) {
registerPublicKeyEntryDecoder(SecurityUtils.getEDDSAPublicKeyEntryDecoder());
}
// order matters, these must be last since they register their PrivateKey type as java.security.PrivateKey
// there is logical code which discovers a decoder type by instance assignability to this registered type
if (SecurityUtils.isECCSupported()) {
registerPublicKeyEntryDecoder(SkECDSAPublicKeyEntryDecoder.INSTANCE);
}
if (SecurityUtils.isEDDSACurveSupported()) {
registerPublicKeyEntryDecoder(SkED25519PublicKeyEntryDecoder.INSTANCE);
}
}
private KeyUtils() {
throw new UnsupportedOperationException("No instance");
}
/**
* <P>
* Checks if a path has strict permissions
* </P>
* <UL>
* <LI>
* <P>
* The path may not have {@link PosixFilePermission#OTHERS_EXECUTE} permission
* </P>
* </LI>
*
* <LI>
* <P>
* (For {@code Unix}) The path may not have group or others permissions
* </P>
* </LI>
*
* <LI>
* <P>
* (For {@code Unix}) If the path is a file, then its folder may not have group or others permissions
* </P>
* </LI>
*
* <LI>
* <P>
* The path must be owned by current user.
* </P>
* </LI>
*
* <LI>
* <P>
* (For {@code Unix}) The path may be owned by root.
* </P>
* </LI>
*
* <LI>
* <P>
* (For {@code Unix}) If the path is a file, then its folder must also have valid owner.
* </P>
* </LI>
*
* </UL>
*
* @param path The {@link Path} to be checked - ignored if {@code null} or does not exist
* @param options The {@link LinkOption}s to use to query the file's permissions
* @return The violated permission as {@link SimpleImmutableEntry} where key is a message and value is
* the offending object {@link PosixFilePermission} or {@link String} for owner - {@code null}
* if no violations detected
* @throws IOException If failed to retrieve the permissions
* @see #STRICTLY_PROHIBITED_FILE_PERMISSION
*/
public static SimpleImmutableEntry<String, Object> validateStrictKeyFilePermissions(Path path, LinkOption... options)
throws IOException {
if ((path == null) || (!Files.exists(path, options))) {
return null;
}
/*
* Android permission are not really consistent with standard Linux ones since the device is
* a "single" user O/S but with each application being a different "user". We therefore assume
* that if the application has access to a file, then it is good enough since there is really
* only one user, and we don't have to worry about multi-user access. Furthermore, the SE Linux
* security available on Android seems to be enough of a safeguard against inadvertent or even
* malicious access.
*/
if (OsUtils.isAndroid()) {
return null;
}
Collection<PosixFilePermission> perms = IoUtils.getPermissions(path, options);
if (GenericUtils.isEmpty(perms)) {
return null;
}
if (perms.contains(PosixFilePermission.OTHERS_EXECUTE)) {
PosixFilePermission p = PosixFilePermission.OTHERS_EXECUTE;
return new SimpleImmutableEntry<>(String.format("Permissions violation (%s)", p), p);
}
if (OsUtils.isUNIX()) {
PosixFilePermission p = IoUtils.validateExcludedPermissions(perms, STRICTLY_PROHIBITED_FILE_PERMISSION);
if (p != null) {
return new SimpleImmutableEntry<>(String.format("Permissions violation (%s)", p), p);
}
if (Files.isRegularFile(path, options)) {
Path parent = path.getParent();
p = IoUtils.validateExcludedPermissions(IoUtils.getPermissions(parent, options),
STRICTLY_PROHIBITED_FILE_PERMISSION);
if (p != null) {
return new SimpleImmutableEntry<>(String.format("Parent permissions violation (%s)", p), p);
}
}
}
String owner = IoUtils.getFileOwner(path, options);
if (GenericUtils.isEmpty(owner)) {
// we cannot get owner
// general issue: jvm does not support permissions
// security issue: specific filesystem does not support permissions
return null;
}
String current = OsUtils.getCurrentUser();
Set<String> expected = new HashSet<>();
expected.add(current);
if (OsUtils.isUNIX()) {
// Windows "Administrator" was considered however in Windows most likely a group is used.
expected.add(OsUtils.ROOT_USER);
}
if (!expected.contains(owner)) {
return new SimpleImmutableEntry<>(String.format("Owner violation (%s)", owner), owner);
}
if (OsUtils.isUNIX()) {
if (Files.isRegularFile(path, options)) {
String parentOwner = IoUtils.getFileOwner(path.getParent(), options);
if ((!GenericUtils.isEmpty(parentOwner)) && (!expected.contains(parentOwner))) {
return new SimpleImmutableEntry<>(String.format("Parent owner violation (%s)", parentOwner), parentOwner);
}
}
}
return null;
}
/**
* Reads a single {@link PublicKey} from a public key file.
*
* @param path {@link Path} of the file to read; must not be {@code null}
* @return the {@link PublicKey}, may be {@code null} if the file is empty
* @throws IOException if the file cannot be read or parsed
* @throws GeneralSecurityException if the file contents cannot be read as a single {@link PublicKey}
*/
public static PublicKey loadPublicKey(Path path) throws IOException, GeneralSecurityException {
List<AuthorizedKeyEntry> keys = AuthorizedKeyEntry.readAuthorizedKeys(Objects.requireNonNull(path));
if (GenericUtils.isEmpty(keys)) {
return null;
}
if (keys.size() > 1) {
throw new InvalidKeySpecException("Public key file contains multiple entries: " + path);
}
return keys.get(0).resolvePublicKey(null, PublicKeyEntryResolver.IGNORING);
}
/**
* @param keyType The key type - {@code OpenSSH} name - e.g., {@code ssh-rsa, ssh-dss}
* @param keySize The key size (in bits)
* @return A {@link KeyPair} of the specified type and size
* @throws GeneralSecurityException If failed to generate the key pair
* @see #getPublicKeyEntryDecoder(String)
* @see PublicKeyEntryDecoder#generateKeyPair(int)
*/
public static KeyPair generateKeyPair(String keyType, int keySize) throws GeneralSecurityException {
PublicKeyEntryDecoder<?, ?> decoder = getPublicKeyEntryDecoder(keyType);
if (decoder == null) {
throw new InvalidKeySpecException("No decoder for key type=" + keyType);
}
return decoder.generateKeyPair(keySize);
}
/**
* Performs a deep-clone of the original {@link KeyPair} - i.e., creates <U>new</U> public/private keys that are
* clones of the original one
*
* @param keyType The key type - {@code OpenSSH} name - e.g., {@code ssh-rsa, ssh-dss}
* @param kp The {@link KeyPair} to clone - ignored if {@code null}
* @return The cloned instance
* @throws GeneralSecurityException If failed to clone the pair
*/
public static KeyPair cloneKeyPair(String keyType, KeyPair kp) throws GeneralSecurityException {
PublicKeyEntryDecoder<?, ?> decoder = getPublicKeyEntryDecoder(keyType);
if (decoder == null) {
throw new InvalidKeySpecException("No decoder for key type=" + keyType);
}
return decoder.cloneKeyPair(kp);
}
/**
* @param decoder The decoder to register
* @throws IllegalArgumentException if no decoder or not key type or no supported names for the decoder
* @see PublicKeyEntryDecoder#getPublicKeyType()
* @see PublicKeyEntryDecoder#getSupportedKeyTypes()
*/
public static void registerPublicKeyEntryDecoder(PublicKeyEntryDecoder<?, ?> decoder) {
Objects.requireNonNull(decoder, "No decoder specified");
Class<?> pubType = Objects.requireNonNull(decoder.getPublicKeyType(), "No public key type declared");
Class<?> prvType = Objects.requireNonNull(decoder.getPrivateKeyType(), "No private key type declared");
synchronized (BY_KEY_CLASS_DECODERS_MAP) {
BY_KEY_CLASS_DECODERS_MAP.put(pubType, decoder);
BY_KEY_CLASS_DECODERS_MAP.put(prvType, decoder);
}
registerPublicKeyEntryDecoderKeyTypes(decoder);
}
/**
* Registers the specified decoder for all the types it {@link PublicKeyEntryDecoder#getSupportedKeyTypes()
* supports}
*
* @param decoder The (never {@code null}) {@link PublicKeyEntryDecoder decoder} to register
* @see #registerPublicKeyEntryDecoderForKeyType(String, PublicKeyEntryDecoder)
*/
public static void registerPublicKeyEntryDecoderKeyTypes(PublicKeyEntryDecoder<?, ?> decoder) {
Objects.requireNonNull(decoder, "No decoder specified");
Collection<String> names
= ValidateUtils.checkNotNullAndNotEmpty(decoder.getSupportedKeyTypes(), "No supported key types");
for (String n : names) {
PublicKeyEntryDecoder<?, ?> prev = registerPublicKeyEntryDecoderForKeyType(n, decoder);
if (prev != null) {
// noinspection UnnecessaryContinue
continue; // debug breakpoint
}
}
}
/**
* @param keyType The key (never {@code null}/empty) key type
* @param decoder The (never {@code null}) {@link PublicKeyEntryDecoder decoder} to register
* @return The previously registered decoder for this key type - {@code null} if none
*/
public static PublicKeyEntryDecoder<?, ?> registerPublicKeyEntryDecoderForKeyType(
String keyType, PublicKeyEntryDecoder<?, ?> decoder) {
keyType = ValidateUtils.checkNotNullAndNotEmpty(keyType, "No key type specified");
Objects.requireNonNull(decoder, "No decoder specified");
synchronized (BY_KEY_TYPE_DECODERS_MAP) {
return BY_KEY_TYPE_DECODERS_MAP.put(keyType, decoder);
}
}
/**
* @param decoder The (never {@code null}) {@link PublicKeyEntryDecoder decoder} to unregister
* @return The case <U>insensitive</U> {@link NavigableSet} of all the effectively un-registered key types
* out of all the {@link PublicKeyEntryDecoder#getSupportedKeyTypes() supported} ones.
* @see #unregisterPublicKeyEntryDecoderKeyTypes(PublicKeyEntryDecoder)
*/
public static NavigableSet<String> unregisterPublicKeyEntryDecoder(PublicKeyEntryDecoder<?, ?> decoder) {
Objects.requireNonNull(decoder, "No decoder specified");
Class<?> pubType = Objects.requireNonNull(decoder.getPublicKeyType(), "No public key type declared");
Class<?> prvType = Objects.requireNonNull(decoder.getPrivateKeyType(), "No private key type declared");
synchronized (BY_KEY_CLASS_DECODERS_MAP) {
BY_KEY_CLASS_DECODERS_MAP.remove(pubType);
BY_KEY_CLASS_DECODERS_MAP.remove(prvType);
}
return unregisterPublicKeyEntryDecoderKeyTypes(decoder);
}
/**
* Unregisters the specified decoder for all the types it supports
*
* @param decoder The (never {@code null}) {@link PublicKeyEntryDecoder decoder} to unregister
* @return The case <U>insensitive</U> {@link NavigableSet} of all the effectively un-registered key types
* out of all the {@link PublicKeyEntryDecoder#getSupportedKeyTypes() supported} ones.
* @see #unregisterPublicKeyEntryDecoderForKeyType(String)
*/
public static NavigableSet<String> unregisterPublicKeyEntryDecoderKeyTypes(PublicKeyEntryDecoder<?, ?> decoder) {
Objects.requireNonNull(decoder, "No decoder specified");
Collection<String> names = ValidateUtils.checkNotNullAndNotEmpty(
decoder.getSupportedKeyTypes(), "No supported key types");
NavigableSet<String> removed = Collections.emptyNavigableSet();
for (String n : names) {
PublicKeyEntryDecoder<?, ?> prev = unregisterPublicKeyEntryDecoderForKeyType(n);
if (prev == null) {
continue;
}
if (removed.isEmpty()) {
removed = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
}
if (!removed.add(n)) {
// noinspection UnnecessaryContinue
continue; // debug breakpoint
}
}
return removed;
}
/**
* Unregister the decoder registered for the specified key type
*
* @param keyType The key (never {@code null}/empty) key type
* @return The unregistered {@link PublicKeyEntryDecoder} - {@code null} if none registered for this key
* type
*/
public static PublicKeyEntryDecoder<?, ?> unregisterPublicKeyEntryDecoderForKeyType(String keyType) {
keyType = ValidateUtils.checkNotNullAndNotEmpty(keyType, "No key type specified");
synchronized (BY_KEY_TYPE_DECODERS_MAP) {
return BY_KEY_TYPE_DECODERS_MAP.remove(keyType);
}
}
/**
* @param keyType The {@code OpenSSH} key type string - e.g., {@code ssh-rsa, ssh-dss} - ignored if
* {@code null}/empty
* @return The registered {@link PublicKeyEntryDecoder} or {code null} if not found
*/
public static PublicKeyEntryDecoder<?, ?> getPublicKeyEntryDecoder(String keyType) {
if (GenericUtils.isEmpty(keyType)) {
return null;
}
synchronized (BY_KEY_TYPE_DECODERS_MAP) {
return BY_KEY_TYPE_DECODERS_MAP.get(keyType);
}
}
/**
* @param kp The {@link KeyPair} to examine - ignored if {@code null}
* @return The matching {@link PublicKeyEntryDecoder} provided <U>both</U> the public and private keys have the
* same decoder - {@code null} if no match found
* @see #getPublicKeyEntryDecoder(Key)
*/
public static PublicKeyEntryDecoder<?, ?> getPublicKeyEntryDecoder(KeyPair kp) {
if (kp == null) {
return null;
}
PublicKeyEntryDecoder<?, ?> d1 = getPublicKeyEntryDecoder(kp.getPublic());
PublicKeyEntryDecoder<?, ?> d2 = getPublicKeyEntryDecoder(kp.getPrivate());
if (d1 == d2) {
return d1;
} else {
return null; // some kind of mixed keys...
}
}
/**
* @param key The {@link Key} (public or private) - ignored if {@code null}
* @return The registered {@link PublicKeyEntryDecoder} for this key or {code null} if no match found
* @see #getPublicKeyEntryDecoder(Class)
*/
public static PublicKeyEntryDecoder<?, ?> getPublicKeyEntryDecoder(Key key) {
if (key == null) {
return null;
} else {
return getPublicKeyEntryDecoder(key.getClass());
}
}
/**
* @param keyType The key {@link Class} - ignored if {@code null} or not a {@link Key} compatible type
* @return The registered {@link PublicKeyEntryDecoder} or {code null} if no match found
*/
public static PublicKeyEntryDecoder<?, ?> getPublicKeyEntryDecoder(Class<?> keyType) {
if ((keyType == null) || (!Key.class.isAssignableFrom(keyType))) {
return null;
}
synchronized (BY_KEY_TYPE_DECODERS_MAP) {
PublicKeyEntryDecoder<?, ?> decoder = BY_KEY_CLASS_DECODERS_MAP.get(keyType);
if (decoder != null) {
return decoder;
}
// in case it is a derived class
for (PublicKeyEntryDecoder<?, ?> dec : BY_KEY_CLASS_DECODERS_MAP.values()) {
Class<?> pubType = dec.getPublicKeyType();
Class<?> prvType = dec.getPrivateKeyType();
if (pubType.isAssignableFrom(keyType) || prvType.isAssignableFrom(keyType)) {
return dec;
}
}
}
return null;
}
/**
* @return The default {@link DigestFactory} by the {@link #getFingerPrint(PublicKey)} and
* {@link #getFingerPrint(String)} methods
* @see #KEY_FINGERPRINT_FACTORY_PROP
* @see #setDefaultFingerPrintFactory(DigestFactory)
*/
public static DigestFactory getDefaultFingerPrintFactory() {
DigestFactory factory = null;
synchronized (DEFAULT_DIGEST_HOLDER) {
factory = DEFAULT_DIGEST_HOLDER.get();
if (factory != null) {
return factory;
}
String propVal = System.getProperty(KEY_FINGERPRINT_FACTORY_PROP);
if (GenericUtils.isEmpty(propVal)) {
factory = DEFAULT_FINGERPRINT_DIGEST_FACTORY;
} else {
factory = ValidateUtils.checkNotNull(BuiltinDigests.fromFactoryName(propVal), "Unknown digest factory: %s",
propVal);
}
ValidateUtils.checkTrue(factory.isSupported(), "Selected fingerprint digest not supported: %s", factory.getName());
DEFAULT_DIGEST_HOLDER.set(factory);
}
return factory;
}
/**
* @param f The {@link DigestFactory} of {@link Digest}s to be used - may not be {@code null}
*/
public static void setDefaultFingerPrintFactory(DigestFactory f) {
synchronized (DEFAULT_DIGEST_HOLDER) {
DEFAULT_DIGEST_HOLDER.set(Objects.requireNonNull(f, "No digest factory"));
}
}
/**
* @param key the public key - ignored if {@code null}
* @return the fingerprint or {@code null} if no key. <B>Note:</B> if exception encountered then returns the
* exception's simple class name
* @see #getFingerPrint(Factory, PublicKey)
*/
public static String getFingerPrint(PublicKey key) {
return getFingerPrint(getDefaultFingerPrintFactory(), key);
}
/**
* @param password The {@link String} to digest - ignored if {@code null}/empty, otherwise its UTF-8 representation
* is used as input for the fingerprint
* @return The fingerprint - {@code null} if {@code null}/empty input. <B>Note:</B> if exception
* encountered then returns the exception's simple class name
* @see #getFingerPrint(String, Charset)
*/
public static String getFingerPrint(String password) {
return getFingerPrint(password, StandardCharsets.UTF_8);
}
/**
* @param password The {@link String} to digest - ignored if {@code null}/empty
* @param charset The {@link Charset} to use in order to convert the string to its byte representation to use as
* input for the fingerprint
* @return The fingerprint - {@code null} if {@code null}/empty input. <B>Note:</B> if exception
* encountered then returns the exception's simple class name
* @see #getFingerPrint(Factory, String, Charset)
* @see #getDefaultFingerPrintFactory()
*/
public static String getFingerPrint(String password, Charset charset) {
return getFingerPrint(getDefaultFingerPrintFactory(), password, charset);
}
/**
* @param f The {@link Factory} to create the {@link Digest} to use
* @param key the public key - ignored if {@code null}
* @return the fingerprint or {@code null} if no key. <B>Note:</B> if exception encountered then returns the
* exception's simple class name
* @see #getFingerPrint(Digest, PublicKey)
*/
public static String getFingerPrint(Factory<? extends Digest> f, PublicKey key) {
return (key == null) ? null : getFingerPrint(Objects.requireNonNull(f, "No digest factory").create(), key);
}
/**
* @param d The {@link Digest} to use
* @param key the public key - ignored if {@code null}
* @return the fingerprint or {@code null} if no key. <B>Note:</B> if exception encountered then returns the
* exception's simple class name
* @see DigestUtils#getFingerPrint(Digest, byte[], int, int)
*/
public static String getFingerPrint(Digest d, PublicKey key) {
if (key == null) {
return null;
}
try {
Buffer buffer = new ByteArrayBuffer();
buffer.putRawPublicKey(key);
return DigestUtils.getFingerPrint(d, buffer.array(), 0, buffer.wpos());
} catch (Exception e) {
return e.toString();
}
}
public static byte[] getRawFingerprint(PublicKey key) throws Exception {
return getRawFingerprint(getDefaultFingerPrintFactory(), key);
}
public static byte[] getRawFingerprint(Factory<? extends Digest> f, PublicKey key) throws Exception {
return (key == null) ? null : getRawFingerprint(Objects.requireNonNull(f, "No digest factory").create(), key);
}
public static byte[] getRawFingerprint(Digest d, PublicKey key) throws Exception {
if (key == null) {
return null;
}
Buffer buffer = new ByteArrayBuffer();
buffer.putRawPublicKey(key);
return DigestUtils.getRawFingerprint(d, buffer.array(), 0, buffer.wpos());
}
/**
* @param f The {@link Factory} to create the {@link Digest} to use
* @param s The {@link String} to digest - ignored if {@code null}/empty, otherwise its UTF-8 representation is
* used as input for the fingerprint
* @return The fingerprint - {@code null} if {@code null}/empty input. <B>Note:</B> if exception encountered then
* returns the exception's simple class name
* @see #getFingerPrint(Digest, String, Charset)
*/
public static String getFingerPrint(Factory<? extends Digest> f, String s) {
return getFingerPrint(f, s, StandardCharsets.UTF_8);
}
/**
* @param f The {@link Factory} to create the {@link Digest} to use
* @param s The {@link String} to digest - ignored if {@code null}/empty
* @param charset The {@link Charset} to use in order to convert the string to its byte representation to use as
* input for the fingerprint
* @return The fingerprint - {@code null} if {@code null}/empty input <B>Note:</B> if exception encountered
* then returns the exception's simple class name
* @see DigestUtils#getFingerPrint(Digest, String, Charset)
*/
public static String getFingerPrint(Factory<? extends Digest> f, String s, Charset charset) {
return getFingerPrint(f.create(), s, charset);
}
/**
* @param d The {@link Digest} to use
* @param s The {@link String} to digest - ignored if {@code null}/empty, otherwise its UTF-8 representation is
* used as input for the fingerprint
* @return The fingerprint - {@code null} if {@code null}/empty input. <B>Note:</B> if exception encountered then
* returns the exception's simple class name
* @see DigestUtils#getFingerPrint(Digest, String, Charset)
*/
public static String getFingerPrint(Digest d, String s) {
return getFingerPrint(d, s, StandardCharsets.UTF_8);
}
/**
* @param d The {@link Digest} to use to calculate the fingerprint
* @param s The string to digest - ignored if {@code null}/empty
* @param charset The {@link Charset} to use in order to convert the string to its byte representation to use as
* input for the fingerprint
* @return The fingerprint - {@code null} if {@code null}/empty input. <B>Note:</B> if exception encountered
* then returns the exception's simple class name
* @see DigestUtils#getFingerPrint(Digest, String, Charset)
*/
public static String getFingerPrint(Digest d, String s, Charset charset) {
if (GenericUtils.isEmpty(s)) {
return null;
}
try {
return DigestUtils.getFingerPrint(d, s, charset);
} catch (Exception e) {
return e.getClass().getSimpleName();
}
}
/**
* @param expected The expected fingerprint if {@code null} or empty then returns a failure with the default
* fingerprint.
* @param key the {@link PublicKey} - if {@code null} then returns null.
* @return SimpleImmutableEntry<Boolean, String> - key is success indicator, value is actual fingerprint,
* {@code null} if no key.
* @see #getDefaultFingerPrintFactory()
* @see #checkFingerPrint(String, Factory, PublicKey)
*/
public static SimpleImmutableEntry<Boolean, String> checkFingerPrint(String expected, PublicKey key) {
return checkFingerPrint(expected, getDefaultFingerPrintFactory(), key);
}
/**
* @param expected The expected fingerprint if {@code null} or empty then returns a failure with the default
* fingerprint.
* @param f The {@link Factory} to be used to generate the default {@link Digest} for the key
* @param key the {@link PublicKey} - if {@code null} then returns null.
* @return SimpleImmutableEntry<Boolean, String> - key is success indicator, value is actual fingerprint,
* {@code null} if no key.
*/
public static SimpleImmutableEntry<Boolean, String> checkFingerPrint(
String expected, Factory<? extends Digest> f, PublicKey key) {
return checkFingerPrint(expected, Objects.requireNonNull(f, "No digest factory").create(), key);
}
/**
* @param expected The expected fingerprint if {@code null} or empty then returns a failure with the default
* fingerprint.
* @param d The {@link Digest} to be used to generate the default fingerprint for the key
* @param key the {@link PublicKey} - if {@code null} then returns null.
* @return SimpleImmutableEntry<Boolean, String> - key is success indicator, value is actual fingerprint,
* {@code null} if no key.
*/
public static SimpleImmutableEntry<Boolean, String> checkFingerPrint(String expected, Digest d, PublicKey key) {
if (key == null) {
return null;
}
if (GenericUtils.isEmpty(expected)) {
return new SimpleImmutableEntry<>(false, getFingerPrint(d, key));
}
// de-construct fingerprint
int pos = expected.indexOf(':');
if ((pos < 0) || (pos >= (expected.length() - 1))) {
return new SimpleImmutableEntry<>(false, getFingerPrint(d, key));
}
String name = expected.substring(0, pos);
String value = expected.substring(pos + 1);
DigestFactory expectedFactory;
// We know that all digest names have a length > 2 - if 2 (or less) then assume a pure HEX value
if (name.length() > 2) {
expectedFactory = BuiltinDigests.fromFactoryName(name);
if (expectedFactory == null) {
return new SimpleImmutableEntry<>(false, getFingerPrint(d, key));
}
expected = name.toUpperCase() + ":" + value;
} else {
expectedFactory = BuiltinDigests.md5;
expected = expectedFactory.getName().toUpperCase() + ":" + expected;
}
String fingerprint = getFingerPrint(expectedFactory, key);
boolean matches = BuiltinDigests.md5.getName().equals(expectedFactory.getName())
? expected.equalsIgnoreCase(fingerprint) // HEX is case insensitive
: expected.equals(fingerprint);
return new SimpleImmutableEntry<>(matches, fingerprint);
}
/**
* @param kp a key pair - ignored if {@code null}. If the private key is non-{@code null} then it is used to
* determine the type, otherwise the public one is used.
* @return the key type or {@code null} if cannot determine it
* @see #getKeyType(Key)
*/
public static String getKeyType(KeyPair kp) {
if (kp == null) {
return null;
}
PrivateKey key = kp.getPrivate();
if (key != null) {
return getKeyType(key);
} else {
return getKeyType(kp.getPublic());
}
}
/**
* @param key a public or private key
* @return the key type or {@code null} if cannot determine it
*/
public static String getKeyType(Key key) {
if (key == null) {
return null;
} else if (key instanceof DSAKey) {
return KeyPairProvider.SSH_DSS;
} else if (key instanceof RSAKey) {
return KeyPairProvider.SSH_RSA;
} else if (key instanceof ECKey) {
ECKey ecKey = (ECKey) key;
ECParameterSpec ecSpec = ecKey.getParams();
ECCurves curve = ECCurves.fromCurveParameters(ecSpec);
if (curve == null) {
return null; // debug breakpoint
} else {
return curve.getKeyType();
}
} else if (key instanceof SkEcdsaPublicKey) {
return SkECDSAPublicKeyEntryDecoder.KEY_TYPE;
} else if (SecurityUtils.EDDSA.equalsIgnoreCase(key.getAlgorithm())) {
return KeyPairProvider.SSH_ED25519;
} else if (key instanceof SkED25519PublicKey) {
return SkED25519PublicKeyEntryDecoder.KEY_TYPE;
} else if (key instanceof OpenSshCertificate) {
return ((OpenSshCertificate) key).getKeyType();
}
return null;
}
/**
* @param keyType A key type name - ignored if {@code null}/empty
* @return A {@link List} of they canonical key name and all its aliases
* @see #getCanonicalKeyType(String)
*/
public static List<String> getAllEquivalentKeyTypes(String keyType) {
if (GenericUtils.isEmpty(keyType)) {
return Collections.emptyList();
}
String canonicalName = getCanonicalKeyType(keyType);
List<String> equivalents = new ArrayList<>();
equivalents.add(canonicalName);
synchronized (KEY_TYPE_ALIASES) {
for (Map.Entry<String, String> ae : KEY_TYPE_ALIASES.entrySet()) {
String alias = ae.getKey();
String name = ae.getValue();
if (canonicalName.equalsIgnoreCase(name)) {
equivalents.add(alias);
}
}
}
return equivalents;
}
/**
* @param keyType The available key-type - ignored if {@code null}/empty
* @return The canonical key type - same as input if no alias registered for the provided key type
* @see #RSA_SHA256_KEY_TYPE_ALIAS
* @see #RSA_SHA512_KEY_TYPE_ALIAS
*/
public static String getCanonicalKeyType(String keyType) {
if (GenericUtils.isEmpty(keyType)) {
return keyType;
}
String canonicalName;
synchronized (KEY_TYPE_ALIASES) {
canonicalName = KEY_TYPE_ALIASES.get(keyType);
}
if (GenericUtils.isEmpty(canonicalName)) {
return keyType;
}
return canonicalName;
}
/**
* @return A case insensitive {@link NavigableSet} of the currently registered key type "aliases".
* @see #getCanonicalKeyType(String)
*/
public static NavigableSet<String> getRegisteredKeyTypeAliases() {
synchronized (KEY_TYPE_ALIASES) {
return KEY_TYPE_ALIASES.isEmpty()
? Collections.emptyNavigableSet()
: GenericUtils.asSortedSet(String.CASE_INSENSITIVE_ORDER, KEY_TYPE_ALIASES.keySet());
}
}
/**
* Registers a collection of aliases to a canonical key type
*
* @param keyType The (never {@code null}/empty) canonical name
* @param aliases The (never {@code null}/empty) aliases
* @return A {@link List} of the replaced aliases - empty if no previous aliases for the canonical name
*/
public static List<String> registerCanonicalKeyTypes(String keyType, Collection<String> aliases) {
ValidateUtils.checkNotNullAndNotEmpty(keyType, "No key type value");
ValidateUtils.checkNotNullAndNotEmpty(aliases, "No aliases provided");
List<String> replaced = Collections.emptyList();
synchronized (KEY_TYPE_ALIASES) {
for (String a : aliases) {
ValidateUtils.checkNotNullAndNotEmpty(a, "Null/empty alias registration for %s", keyType);
String prev = KEY_TYPE_ALIASES.put(a, keyType);
if (GenericUtils.isEmpty(prev)) {
continue;
}
if (replaced.isEmpty()) {
replaced = new ArrayList<>();
}
replaced.add(prev);
}
}
return replaced;
}
/**
* @param alias The alias to unregister (ignored if {@code null}/empty)
* @return The associated canonical key type - {@code null} if alias not registered
*/
public static String unregisterCanonicalKeyTypeAlias(String alias) {
if (GenericUtils.isEmpty(alias)) {
return alias;
}
synchronized (KEY_TYPE_ALIASES) {
return KEY_TYPE_ALIASES.remove(alias);
}
}
/**
* Determines the key size in bits
*
* @param key The {@link Key} to examine - ignored if {@code null}
* @return The key size - non-positive value if cannot determine it
*/
public static int getKeySize(Key key) {
if (key == null) {