| /* | |
| * Copyright (c) 2003, 2024, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. Oracle designates this | |
| * particular file as subject to the "Classpath" exception as provided | |
| * by Oracle in the LICENSE file that accompanied this code. | |
| * | |
| * This code is distributed in the hope that it will be useful, but WITHOUT | |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| */ | |
| package sun.security.pkcs11; | |
| import java.util.*; | |
| import java.util.concurrent.ConcurrentHashMap; | |
| import java.io.*; | |
| import java.lang.ref.*; | |
| import java.security.*; | |
| import javax.security.auth.login.LoginException; | |
| import sun.security.jca.JCAUtil; | |
| import sun.security.pkcs11.wrapper.*; | |
| import static sun.security.pkcs11.TemplateManager.*; | |
| import static sun.security.pkcs11.wrapper.PKCS11Constants.*; | |
| import static sun.security.pkcs11.wrapper.PKCS11Exception.RV.*; | |
| /** | |
| * PKCS#11 token. | |
| * | |
| * @author Andreas Sterbenz | |
| * @since 1.5 | |
| */ | |
| final class Token implements Serializable { | |
| public enum CTSVariant {CS1, CS2, CS3} | |
| // need to be serializable to allow SecureRandom to be serialized | |
| @Serial | |
| private static final long serialVersionUID = 2541527649100571747L; | |
| // how often to check if the token is still present (in ms) | |
| // this is different from checking if a token has been inserted, | |
| // that is done in SunPKCS11. Currently, 50 ms. | |
| private static final long CHECK_INTERVAL = 50; | |
| final SunPKCS11 provider; | |
| @SuppressWarnings("serial") // Type of field is not Serializable | |
| final PKCS11 p11; | |
| @SuppressWarnings("serial") // Type of field is not Serializable | |
| final Config config; | |
| final transient CTSVariant ctsVariant; | |
| @SuppressWarnings("serial") // Type of field is not Serializable | |
| final CK_TOKEN_INFO tokenInfo; | |
| // session manager to pool sessions | |
| @SuppressWarnings("serial") // Type of field is not Serializable | |
| final SessionManager sessionManager; | |
| // template manager to customize the attributes used when creating objects | |
| @SuppressWarnings("serial") // Type of field is not Serializable | |
| private final TemplateManager templateManager; | |
| // flag indicating whether we need to explicitly cancel operations | |
| // we started on the token. If false, we assume operations are | |
| // automatically cancelled once we start another one | |
| final boolean explicitCancel; | |
| // translation cache for secret keys | |
| @SuppressWarnings("serial") // Type of field is not Serializable | |
| final KeyCache secretCache; | |
| // translation cache for asymmetric keys (public and private) | |
| @SuppressWarnings("serial") // Type of field is not Serializable | |
| final KeyCache privateCache; | |
| // cached instances of the various key factories, initialized on demand | |
| @SuppressWarnings("serial") // Type of field is not Serializable | |
| private volatile P11KeyFactory rsaFactory, dsaFactory, dhFactory, ecFactory; | |
| // table which maps mechanisms to the corresponding cached | |
| // MechanismInfo objects | |
| @SuppressWarnings("serial") // Type of field is not Serializable | |
| private final Map<Long, CK_MECHANISM_INFO> mechInfoMap; | |
| // single SecureRandomSpi instance we use per token | |
| // initialized on demand (if supported) | |
| private volatile P11SecureRandom secureRandom; | |
| // single KeyStoreSpi instance we use per provider | |
| // initialized on demand | |
| @SuppressWarnings("serial") // Type of field is not Serializable | |
| private volatile P11KeyStore keyStore; | |
| // whether this token is a removable token | |
| private final boolean removable; | |
| // for removable tokens: whether this token is valid or has been removed | |
| private volatile boolean valid; | |
| // for removable tokens: time last checked for token presence | |
| private long lastPresentCheck; | |
| // unique token id, used for serialization only | |
| private byte[] tokenId; | |
| // flag indicating whether the token is write protected | |
| private boolean writeProtected; | |
| // flag indicating whether we are logged in | |
| private volatile boolean loggedIn; | |
| // time we last checked login status | |
| private long lastLoginCheck; | |
| // mutex for token-present-check | |
| private static final Object CHECK_LOCK = new Object(); | |
| // object for indicating unsupported mechanism in 'mechInfoMap' | |
| private static final CK_MECHANISM_INFO INVALID_MECH = | |
| new CK_MECHANISM_INFO(0, 0, 0); | |
| // flag indicating whether the token supports raw secret key material import | |
| private Boolean supportsRawSecretKeyImport; | |
| Token(SunPKCS11 provider) throws PKCS11Exception { | |
| this.provider = provider; | |
| this.removable = provider.removable; | |
| this.valid = true; | |
| p11 = provider.p11; | |
| config = provider.config; | |
| tokenInfo = p11.C_GetTokenInfo(provider.slotID); | |
| writeProtected = (tokenInfo.flags & CKF_WRITE_PROTECTED) != 0; | |
| ctsVariant = getCTSVariant(); | |
| // create session manager and open a test session | |
| SessionManager sessionManager; | |
| try { | |
| sessionManager = new SessionManager(this); | |
| Session s = sessionManager.getOpSession(); | |
| sessionManager.releaseSession(s); | |
| } catch (PKCS11Exception e) { | |
| if (writeProtected) { | |
| throw e; | |
| } | |
| // token might not permit RW sessions even though | |
| // CKF_WRITE_PROTECTED is not set | |
| writeProtected = true; | |
| sessionManager = new SessionManager(this); | |
| Session s = sessionManager.getOpSession(); | |
| sessionManager.releaseSession(s); | |
| } | |
| this.sessionManager = sessionManager; | |
| secretCache = new KeyCache(); | |
| privateCache = new KeyCache(); | |
| templateManager = config.getTemplateManager(); | |
| explicitCancel = config.getExplicitCancel(); | |
| mechInfoMap = new ConcurrentHashMap<>(10); | |
| } | |
| boolean isWriteProtected() { | |
| return writeProtected; | |
| } | |
| // return whether the token supports raw secret key material import | |
| boolean supportsRawSecretKeyImport() { | |
| if (supportsRawSecretKeyImport == null) { | |
| SecureRandom random = JCAUtil.getSecureRandom(); | |
| byte[] encoded = new byte[48]; | |
| random.nextBytes(encoded); | |
| CK_ATTRIBUTE[] attributes = new CK_ATTRIBUTE[3]; | |
| attributes[0] = new CK_ATTRIBUTE(CKA_CLASS, CKO_SECRET_KEY); | |
| attributes[1] = new CK_ATTRIBUTE(CKA_KEY_TYPE, CKK_GENERIC_SECRET); | |
| attributes[2] = new CK_ATTRIBUTE(CKA_VALUE, encoded); | |
| Session session = null; | |
| try { | |
| attributes = getAttributes(O_IMPORT, | |
| CKO_SECRET_KEY, CKK_GENERIC_SECRET, attributes); | |
| session = getObjSession(); | |
| long keyID = p11.C_CreateObject(session.id(), attributes); | |
| supportsRawSecretKeyImport = Boolean.TRUE; | |
| } catch (PKCS11Exception e) { | |
| supportsRawSecretKeyImport = Boolean.FALSE; | |
| } finally { | |
| releaseSession(session); | |
| } | |
| } | |
| return supportsRawSecretKeyImport; | |
| } | |
| // return whether we are logged in | |
| // uses cached result if current. session is optional and may be null | |
| boolean isLoggedIn(Session session) throws PKCS11Exception { | |
| // volatile load first | |
| boolean loggedIn = this.loggedIn; | |
| long time = System.currentTimeMillis(); | |
| if (time - lastLoginCheck > CHECK_INTERVAL) { | |
| loggedIn = isLoggedInNow(session); | |
| lastLoginCheck = time; | |
| } | |
| return loggedIn; | |
| } | |
| // return whether we are logged in now | |
| // does not use cache | |
| boolean isLoggedInNow(Session session) throws PKCS11Exception { | |
| boolean allocSession = (session == null); | |
| try { | |
| if (allocSession) { | |
| session = getOpSession(); | |
| } | |
| CK_SESSION_INFO info = p11.C_GetSessionInfo(session.id()); | |
| boolean loggedIn = (info.state == CKS_RO_USER_FUNCTIONS) || | |
| (info.state == CKS_RW_USER_FUNCTIONS); | |
| this.loggedIn = loggedIn; | |
| return loggedIn; | |
| } finally { | |
| if (allocSession) { | |
| releaseSession(session); | |
| } | |
| } | |
| } | |
| // ensure that we are logged in | |
| // call provider.login() if not | |
| void ensureLoggedIn(Session session) throws PKCS11Exception, LoginException { | |
| if (!isLoggedIn(session)) { | |
| provider.login(null, null); | |
| } | |
| } | |
| // return whether this token object is valid (i.e. token not removed) | |
| // returns value from last check, does not perform new check | |
| boolean isValid() { | |
| if (!removable) { | |
| return true; | |
| } | |
| return valid; | |
| } | |
| void ensureValid() { | |
| if (!isValid()) { | |
| throw new ProviderException("Token has been removed"); | |
| } | |
| } | |
| // return whether a token is present (i.e. token not removed) | |
| // returns cached value if current, otherwise performs new check | |
| boolean isPresent(long sessionID) { | |
| if (!removable) { | |
| return true; | |
| } | |
| if (!valid) { | |
| return false; | |
| } | |
| long time = System.currentTimeMillis(); | |
| if ((time - lastPresentCheck) >= CHECK_INTERVAL) { | |
| synchronized (CHECK_LOCK) { | |
| if ((time - lastPresentCheck) >= CHECK_INTERVAL) { | |
| boolean ok = false; | |
| try { | |
| // check if token still present | |
| CK_SLOT_INFO slotInfo = | |
| provider.p11.C_GetSlotInfo(provider.slotID); | |
| if ((slotInfo.flags & CKF_TOKEN_PRESENT) != 0) { | |
| // if the token has been removed and re-inserted, | |
| // the token should return an error | |
| CK_SESSION_INFO sessInfo = | |
| provider.p11.C_GetSessionInfo | |
| (sessionID); | |
| ok = true; | |
| } | |
| } catch (PKCS11Exception e) { | |
| // empty | |
| } | |
| valid = ok; | |
| lastPresentCheck = System.currentTimeMillis(); | |
| if (!ok) { | |
| destroy(); | |
| } | |
| } | |
| } | |
| } | |
| return valid; | |
| } | |
| void destroy() { | |
| secretCache.clear(); | |
| privateCache.clear(); | |
| sessionManager.clearPools(); | |
| provider.uninitToken(this); | |
| valid = false; | |
| } | |
| Session getObjSession() throws PKCS11Exception { | |
| return sessionManager.getObjSession(); | |
| } | |
| Session getOpSession() throws PKCS11Exception { | |
| return sessionManager.getOpSession(); | |
| } | |
| Session releaseSession(Session session) { | |
| return sessionManager.releaseSession(session); | |
| } | |
| Session killSession(Session session) { | |
| return sessionManager.killSession(session); | |
| } | |
| CK_ATTRIBUTE[] getAttributes(String op, long type, long alg, | |
| CK_ATTRIBUTE[] attrs) throws PKCS11Exception { | |
| CK_ATTRIBUTE[] newAttrs = | |
| templateManager.getAttributes(op, type, alg, attrs); | |
| for (CK_ATTRIBUTE attr : newAttrs) { | |
| if (attr.type == CKA_TOKEN) { | |
| if (attr.getBoolean()) { | |
| try { | |
| ensureLoggedIn(null); | |
| } catch (LoginException e) { | |
| throw new ProviderException("Login failed", e); | |
| } | |
| } | |
| // break once we have found a CKA_TOKEN attribute | |
| break; | |
| } | |
| } | |
| return newAttrs; | |
| } | |
| P11KeyFactory getKeyFactory(String algorithm) { | |
| P11KeyFactory f; | |
| if (algorithm.equals("RSA")) { | |
| f = rsaFactory; | |
| if (f == null) { | |
| f = new P11RSAKeyFactory(this, algorithm); | |
| rsaFactory = f; | |
| } | |
| } else if (algorithm.equals("DSA")) { | |
| f = dsaFactory; | |
| if (f == null) { | |
| f = new P11DSAKeyFactory(this, algorithm); | |
| dsaFactory = f; | |
| } | |
| } else if (algorithm.equals("DH")) { | |
| f = dhFactory; | |
| if (f == null) { | |
| f = new P11DHKeyFactory(this, algorithm); | |
| dhFactory = f; | |
| } | |
| } else if (algorithm.equals("EC")) { | |
| f = ecFactory; | |
| if (f == null) { | |
| f = new P11ECKeyFactory(this, algorithm); | |
| ecFactory = f; | |
| } | |
| } else { | |
| throw new ProviderException("Unknown algorithm " + algorithm); | |
| } | |
| return f; | |
| } | |
| P11SecureRandom getRandom() { | |
| if (secureRandom == null) { | |
| secureRandom = new P11SecureRandom(this); | |
| } | |
| return secureRandom; | |
| } | |
| P11KeyStore getKeyStore() { | |
| if (keyStore == null) { | |
| keyStore = new P11KeyStore(this); | |
| } | |
| return keyStore; | |
| } | |
| CK_MECHANISM_INFO getMechanismInfo(long mechanism) throws PKCS11Exception { | |
| CK_MECHANISM_INFO result = mechInfoMap.get(mechanism); | |
| if (result == null) { | |
| try { | |
| result = p11.C_GetMechanismInfo(provider.slotID, | |
| mechanism); | |
| mechInfoMap.put(mechanism, result); | |
| } catch (PKCS11Exception e) { | |
| if (!e.match(CKR_MECHANISM_INVALID)) { | |
| throw e; | |
| } else { | |
| mechInfoMap.put(mechanism, INVALID_MECH); | |
| } | |
| } | |
| } else if (result == INVALID_MECH) { | |
| result = null; | |
| } | |
| return result; | |
| } | |
| private CTSVariant getCTSVariant() { | |
| CTSVariant ctsVariant = config.getCTSVariant(); | |
| if (ctsVariant != null) { | |
| return ctsVariant; | |
| } | |
| // 'cipherTextStealingVariant' needs an explicit value for the | |
| // CKM_AES_CTS mechanism to be enabled. In the case of NSS we know | |
| // that this value is 'CS1', so we can set it for the user. See: | |
| // https://bugzilla.mozilla.org/show_bug.cgi?id=373108#c7 | |
| // https://github.com/nss-dev/nss/blob/NSS_3_99_RTM/lib/freebl/cts.c#L65 | |
| return P11Util.isNSS(this) ? CTSVariant.CS1 : null; | |
| } | |
| private synchronized byte[] getTokenId() { | |
| if (tokenId == null) { | |
| SecureRandom random = JCAUtil.getSecureRandom(); | |
| tokenId = new byte[20]; | |
| random.nextBytes(tokenId); | |
| serializedTokens.add(new WeakReference<>(this)); | |
| } | |
| return tokenId; | |
| } | |
| // list of all tokens that have been serialized within this VM | |
| // NOTE that elements are never removed from this list | |
| // the assumption is that the number of tokens that are serialized | |
| // is relatively small | |
| private static final List<Reference<Token>> serializedTokens = new ArrayList<>(); | |
| @java.io.Serial | |
| private Object writeReplace() throws ObjectStreamException { | |
| if (!isValid()) { | |
| throw new InvalidObjectException("Token has been removed"); | |
| } | |
| return new TokenRep(this); | |
| } | |
| /** | |
| * Restores the state of this object from the stream. | |
| * <p> | |
| * Deserialization of this object is not supported. | |
| * | |
| * @param stream the {@code ObjectInputStream} from which data is read | |
| * @throws IOException if an I/O error occurs | |
| * @throws ClassNotFoundException if a serialized class cannot be loaded | |
| */ | |
| @java.io.Serial | |
| private void readObject(ObjectInputStream stream) | |
| throws IOException, ClassNotFoundException { | |
| throw new InvalidObjectException( | |
| "Tokens are not directly deserializable"); | |
| } | |
| // serialized representation of a token | |
| // tokens can only be de-serialized within the same VM invocation | |
| // and if the token has not been removed in the meantime | |
| private static class TokenRep implements Serializable { | |
| @Serial | |
| private static final long serialVersionUID = 3503721168218219807L; | |
| private final byte[] tokenId; | |
| TokenRep(Token token) { | |
| tokenId = token.getTokenId(); | |
| } | |
| @java.io.Serial | |
| private Object readResolve() throws ObjectStreamException { | |
| for (Reference<Token> tokenRef : serializedTokens) { | |
| Token token = tokenRef.get(); | |
| if ((token != null) && token.isValid()) { | |
| if (Arrays.equals(token.getTokenId(), tokenId)) { | |
| return token; | |
| } | |
| } | |
| } | |
| throw new InvalidObjectException("Could not find token"); | |
| } | |
| } | |
| } |