1 /*
   2  * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package sun.security.provider;
  27 
  28 import sun.security.pkcs12.PKCS12KeyStore;
  29 import sun.security.util.KeyStoreDelegator;
  30 import sun.security.x509.X509CertImpl;
  31 
  32 import java.io.BufferedReader;
  33 import java.io.IOException;
  34 import java.io.InputStream;
  35 import java.io.InputStreamReader;
  36 import java.io.OutputStream;
  37 import java.nio.charset.StandardCharsets;
  38 import java.security.Key;
  39 import java.security.KeyStore;
  40 import java.security.KeyStoreSpi;
  41 import java.security.cert.Certificate;
  42 import java.security.cert.CertificateException;
  43 import java.security.cert.X509Certificate;
  44 import java.util.Base64;
  45 import java.util.Collections;
  46 import java.util.Date;
  47 import java.util.Enumeration;
  48 import java.util.HashSet;
  49 import java.util.LinkedHashMap;
  50 import java.util.List;
  51 import java.util.Map;
  52 import java.util.Objects;
  53 import java.util.Set;
  54 
  55 /**
  56  * This class provides the keystore implementation referred to as "PEM".
  57  *
  58  * @see java.security.KeyStoreSpi
  59  * @since 14
  60  */
  61 
  62 public class PemKeyStore extends KeyStoreSpi {
  63 
  64     // special keystore that supports JKS/PKCS12/PEM file formats
  65     public static final class MultipleFormatPEM extends KeyStoreDelegator {
  66         public MultipleFormatPEM() {
  67             super("PEM", PemKeyStore.class,
  68                     List.of("JKS", "PKCS12"),
  69                     List.of(JavaKeyStore.JKS.class, PKCS12KeyStore.class));
  70         }
  71     }
  72 
  73     private Map<String,Certificate> certs = new LinkedHashMap<>();
  74     private Map<String, Set<KeyStore.Entry.Attribute>> attrs = new LinkedHashMap<>();
  75 
  76     @Override
  77     public Key engineGetKey(String alias, char[] password) {
  78         return null;
  79     }
  80 
  81     @Override
  82     public Certificate[] engineGetCertificateChain(String alias) {
  83         return null;
  84     }
  85 
  86     @Override
  87     public Certificate engineGetCertificate(String alias) {
  88         return certs.get(alias);
  89     }
  90 
  91     @Override
  92     public Date engineGetCreationDate(String alias) {
  93         Certificate c = certs.get(alias);
  94         if (c instanceof X509Certificate) {
  95             return ((X509Certificate) c).getNotBefore();
  96         } else {
  97             return null;
  98         }
  99     }
 100 
 101     @Override
 102     public void engineSetKeyEntry(
 103             String alias, Key key, char[] password, Certificate[] chain) {
 104         throw new UnsupportedOperationException("cannot store key");
 105     }
 106 
 107     @Override
 108     public void engineSetKeyEntry(String alias, byte[] key, Certificate[] chain) {
 109         throw new UnsupportedOperationException("cannot store key");
 110     }
 111 
 112     @Override
 113     public void engineSetCertificateEntry(String alias, Certificate cert) {
 114         certs.put(alias, cert);
 115     }
 116 
 117     @Override
 118     public void engineDeleteEntry(String alias) {
 119         certs.remove(alias);
 120     }
 121 
 122     @Override
 123     public Enumeration<String> engineAliases() {
 124         return Collections.enumeration(certs.keySet());
 125     }
 126 
 127     @Override
 128     public boolean engineContainsAlias(String alias) {
 129         return certs.containsKey(alias);
 130     }
 131 
 132     @Override
 133     public int engineSize() {
 134         return certs.size();
 135     }
 136 
 137     @Override
 138     public boolean engineIsKeyEntry(String alias) {
 139         return false;
 140     }
 141 
 142     @Override
 143     public boolean engineIsCertificateEntry(String alias) {
 144         return engineContainsAlias(alias);
 145     }
 146 
 147     @Override
 148     public String engineGetCertificateAlias(Certificate cert) {
 149         for (var entry : certs.entrySet()) {
 150             if (entry.getValue().equals(cert)) {
 151                 return entry.getKey();
 152             }
 153         }
 154         return null;
 155     }
 156 
 157     @Override
 158     public void engineStore(OutputStream stream, char[] password) {
 159         throw new UnsupportedOperationException("cannot store keystore");
 160     }
 161 
 162     @Override
 163     public void engineLoad(InputStream stream, char[] password)
 164             throws IOException, CertificateException {
 165 
 166         String alias = null;
 167         StringBuilder block = new StringBuilder();
 168         Set<KeyStore.Entry.Attribute> attr = new HashSet<>();
 169 
 170         boolean inPEM = false;
 171 
 172         try (BufferedReader r = new BufferedReader(
 173                 new InputStreamReader(stream, StandardCharsets.UTF_8))) {
 174             while (true) {
 175                 String s = r.readLine();
 176                 if (s == null) {
 177                     break;
 178                 }
 179                 s = s.trim();
 180                 if (s.equals(X509Factory.BEGIN_CERT)) {
 181                     inPEM = true;
 182                 } else if (s.equals(X509Factory.END_CERT)) {
 183                     byte[] data = Base64.getMimeDecoder().decode(block.toString());
 184                     X509CertImpl c = new X509CertImpl(data);
 185                     if (alias == null) {
 186                         alias = c.getFingerprint("SHA-256");
 187                     }
 188                     certs.put(alias, c);
 189                     attrs.put(alias, attr);
 190                     alias = null;
 191                     block.setLength(0);
 192                     attr = new HashSet<>(); // must recreate
 193                     inPEM = false;
 194                 } else if (inPEM) {
 195                     block.append(s);
 196                 } else if (s.charAt(0) == '@') {
 197                     String[] kv = s.substring(1).split(":\\s*", 2);
 198                     if (kv[0].equals("alias") && kv.length == 2) {
 199                         alias = kv[1];
 200                     } else {
 201                         attr.add(new PemAttribute(
 202                                 kv[0], kv.length == 2 ? kv[1] : "true"));
 203                     }
 204                 }
 205             }
 206         }
 207     }
 208 
 209     @Override
 210     public KeyStore.Entry engineGetEntry(
 211             String alias, KeyStore.ProtectionParameter protParam) {
 212         Certificate cert = certs.get(alias);
 213         if (cert == null) {
 214             return null;
 215         } else {
 216             return new KeyStore.TrustedCertificateEntry(cert, attrs.get(alias));
 217         }
 218     }
 219 
 220     @Override
 221     public boolean engineProbe(InputStream stream) throws IOException {
 222         Objects.requireNonNull(stream);
 223         // Ensure the file starts with 5 readable text chars
 224         return isReadable(stream.read())
 225                 && isReadable(stream.read())
 226                 && isReadable(stream.read())
 227                 && isReadable(stream.read())
 228                 && isReadable(stream.read());
 229     }
 230 
 231     private static boolean isReadable(int i) {
 232         return i >= 9 && i <= 13 || i > 31 && i < 128;
 233     }
 234 
 235     private static class PemAttribute implements KeyStore.Entry.Attribute {
 236 
 237         private final String name, value;
 238 
 239         public PemAttribute(String name, String value) {
 240             this.name = Objects.requireNonNull(name);
 241             this.value = Objects.requireNonNull(value);
 242         }
 243 
 244         @Override
 245         public String getName() {
 246             return name;
 247         }
 248 
 249         @Override
 250         public String getValue() {
 251             return value;
 252         }
 253 
 254         @Override
 255         public int hashCode() {
 256             return Objects.hash(name, value);
 257         }
 258 
 259         @Override
 260         public boolean equals(Object obj) {
 261             return Objects.equals(name, value);
 262         }
 263     }
 264 }