From 2a4d33f2341b7d24a2782402921f84c285e9e8dc Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Fri, 21 Oct 2016 17:25:19 +0200 Subject: [PATCH] IDEA-161462: master password --- .../src/KeePassCredentialStore.kt | 15 +- .../src/PasswordSafeConfigurable.kt | 147 ++++++++++++++---- .../credential-store/src/PasswordSafeImpl.kt | 41 +++-- .../src/PasswordSafeSettings.java | 19 +-- .../src/PasswordSafeSettingsListener.java | 2 +- .../credential-store/src/dbV1Convertor.kt | 2 +- .../test/KeePassCredentialStoreTest.kt | 2 +- .../com/intellij/ui/layout/LayoutBuilder.kt | 12 ++ .../intellij/ui/layout/MigLayoutBuilder.kt | 78 ++++++++-- .../src/com/intellij/ui/layout/Row.kt | 12 +- .../src/com/intellij/ui/layout/layoutImpl.kt | 2 +- 11 files changed, 230 insertions(+), 102 deletions(-) diff --git a/platform/credential-store/src/KeePassCredentialStore.kt b/platform/credential-store/src/KeePassCredentialStore.kt index f373d1fcfd9f..e7ac517a9984 100644 --- a/platform/credential-store/src/KeePassCredentialStore.kt +++ b/platform/credential-store/src/KeePassCredentialStore.kt @@ -41,7 +41,7 @@ internal class KeePassCredentialStore(keyToValue: Map, store: PasswordStorage) { diff --git a/platform/credential-store/src/PasswordSafeConfigurable.kt b/platform/credential-store/src/PasswordSafeConfigurable.kt index 89292531183d..e6ead52b6438 100644 --- a/platform/credential-store/src/PasswordSafeConfigurable.kt +++ b/platform/credential-store/src/PasswordSafeConfigurable.kt @@ -15,16 +15,20 @@ */ package com.intellij.credentialStore -import com.intellij.credentialStore.PasswordSafeSettings.ProviderType import com.intellij.ide.passwordSafe.PasswordSafe import com.intellij.ide.passwordSafe.impl.PasswordSafeImpl +import com.intellij.ide.passwordSafe.impl.createPersistentCredentialStore import com.intellij.openapi.options.ConfigurableBase import com.intellij.openapi.options.ConfigurableUi import com.intellij.openapi.ui.Messages +import com.intellij.openapi.util.SystemInfo +import com.intellij.ui.components.JBPasswordField import com.intellij.ui.components.RadioButton import com.intellij.ui.layout.* import com.intellij.util.text.nullize +import gnu.trove.THashMap import java.awt.Component +import javax.swing.JPanel class PasswordSafeConfigurable(private val settings: PasswordSafeSettings) : ConfigurableBase("application.passwordSafe", "Passwords", "reference.ide.settings.password.safe") { override fun getSettings() = settings @@ -33,56 +37,139 @@ class PasswordSafeConfigurable(private val settings: PasswordSafeSettings) : Con } class PasswordSafeConfigurableUi : ConfigurableUi { - private val saveOnDisk = RadioButton("Save on &disk") - private val rememberPasswordsUntilClosing = RadioButton("Remember passwords &until the application is closed") + private val inKeychain = RadioButton("In Native Keychain") + + private val inKeePass = RadioButton("In KeePass") + private val keePassMasterPassword = JBPasswordField() + + private val rememberPasswordsUntilClosing = RadioButton("Do not save, forget passwords after restart") + + private val modeToRow = THashMap() override fun reset(settings: PasswordSafeSettings) { when (settings.providerType) { ProviderType.MEMORY_ONLY -> rememberPasswordsUntilClosing.isSelected = true - ProviderType.MASTER_PASSWORD -> saveOnDisk.isSelected = true + ProviderType.KEYCHAIN -> inKeychain.isSelected = true + ProviderType.KEEPASS -> inKeePass.isSelected = true else -> throw IllegalStateException("Unknown provider type: ${settings.providerType}") } + + updateEnabledState() } - override fun isModified(settings: PasswordSafeSettings) = getProviderType() != settings.providerType + override fun isModified(settings: PasswordSafeSettings): Boolean { + if (getProviderType() != settings.providerType) { + return true + } + + if (getProviderType() == ProviderType.KEEPASS && String(keePassMasterPassword.password).nullize(true) != null) { + return true + } + return false + } override fun apply(settings: PasswordSafeSettings) { - settings.providerType = getProviderType() + val providerType = getProviderType() + val passwordSafe = PasswordSafe.getInstance() as PasswordSafeImpl + var provider = passwordSafe.currentProvider + + val masterPassword = String(keePassMasterPassword.password).nullize(true)?.toByteArray() + + if (settings.providerType != providerType) { + @Suppress("NON_EXHAUSTIVE_WHEN") + when (providerType) { + ProviderType.MEMORY_ONLY -> { + if (provider is KeePassCredentialStore) { + provider.memoryOnly = true + provider.deleteFileStorage() + } + else { + provider = KeePassCredentialStore(memoryOnly = true) + } + } + + ProviderType.KEYCHAIN -> { + provider = createPersistentCredentialStore(provider as? KeePassCredentialStore) + } + + ProviderType.KEEPASS -> { + provider = KeePassCredentialStore(memoryOnly = true, existingMasterPassword = masterPassword) + } + } + } + + if (providerType == ProviderType.KEEPASS) { + if (provider === passwordSafe.currentProvider && masterPassword != null) { + // so, provider is the same and we must change master password for existing database file + (provider as KeePassCredentialStore).setMasterPassword(masterPassword) + } + } + + settings.providerType = providerType } - override fun getComponent() = panel { - val passwordSafe = PasswordSafe.getInstance() as PasswordSafeImpl + fun updateEnabledState() { + modeToRow[ProviderType.KEEPASS]?.enabled = getProviderType() == ProviderType.KEEPASS + } - buttonGroup { - row { - saveOnDisk() - if (!passwordSafe.isNativeCredentialStoreUsed) { - button("Set Master Password") { event -> - Messages.showInputDialog(event.source as Component, "Master Password:", "Set Master Password", null)?.trim().nullize()?.let { - passwordSafe.setFileDatabaseMasterPassword(it) - } + override fun getComponent(): JPanel { + val passwordSafe = PasswordSafe.getInstance() as PasswordSafeImpl + val currentProvider = passwordSafe.currentProvider + + keePassMasterPassword.setPasswordIsStored(true) + + val panel = panel { + row { label("Save passwords:") } + + buttonGroup({ updateEnabledState() }) { + if (SystemInfo.isLinux || isMacOsCredentialStoreSupported) { + row { + inKeychain() + } + } + + row { + inKeePass() + + modeToRow[ProviderType.KEEPASS] = row("Master Password:") { + keePassMasterPassword(growPolicy = GrowPolicy.SHORT_TEXT) + } + } + + row { + rememberPasswordsUntilClosing() + } + if (currentProvider is KeePassCredentialStore && !currentProvider.memoryOnly) { + row { hint("Existing KeePass file will be removed.") } + } + } + + if (!passwordSafe.isNativeCredentialStoreUsed) { + row(separated = true) { + button("Clear Passwords") { event -> + passwordSafe.clearPasswords() + Messages.showInfoMessage(event.source as Component, "Passwords were cleared", "Clear Passwords") } } } - row { rememberPasswordsUntilClosing() } } - if (!passwordSafe.isNativeCredentialStoreUsed) { - row(separated = true) { - button("Clear Passwords") { event -> - passwordSafe.clearPasswords() - Messages.showInfoMessage(event.source as Component, "Passwords were cleared", "Clear Passwords") - } - } - } + return panel } private fun getProviderType(): ProviderType { - if (rememberPasswordsUntilClosing.isSelected) { - return ProviderType.MEMORY_ONLY - } - else { - return ProviderType.MASTER_PASSWORD + return when { + rememberPasswordsUntilClosing.isSelected -> ProviderType.MEMORY_ONLY + inKeePass.isSelected -> ProviderType.KEEPASS + else -> ProviderType.KEYCHAIN } } +} + +internal enum class ProviderType { + MEMORY_ONLY, KEYCHAIN, KEEPASS, + + // unused, but we cannot remove it because enum value maybe stored in the config and we must correctly deserialize it + @Deprecated("") + DO_NOT_STORE } \ No newline at end of file diff --git a/platform/credential-store/src/PasswordSafeImpl.kt b/platform/credential-store/src/PasswordSafeImpl.kt index ee8a53d9c3f1..cad093d65074 100644 --- a/platform/credential-store/src/PasswordSafeImpl.kt +++ b/platform/credential-store/src/PasswordSafeImpl.kt @@ -18,7 +18,6 @@ package com.intellij.ide.passwordSafe.impl import com.intellij.credentialStore.* -import com.intellij.credentialStore.PasswordSafeSettings.ProviderType import com.intellij.ide.passwordSafe.PasswordSafe import com.intellij.ide.passwordSafe.PasswordStorage import com.intellij.openapi.application.ApplicationManager @@ -28,7 +27,7 @@ import org.jetbrains.concurrency.runAsync import java.nio.file.Path class PasswordSafeImpl(/* public - backward compatibility */val settings: PasswordSafeSettings) : PasswordSafe(), SettingsSavingComponent { - private @Volatile var currentProvider: PasswordStorage + internal @Volatile var currentProvider: PasswordStorage // it is helper storage to support set password as memory-only (see setPassword memoryOnly flag) private val memoryHelperProvider = lazy { KeePassCredentialStore(emptyMap(), memoryOnly = true) } @@ -45,25 +44,23 @@ class PasswordSafeImpl(/* public - backward compatibility */val settings: Passwo 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 - if (memoryOnly) { - val provider = currentProvider - if (provider is KeePassCredentialStore) { - provider.memoryOnly = true - provider.deleteFileStorage() - } - else { - currentProvider = KeePassCredentialStore(memoryOnly = true) - } - } - else { - currentProvider = createPersistentCredentialStore(currentProvider as? KeePassCredentialStore) - } + internal fun setProvider(type: ProviderType) { + val memoryOnly = type == ProviderType.MEMORY_ONLY + if (memoryOnly) { + val provider = currentProvider + if (provider is KeePassCredentialStore) { + provider.memoryOnly = true + provider.deleteFileStorage() } - }) + else { + currentProvider = KeePassCredentialStore(memoryOnly = true) + } + } + else { + currentProvider = createPersistentCredentialStore(currentProvider as? KeePassCredentialStore) + } } override fun get(attributes: CredentialAttributes): Credentials? { @@ -122,9 +119,7 @@ class PasswordSafeImpl(/* public - backward compatibility */val settings: Passwo ApplicationManager.getApplication().messageBus.syncPublisher(PasswordSafeSettings.TOPIC).credentialStoreCleared() } - fun setFileDatabaseMasterPassword(password: String) { - (currentProvider as KeePassCredentialStore).setMasterPassword(password) - } + internal fun getKeePassCredentialStore() = currentProvider as KeePassCredentialStore fun importFileDatabase(path: Path, masterPassword: String) { currentProvider = copyFileDatabase(path, masterPassword) @@ -157,7 +152,7 @@ class PasswordSafeImpl(/* public - backward compatibility */val settings: Passwo get() = memoryHelperProvider.value } -private fun createPersistentCredentialStore(existing: KeePassCredentialStore? = null, convertFileStore: Boolean = false): PasswordStorage { +internal fun createPersistentCredentialStore(existing: KeePassCredentialStore? = null, convertFileStore: Boolean = false): PasswordStorage { LOG.catchAndLog { for (factory in CredentialStoreFactory.CREDENTIAL_STORE_FACTORY.extensions) { val store = factory.create() ?: continue diff --git a/platform/credential-store/src/PasswordSafeSettings.java b/platform/credential-store/src/PasswordSafeSettings.java index 080f8429ff57..8121e3b3397d 100644 --- a/platform/credential-store/src/PasswordSafeSettings.java +++ b/platform/credential-store/src/PasswordSafeSettings.java @@ -28,7 +28,7 @@ import org.jetbrains.annotations.NotNull; public class PasswordSafeSettings implements PersistentStateComponent { public static final Topic TOPIC = Topic.create("PasswordSafeSettingsListener", PasswordSafeSettingsListener.class); - private ProviderType myProviderType = ProviderType.MASTER_PASSWORD; + private ProviderType myProviderType = ProviderType.KEYCHAIN; @NotNull public ProviderType getProviderType() { @@ -56,23 +56,10 @@ public class PasswordSafeSettings implements PersistentStateComponent Unit) { LayoutBuilder(`$`, ButtonGroup()).init() } + + inline fun buttonGroup(crossinline elementActionListener: () -> Unit, init: LayoutBuilder.() -> Unit): ButtonGroup { + val group = ButtonGroup() + LayoutBuilder(`$`, group).init() + + val listener = ActionListener { elementActionListener() } + for (button in group.elements) { + button.addActionListener(listener) + } + return group + } } \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/ui/layout/MigLayoutBuilder.kt b/platform/platform-impl/src/com/intellij/ui/layout/MigLayoutBuilder.kt index c9b2d09fc8dd..e2c4b80395d4 100644 --- a/platform/platform-impl/src/com/intellij/ui/layout/MigLayoutBuilder.kt +++ b/platform/platform-impl/src/com/intellij/ui/layout/MigLayoutBuilder.kt @@ -22,6 +22,7 @@ import com.intellij.openapi.actionSystem.ToggleAction import com.intellij.openapi.project.DumbAware import com.intellij.openapi.ui.OnePixelDivider import com.intellij.ui.SeparatorComponent +import com.intellij.ui.components.Label import com.intellij.ui.components.noteComponent import com.intellij.util.SmartList import net.miginfocom.layout.* @@ -40,14 +41,14 @@ internal class MigLayoutBuilder : LayoutBuilderImpl { private val componentConstraints: MutableMap = SmartHashMap() - override fun newRow(label: JLabel?, buttonGroup: ButtonGroup?, separated: Boolean): Row { + override fun newRow(label: JLabel?, buttonGroup: ButtonGroup?, separated: Boolean, indented: Boolean): Row { if (separated) { - val row = MigLayoutRow(componentConstraints, noGrid = true, separated = true) + val row = MigLayoutRow(componentConstraints, this, noGrid = true, separated = true) rows.add(row) row.apply { SeparatorComponent(0, OnePixelDivider.BACKGROUND, null)() } } - val row = MigLayoutRow(componentConstraints, label != null, buttonGroup = buttonGroup) + val row = MigLayoutRow(componentConstraints, this, label != null, buttonGroup = buttonGroup, indented = indented) rows.add(row) label?.let { row.apply { label() } } @@ -59,13 +60,13 @@ internal class MigLayoutBuilder : LayoutBuilderImpl { // add empty row as top gap newRow() - val row = MigLayoutRow(componentConstraints, noGrid = true) + val row = MigLayoutRow(componentConstraints, this, noGrid = true) rows.add(row) row.apply { noteComponent(text)() } } override fun build(container: Container, layoutConstraints: Array) { - val labeled = rows.firstOrNull(MigLayoutRow::labeled) != null + val labeled = rows.firstOrNull({ it.labeled && !it.indented }) != null var gapTop = -1 val lc = c() @@ -77,6 +78,8 @@ internal class MigLayoutBuilder : LayoutBuilderImpl { lc.apply(layoutConstraints) } + lc.noVisualPadding() + container.layout = MigLayout(lc) val noGrid = layoutConstraints.contains(LCFlags.noGrid) @@ -124,12 +127,18 @@ internal class MigLayoutBuilder : LayoutBuilderImpl { } else { var isSkippableComponent = true - if (component === row.components.first() && labeled) { - if (row.labeled) { - isSkippableComponent = false + if (component === row.components.first()) { + if (row.indented) { + cc.horizontal.gapBefore = gapToBoundSize(HORIZONTAL_GAP * 3, true) } - else { - cc.skip() + + if (labeled) { + if (row.labeled) { + isSkippableComponent = false + } + else { + cc.skip() + } } } @@ -138,7 +147,10 @@ internal class MigLayoutBuilder : LayoutBuilderImpl { isSplitRequired = false cc.split() } - cc.horizontal.gapAfter = gapToBoundSize(HORIZONTAL_GAP * 2, true) + + if (component !== lastComponent) { + cc.horizontal.gapAfter = gapToBoundSize(HORIZONTAL_GAP * 2, true) + } } } @@ -166,20 +178,39 @@ private fun addGrowIfNeed(cc: CC, component: Component) { } } -private class MigLayoutRow(private val componentConstraints: MutableMap, val labeled: Boolean = false, val noGrid: Boolean = false, private val buttonGroup: ButtonGroup? = null, val separated: Boolean = false) : Row() { +private class MigLayoutRow(private val componentConstraints: MutableMap, + private val builder: MigLayoutBuilder, + val labeled: Boolean = false, + val noGrid: Boolean = false, + private val buttonGroup: ButtonGroup? = null, + val separated: Boolean = false, + val indented: Boolean = false) : Row() { val components = SmartList() var rightIndex = Int.MAX_VALUE - override operator fun JComponent.invoke(vararg constraints: CCFlags, gapLeft: Int) { - addComponent(this, constraints, gapLeft = gapLeft) + override var enabled: Boolean = true + get() = field + set(value) { + if (field == value) { + return + } + + field = value + for (c in components) { + c.isEnabled = value + } + } + + override operator fun JComponent.invoke(vararg constraints: CCFlags, gapLeft: Int, growPolicy: GrowPolicy?) { + addComponent(this, constraints, gapLeft = gapLeft, growPolicy = growPolicy) } - private fun addComponent(component: Component, constraints: Array, gapLeft: Int) { + private fun addComponent(component: Component, constraints: Array, gapLeft: Int, growPolicy: GrowPolicy?) { if (buttonGroup != null && component is JToggleButton) { buttonGroup.add(component) } - createComponentConstraints(constraints, gapLeft = gapLeft)?.let { + createComponentConstraints(constraints, gapLeft = gapLeft, growPolicy = growPolicy)?.let { componentConstraints.put(component, it) } components.add(component) @@ -191,6 +222,12 @@ private class MigLayoutRow(private val componentConstraints: MutableMap Unit): Row { + val row = builder.newRow(Label(label), indented = true) + row.init() + return row + } } private fun createComponentConstraints(constraints: Array? = null, @@ -198,7 +235,8 @@ private fun createComponentConstraints(constraints: Array? = null, gapAfter: Int = 0, gapTop: Int = 0, gapBottom: Int = 0, - split: Int = -1): CC? { + split: Int = -1, + growPolicy: GrowPolicy?): CC? { var _cc = constraints?.create() fun cc(): CC { if (_cc == null) { @@ -224,6 +262,12 @@ private fun createComponentConstraints(constraints: Array? = null, if (split != -1) { cc().split = split } + + if (growPolicy == GrowPolicy.SHORT_TEXT) { +// cc().minWidth("210") + cc().maxWidth("210") + } + return _cc } diff --git a/platform/platform-impl/src/com/intellij/ui/layout/Row.kt b/platform/platform-impl/src/com/intellij/ui/layout/Row.kt index 012acdb5efa4..9153addf4ed5 100644 --- a/platform/platform-impl/src/com/intellij/ui/layout/Row.kt +++ b/platform/platform-impl/src/com/intellij/ui/layout/Row.kt @@ -29,6 +29,8 @@ import javax.swing.JComponent import javax.swing.JLabel abstract class Row() { + abstract var enabled: Boolean + fun label(text: String, gapLeft: Int = 0, style: ComponentStyle? = null, fontColor: FontColor? = null, bold: Boolean = false) { Label(text, style, fontColor, bold)(gapLeft = gapLeft) } @@ -55,7 +57,7 @@ abstract class Row() { panel(*constraints) } - abstract operator fun JComponent.invoke(vararg constraints: CCFlags, gapLeft: Int = 0) + abstract operator fun JComponent.invoke(vararg constraints: CCFlags, gapLeft: Int = 0, growPolicy: GrowPolicy? = null) inline fun right(init: Row.() -> Unit) { alignRight() @@ -64,9 +66,7 @@ abstract class Row() { protected abstract fun alignRight() - @Deprecated(message = "Nested row is prohibited", level = DeprecationLevel.ERROR) - fun row(label: String, init: Row.() -> Unit) { - } + abstract fun row(label: String, init: Row.() -> Unit): Row @Deprecated(message = "Nested row is prohibited", level = DeprecationLevel.ERROR) fun row(label: JLabel? = null, init: Row.() -> Unit) { @@ -75,4 +75,8 @@ abstract class Row() { @Deprecated(message = "Nested noteRow is prohibited", level = DeprecationLevel.ERROR) fun noteRow(text: String) { } +} + +enum class GrowPolicy { + SHORT_TEXT } \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/ui/layout/layoutImpl.kt b/platform/platform-impl/src/com/intellij/ui/layout/layoutImpl.kt index a0aff8543335..f53366913449 100644 --- a/platform/platform-impl/src/com/intellij/ui/layout/layoutImpl.kt +++ b/platform/platform-impl/src/com/intellij/ui/layout/layoutImpl.kt @@ -32,7 +32,7 @@ fun createLayoutBuilder() = LayoutBuilder(MigLayoutBuilder()) // "When a protected member is accessed from an inline function, a public accessor method is created to provide an access to that protected member from the outside of the class where the function will be inlined to." // (https://youtrack.jetbrains.com/issue/KT-12215) interface LayoutBuilderImpl { - fun newRow(label: JLabel? = null, buttonGroup: ButtonGroup? = null, separated: Boolean = false): Row + fun newRow(label: JLabel? = null, buttonGroup: ButtonGroup? = null, separated: Boolean = false, indented: Boolean = false): Row fun build(container: Container, layoutConstraints: Array)