diff --git a/platform/platform-impl/src/com/intellij/ide/passwordSafe/FilePasswordSafeProvider.kt b/platform/platform-impl/src/com/intellij/ide/passwordSafe/FilePasswordSafeProvider.kt index 01a0aa9d6714..87a66cd1e7ef 100644 --- a/platform/platform-impl/src/com/intellij/ide/passwordSafe/FilePasswordSafeProvider.kt +++ b/platform/platform-impl/src/com/intellij/ide/passwordSafe/FilePasswordSafeProvider.kt @@ -38,7 +38,7 @@ import javax.crypto.spec.SecretKeySpec internal val LOG = Logger.getInstance(FilePasswordSafeProvider::class.java) -class FilePasswordSafeProvider @JvmOverloads constructor(keyToValue: Map? = null, baseDirectory: Path = Paths.get(PathManager.getConfigPath()), var memoryOnly: Boolean = false) : PasswordSafeProvider() { +class FilePasswordSafeProvider @JvmOverloads constructor(keyToValue: Map? = null, baseDirectory: Path = Paths.get(PathManager.getConfigPath()), var memoryOnly: Boolean = false) : PasswordSafeProvider { private val db = ContainerUtil.newConcurrentMap() private val dbFile = baseDirectory.resolve("pdb") @@ -156,11 +156,11 @@ class FilePasswordSafeProvider @JvmOverloads constructor(keyToValue: Map?) = "${if (requestor == null) "" else "${requestor.name}/"}$key" +internal fun getRawKey(key: String?, requestor: Class<*>?) = "${if (requestor == null) "" else "${requestor.name}/"}$key" + +internal fun toOldKey(hash: ByteArray) = "old-hashed-key|" + Base64.getEncoder().encodeToString(hash) internal fun generate(): ByteArray { val bytes = ByteArray(16) diff --git a/platform/platform-impl/src/com/intellij/ide/passwordSafe/MacOsCredentialStore.kt b/platform/platform-impl/src/com/intellij/ide/passwordSafe/MacOsCredentialStore.kt new file mode 100644 index 000000000000..14f99e1587bb --- /dev/null +++ b/platform/platform-impl/src/com/intellij/ide/passwordSafe/MacOsCredentialStore.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed 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 com.intellij.ide.passwordSafe + +import com.intellij.ide.passwordSafe.impl.PasswordSafeProvider +import com.intellij.ide.passwordSafe.macOs.deleteGenericPassword +import com.intellij.ide.passwordSafe.macOs.findGenericPassword +import com.intellij.ide.passwordSafe.macOs.saveGenericPassword +import com.intellij.openapi.diagnostic.catchAndLog +import java.security.MessageDigest + +internal class MacOsCredentialStore(serviceName: String) : PasswordSafeProvider { + private val serviceName = serviceName.toByteArray() + + override fun getPassword(requestor: Class<*>?, key: String): String? { + val rawKey = getRawKey(key, requestor) + // try old key - as hash + @Suppress("CanBeVal") + var value: String? + try { + value = findGenericPassword(serviceName, rawKey) + } + catch (e: Throwable) { + LOG.error(e) + return null + } + + if (value == null) { + LOG.catchAndLog { + val oldKey = toOldKey(MessageDigest.getInstance("SHA-256").digest(rawKey.toByteArray())) + value = findGenericPassword(serviceName, oldKey) + if (value != null) { + LOG.catchAndLog { deleteGenericPassword(serviceName, oldKey) } + saveGenericPassword(serviceName, key, value!!) + } + } + } + return value + } + + override fun setPassword(requestor: Class<*>?, key: String, value: String?) { + LOG.catchAndLog { + val rawKey = getRawKey(key, requestor) + if (value == null) { + deleteGenericPassword(serviceName, rawKey) + } + else { + saveGenericPassword(serviceName, rawKey, value) + } + } + } +} \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/ide/passwordSafe/PasswordSafeImpl.kt b/platform/platform-impl/src/com/intellij/ide/passwordSafe/PasswordSafeImpl.kt index 40fe907e891c..c6dda5eb2540 100644 --- a/platform/platform-impl/src/com/intellij/ide/passwordSafe/PasswordSafeImpl.kt +++ b/platform/platform-impl/src/com/intellij/ide/passwordSafe/PasswordSafeImpl.kt @@ -15,17 +15,16 @@ */ package com.intellij.ide.passwordSafe.impl -import com.intellij.ide.passwordSafe.FilePasswordSafeProvider -import com.intellij.ide.passwordSafe.LOG -import com.intellij.ide.passwordSafe.PasswordSafe -import com.intellij.ide.passwordSafe.PasswordSafeSettingsListener +import com.intellij.ide.passwordSafe.* import com.intellij.ide.passwordSafe.config.PasswordSafeSettings import com.intellij.ide.passwordSafe.config.PasswordSafeSettings.ProviderType +import com.intellij.ide.passwordSafe.macOs.isMacOsCredentialsStoreSupported import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.SettingsSavingComponent +import com.intellij.openapi.diagnostic.catchAndLog class PasswordSafeImpl(/* public - backward compatibility */val settings: PasswordSafeSettings) : PasswordSafe(), SettingsSavingComponent { - private val currentProvider: FilePasswordSafeProvider + private @Volatile var currentProvider: PasswordSafeProvider // it is helper storage to support set password as memory-only (see setPassword memoryOnly flag) private val memoryHelperProvider = lazy { FilePasswordSafeProvider(emptyMap(), memoryOnly = true) } @@ -33,13 +32,28 @@ class PasswordSafeImpl(/* public - backward compatibility */val settings: Passwo override fun isMemoryOnly() = settings.providerType == ProviderType.MEMORY_ONLY init { - currentProvider = FilePasswordSafeProvider(memoryOnly = settings.providerType == ProviderType.MEMORY_ONLY) + if (settings.providerType == ProviderType.MEMORY_ONLY) { + currentProvider = FilePasswordSafeProvider(memoryOnly = true) + } + else { + currentProvider = createPersistentCredentialStore() + } + ApplicationManager.getApplication().messageBus.connect().subscribe(PasswordSafeSettings.TOPIC, object: PasswordSafeSettingsListener { override fun typeChanged(oldValue: ProviderType, newValue: ProviderType) { val memoryOnly = newValue == ProviderType.MEMORY_ONLY - currentProvider.memoryOnly = memoryOnly if (memoryOnly) { - currentProvider.deleteFileStorage() + val provider = currentProvider + if (provider is FilePasswordSafeProvider) { + provider.memoryOnly = true + provider.deleteFileStorage() + } + else { + currentProvider = FilePasswordSafeProvider(memoryOnly = true) + } + } + else { + currentProvider = createPersistentCredentialStore(currentProvider as? FilePasswordSafeProvider) } } }) @@ -77,7 +91,7 @@ class PasswordSafeImpl(/* public - backward compatibility */val settings: Passwo } override fun save() { - currentProvider.save() + (currentProvider as? FilePasswordSafeProvider)?.let { it.save() } } fun clearPasswords() { @@ -88,7 +102,7 @@ class PasswordSafeImpl(/* public - backward compatibility */val settings: Passwo } } finally { - currentProvider.clear() + (currentProvider as? FilePasswordSafeProvider)?.let { it.clear() } } } @@ -104,3 +118,17 @@ class PasswordSafeImpl(/* public - backward compatibility */val settings: Passwo val memoryProvider: PasswordSafeProvider get() = memoryHelperProvider.value } + +private fun createPersistentCredentialStore(existing: FilePasswordSafeProvider? = null): PasswordSafeProvider { + LOG.catchAndLog { + if (isMacOsCredentialsStoreSupported && com.intellij.util.SystemProperties.getBooleanProperty("use.osx.keychain", false)) { + return MacOsCredentialStore("IntelliJ Platform") + } + } + + existing?.let { + it.memoryOnly = false + return it + } + return FilePasswordSafeProvider() +} \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/ide/passwordSafe/dbV1Convertor.kt b/platform/platform-impl/src/com/intellij/ide/passwordSafe/dbV1Convertor.kt index 4136a2e8bced..1ea0af318fe1 100644 --- a/platform/platform-impl/src/com/intellij/ide/passwordSafe/dbV1Convertor.kt +++ b/platform/platform-impl/src/com/intellij/ide/passwordSafe/dbV1Convertor.kt @@ -31,7 +31,6 @@ import com.intellij.openapi.util.SystemInfo import gnu.trove.THashMap import java.nio.file.Files import java.nio.file.Paths -import java.util.* import java.util.function.Function private val TEST_PASSWORD_VALUE = "test password" @@ -112,8 +111,6 @@ internal fun convertOldDb(oldKey: String, @Suppress("DEPRECATION") db: PasswordD return newDb } -fun toOldKey(hash: ByteArray) = "old-hashed-key|" + Base64.getEncoder().encodeToString(hash) - private fun rawTestKey(oldKey: String) = EncryptionUtil.hash("com.intellij.ide.passwordSafe.impl.providers.masterKey.MasterKeyPasswordSafe/TEST_PASSWORD:${oldKey}".toByteArray()) internal class PasswordDatabaseConvertor : ApplicationLoadListener { diff --git a/platform/platform-impl/src/com/intellij/ide/passwordSafe/impl/PasswordSafeProvider.java b/platform/platform-impl/src/com/intellij/ide/passwordSafe/impl/PasswordSafeProvider.java index f0f076764c81..309bc31a42e5 100644 --- a/platform/platform-impl/src/com/intellij/ide/passwordSafe/impl/PasswordSafeProvider.java +++ b/platform/platform-impl/src/com/intellij/ide/passwordSafe/impl/PasswordSafeProvider.java @@ -20,16 +20,11 @@ import com.intellij.ide.passwordSafe.PasswordStorage; /** * The provider for password safe component */ -public abstract class PasswordSafeProvider implements PasswordStorage { +public interface PasswordSafeProvider extends PasswordStorage { /** * @return true, the implementation is supported in the current environment */ - public boolean isSupported() { + default boolean isSupported() { return true; } - - /** - * @return the name of provider - */ - public abstract String getName(); } diff --git a/platform/platform-impl/src/com/intellij/ide/passwordSafe/impl/providers/BasePasswordSafeProvider.java b/platform/platform-impl/src/com/intellij/ide/passwordSafe/impl/providers/BasePasswordSafeProvider.java index b34c9751590f..5926b95a4b02 100644 --- a/platform/platform-impl/src/com/intellij/ide/passwordSafe/impl/providers/BasePasswordSafeProvider.java +++ b/platform/platform-impl/src/com/intellij/ide/passwordSafe/impl/providers/BasePasswordSafeProvider.java @@ -24,7 +24,7 @@ import org.jetbrains.annotations.Nullable; /** * Base Java-based provider for password safe that assumes a simple key-value storage. */ -public abstract class BasePasswordSafeProvider extends PasswordSafeProvider { +public abstract class BasePasswordSafeProvider implements PasswordSafeProvider { /** *

Get secret key for the provider.

diff --git a/platform/platform-impl/src/com/intellij/ide/passwordSafe/impl/providers/memory/MemoryPasswordSafe.java b/platform/platform-impl/src/com/intellij/ide/passwordSafe/impl/providers/memory/MemoryPasswordSafe.java index e50877515cb9..7d95f06bcfe2 100644 --- a/platform/platform-impl/src/com/intellij/ide/passwordSafe/impl/providers/memory/MemoryPasswordSafe.java +++ b/platform/platform-impl/src/com/intellij/ide/passwordSafe/impl/providers/memory/MemoryPasswordSafe.java @@ -83,11 +83,6 @@ public class MemoryPasswordSafe extends BasePasswordSafeProvider { database.get().put(new ByteArrayWrapper(key), encryptedPassword); } - @Override - public String getName() { - return "Memory PasswordSafe"; - } - public void clear() { database.get().clear(); } diff --git a/platform/platform-impl/src/com/intellij/ide/passwordSafe/macOsKeychainLibrary.kt b/platform/platform-impl/src/com/intellij/ide/passwordSafe/macOsKeychainLibrary.kt index 9b1a1687168c..3f312c92b6e6 100644 --- a/platform/platform-impl/src/com/intellij/ide/passwordSafe/macOsKeychainLibrary.kt +++ b/platform/platform-impl/src/com/intellij/ide/passwordSafe/macOsKeychainLibrary.kt @@ -13,100 +13,51 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.intellij.ide.passwordSafe +package com.intellij.ide.passwordSafe.macOs +import com.intellij.ide.passwordSafe.LOG import com.intellij.openapi.util.SystemInfo import com.sun.jna.Pointer -import java.nio.ByteBuffer -import java.nio.CharBuffer -val isOSXCredentialsStoreSupported: Boolean +val isMacOsCredentialsStoreSupported: Boolean get() = SystemInfo.isMacIntel64 && SystemInfo.isMacOSLeopard +private val LIBRARY = com.sun.jna.Native.loadLibrary("Security", MacOsKeychainLibrary::class.java) as MacOsKeychainLibrary + +fun saveGenericPassword(serviceName: ByteArray, accountName: String, password: String) { + saveGenericPassword(serviceName, accountName, password.toByteArray()) +} + +private fun saveGenericPassword(serviceName: ByteArray, accountName: String, passwordData: ByteArray) { + saveGenericPassword(serviceName, accountName, passwordData, passwordData.size) +} + +fun findGenericPassword(serviceName: ByteArray, accountName: String): String? { + val accountNameBytes = accountName.toByteArray() + val passwordSize = IntArray(1) + val passwordData = arrayOf(null) + checkForError("find", LIBRARY.SecKeychainFindGenericPassword(null, serviceName.size, serviceName, accountNameBytes.size, accountNameBytes, passwordSize, passwordData)) + val pointer = passwordData[0] ?: return null + + val result = String(pointer.getByteArray(0, passwordSize[0])) + LIBRARY.SecKeychainItemFreeContent(null, pointer) + return result +} + +fun deleteGenericPassword(serviceName: ByteArray, accountName: String) { + val itemRef = arrayOf(null) + val accountNameBytes = accountName.toByteArray() + checkForError("find (for delete)", LIBRARY.SecKeychainFindGenericPassword(null, serviceName.size, serviceName, accountNameBytes.size, accountNameBytes, null, null, itemRef)) + val pointer = itemRef[0] + if (pointer != null) { + checkForError("delete", LIBRARY.SecKeychainItemDelete(pointer)) + LIBRARY.CFRelease(pointer) + } +} + // http://developer.apple.com/mac/library/DOCUMENTATION/Security/Reference/keychainservices/Reference/reference.html // It is very, very important to use CFRelease/SecKeychainItemFreeContent You must do it, otherwise you can get "An invalid record was encountered." interface MacOsKeychainLibrary : com.sun.jna.Library { - companion object { - private val LIBRARY = com.sun.jna.Native.loadLibrary("Security", MacOsKeychainLibrary::class.java) as MacOsKeychainLibrary - - fun saveGenericPassword(serviceName: ByteArray, accountName: String, password: CharArray) { - saveGenericPassword(serviceName, accountName, Charsets.UTF_8.encode(CharBuffer.wrap(password))) - } - - fun saveGenericPassword(serviceName: ByteArray, accountName: String, password: String) { - saveGenericPassword(serviceName, accountName, Charsets.UTF_8.encode(password)) - } - - private fun saveGenericPassword(serviceName: ByteArray, accountName: String, passwordBuffer: ByteBuffer) { - val passwordData: ByteArray - val passwordDataSize = passwordBuffer.limit() - if (passwordBuffer.hasArray() && passwordBuffer.arrayOffset() == 0) { - passwordData = passwordBuffer.array() - } - else { - passwordData = ByteArray(passwordDataSize) - passwordBuffer.get(passwordData) - } - saveGenericPassword(serviceName, accountName, passwordData, passwordDataSize) - } - - fun findGenericPassword(serviceName: ByteArray, accountName: String): String? { - val accountNameBytes = accountName.toByteArray() - val passwordSize = IntArray(1) - val passwordData = arrayOf(null) - checkForError("find", LIBRARY.SecKeychainFindGenericPassword(null, serviceName.size, serviceName, accountNameBytes.size, accountNameBytes, passwordSize, passwordData)) - val pointer = passwordData[0] ?: return null - - val result = String(pointer.getByteArray(0, passwordSize[0])) - LIBRARY.SecKeychainItemFreeContent(null, pointer) - return result - } - - private fun saveGenericPassword(serviceName: ByteArray, accountName: String, password: ByteArray, passwordSize: Int) { - val accountNameBytes = accountName.toByteArray() - val itemRef = arrayOf(null) - checkForError("find (for save)", LIBRARY.SecKeychainFindGenericPassword(null, serviceName.size, serviceName, accountNameBytes.size, accountNameBytes, null, null, itemRef)) - val pointer = itemRef[0] - if (pointer == null) { - checkForError("save (new)", LIBRARY.SecKeychainAddGenericPassword(null, serviceName.size, serviceName, accountNameBytes.size, accountNameBytes, passwordSize, password)) - } - else { - checkForError("save (update)", LIBRARY.SecKeychainItemModifyContent(pointer, null, passwordSize, password)) - LIBRARY.CFRelease(pointer) - } - } - - fun deleteGenericPassword(serviceName: ByteArray, accountName: String) { - val itemRef = arrayOf(null) - val accountNameBytes = accountName.toByteArray() - checkForError("find (for delete)", LIBRARY.SecKeychainFindGenericPassword(null, serviceName.size, serviceName, accountNameBytes.size, accountNameBytes, null, null, itemRef)) - val pointer = itemRef[0] - if (pointer != null) { - checkForError("delete", LIBRARY.SecKeychainItemDelete(pointer)) - LIBRARY.CFRelease(pointer) - } - } - - fun checkForError(message: String, code: Int) { - if (code != 0 && code != /* errSecItemNotFound, always returned from find it seems */-25300) { - val translated = LIBRARY.SecCopyErrorMessageString(code, null) - val builder = StringBuilder(message).append(": ") - if (translated == null) { - builder.append(code) - } - else { - val buf = CharArray(LIBRARY.CFStringGetLength(translated).toInt()) - for (i in 0..buf.size - 1) { - buf[i] = LIBRARY.CFStringGetCharacterAtIndex(translated, i.toLong()) - } - LIBRARY.CFRelease(translated) - builder.append(buf).append(" (").append(code).append(')') - } - LOG.error(builder.toString()) - } - } - } - fun SecKeychainAddGenericPassword(keychain: Pointer?, serviceNameLength: Int, serviceName: ByteArray, accountNameLength: Int, accountName: ByteArray, passwordLength: Int, passwordData: ByteArray, itemRef: Pointer? = null): Int fun SecKeychainItemModifyContent(/*SecKeychainItemRef*/ itemRef: Pointer, /*SecKeychainAttributeList**/ attrList: Pointer?, length: Int, data: ByteArray): Int @@ -134,3 +85,36 @@ interface MacOsKeychainLibrary : com.sun.jna.Library { fun SecKeychainItemFreeContent(/*SecKeychainAttributeList*/attrList: Pointer?, data: Pointer?) } + +private fun saveGenericPassword(serviceName: ByteArray, accountName: String, password: ByteArray, passwordSize: Int) { + val accountNameBytes = accountName.toByteArray() + val itemRef = arrayOf(null) + checkForError("find (for save)", LIBRARY.SecKeychainFindGenericPassword(null, serviceName.size, serviceName, accountNameBytes.size, accountNameBytes, null, null, itemRef)) + val pointer = itemRef[0] + if (pointer == null) { + checkForError("save (new)", LIBRARY.SecKeychainAddGenericPassword(null, serviceName.size, serviceName, accountNameBytes.size, accountNameBytes, passwordSize, password)) + } + else { + checkForError("save (update)", LIBRARY.SecKeychainItemModifyContent(pointer, null, passwordSize, password)) + LIBRARY.CFRelease(pointer) + } +} + +private fun checkForError(message: String, code: Int) { + if (code != 0 && code != /* errSecItemNotFound, always returned from find it seems */-25300) { + val translated = LIBRARY.SecCopyErrorMessageString(code, null) + val builder = StringBuilder(message).append(": ") + if (translated == null) { + builder.append(code) + } + else { + val buf = CharArray(LIBRARY.CFStringGetLength(translated).toInt()) + for (i in 0..buf.size - 1) { + buf[i] = LIBRARY.CFStringGetCharacterAtIndex(translated, i.toLong()) + } + LIBRARY.CFRelease(translated) + builder.append(buf).append(" (").append(code).append(')') + } + LOG.error(builder.toString()) + } +} \ No newline at end of file diff --git a/platform/platform-resources/src/META-INF/PlatformExtensions.xml b/platform/platform-resources/src/META-INF/PlatformExtensions.xml index a2faa0297dda..0b650a14dbd1 100644 --- a/platform/platform-resources/src/META-INF/PlatformExtensions.xml +++ b/platform/platform-resources/src/META-INF/PlatformExtensions.xml @@ -163,7 +163,7 @@ serviceImplementation="com.intellij.ide.passwordSafe.config.PasswordSafeSettings"/> - + diff --git a/plugins/settings-repository/src/IcsManager.kt b/plugins/settings-repository/src/IcsManager.kt index 6578c38f01ef..f9ce218fe9b2 100644 --- a/plugins/settings-repository/src/IcsManager.kt +++ b/plugins/settings-repository/src/IcsManager.kt @@ -18,7 +18,7 @@ package org.jetbrains.settingsRepository import com.intellij.configurationStore.StateStorageManagerImpl import com.intellij.configurationStore.StreamProvider import com.intellij.ide.ApplicationLoadListener -import com.intellij.ide.passwordSafe.isOSXCredentialsStoreSupported +import com.intellij.ide.passwordSafe.macOs.isMacOsCredentialsStoreSupported import com.intellij.openapi.application.Application import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.PathManager @@ -56,7 +56,7 @@ val icsManager by lazy(LazyThreadSafetyMode.NONE) { class IcsManager(dir: Path) { val credentialsStore = object : AtomicNotNullLazyValue() { override fun compute(): CredentialsStore { - if (isOSXCredentialsStoreSupported && SystemProperties.getBooleanProperty("ics.use.osx.keychain", true)) { + if (isMacOsCredentialsStoreSupported && SystemProperties.getBooleanProperty("use.osx.keychain", true)) { catchAndLog { return OsXCredentialsStore("IntelliJ Platform Settings Repository") } diff --git a/plugins/settings-repository/src/git/JGitCredentialsProvider.kt b/plugins/settings-repository/src/git/JGitCredentialsProvider.kt index a144eac83fe7..3c0f91779b9c 100644 --- a/plugins/settings-repository/src/git/JGitCredentialsProvider.kt +++ b/plugins/settings-repository/src/git/JGitCredentialsProvider.kt @@ -15,7 +15,7 @@ */ package org.jetbrains.settingsRepository.git -import com.intellij.ide.passwordSafe.isOSXCredentialsStoreSupported +import com.intellij.ide.passwordSafe.macOs.isMacOsCredentialsStoreSupported import com.intellij.openapi.ui.MessageDialogBuilder import com.intellij.openapi.ui.Messages import com.intellij.openapi.util.NotNullLazyValue @@ -94,7 +94,7 @@ class JGitCredentialsProvider(private val credentialsStore: NotNullLazyValue 0) { @@ -73,12 +75,12 @@ class OsXCredentialsStore(serviceName: String) : CredentialsStore { } val data = if (sshKeyFile == null) "${PasswordUtil.encodePassword(credentials.id)}@${PasswordUtil.encodePassword(credentials.token)}" else credentials.token!! - MacOsKeychainLibrary.saveGenericPassword(getServiceName(sshKeyFile), accountName, data) + saveGenericPassword(getServiceName(sshKeyFile), accountName, data) } override fun reset(host: String) { if (accountToCredentials.remove(host) != null) { - MacOsKeychainLibrary.deleteGenericPassword(serviceName, host) + deleteGenericPassword(serviceName, host) } } }