IDEA-161462: master password

This commit is contained in:
Vladimir Krivosheev
2016-10-21 17:27:04 +02:00
parent 0ab414978b
commit 2a4d33f234
11 changed files with 230 additions and 102 deletions
@@ -41,7 +41,7 @@ internal class KeePassCredentialStore(keyToValue: Map<CredentialAttributes, Cred
baseDirectory: Path = Paths.get(PathManager.getConfigPath()),
var memoryOnly: Boolean = false,
private val dbFile: Path = baseDirectory.resolve(DB_FILE_NAME),
existingMasterPassword: String? = null) : PasswordStorage, CredentialStore {
existingMasterPassword: ByteArray? = null) : PasswordStorage, CredentialStore {
private val db: KeePassDatabase
private val masterKeyStorage = MasterKeyFileStorage(baseDirectory)
@@ -52,7 +52,7 @@ internal class KeePassCredentialStore(keyToValue: Map<CredentialAttributes, Cred
if (keyToValue == null) {
needToSave = AtomicBoolean(false)
val masterPassword = existingMasterPassword?.toByteArray() ?: masterKeyStorage.get()
val masterPassword = existingMasterPassword ?: masterKeyStorage.get()
if (masterPassword == null) {
LOG.catchAndLog {
if (dbFile.exists()) {
@@ -66,7 +66,7 @@ internal class KeePassCredentialStore(keyToValue: Map<CredentialAttributes, Cred
else {
db = loadKdbx(dbFile, KdbxPassword(masterPassword)) ?: KeePassDatabase()
if (existingMasterPassword != null) {
masterKeyStorage.set(existingMasterPassword.toByteArray())
masterKeyStorage.set(existingMasterPassword)
}
}
}
@@ -178,12 +178,11 @@ internal class KeePassCredentialStore(keyToValue: Map<CredentialAttributes, Cred
}
}
fun setMasterPassword(password: String) {
fun setMasterPassword(masterPassword: ByteArray) {
LOG.assertTrue(!memoryOnly)
val masterKey = password.toByteArray()
masterKeyStorage.set(masterKey)
dbFile.writeSafe { db.save(KdbxPassword(masterKey), it) }
masterKeyStorage.set(masterPassword)
dbFile.writeSafe { db.save(KdbxPassword(masterPassword), it) }
dbFile.setOwnerPermissions()
}
}
@@ -192,7 +191,7 @@ internal fun copyFileDatabase(path: Path, masterPassword: String, baseDirectory:
val dbFile = baseDirectory.resolve(DB_FILE_NAME)
Files.copy(path, dbFile, StandardCopyOption.REPLACE_EXISTING)
dbFile.setOwnerPermissions()
return KeePassCredentialStore(baseDirectory = baseDirectory, dbFile = dbFile, existingMasterPassword = masterPassword)
return KeePassCredentialStore(baseDirectory = baseDirectory, dbFile = dbFile, existingMasterPassword = masterPassword.toByteArray())
}
internal fun copyTo(from: Map<CredentialAttributes, Credentials>, store: PasswordStorage) {
@@ -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<PasswordSafeConfigurableUi, PasswordSafeSettings>("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<PasswordSafeSettings> {
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<ProviderType, Row>()
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
}
@@ -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
@@ -28,7 +28,7 @@ import org.jetbrains.annotations.NotNull;
public class PasswordSafeSettings implements PersistentStateComponent<PasswordSafeSettings.State> {
public static final Topic<PasswordSafeSettingsListener> 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<PasswordSa
}
public void loadState(@NotNull State state) {
setProviderType(ObjectUtils.chooseNotNull(state.PROVIDER, ProviderType.MASTER_PASSWORD));
setProviderType(ObjectUtils.chooseNotNull(state.PROVIDER, ProviderType.KEYCHAIN));
}
public static class State {
public ProviderType PROVIDER = ProviderType.MASTER_PASSWORD;
}
public enum ProviderType {
@Deprecated
DO_NOT_STORE,
/**
* The passwords are stored only in the memory
*/
MEMORY_ONLY,
/**
* The passwords are encrypted with master password
*/
MASTER_PASSWORD
public ProviderType PROVIDER = ProviderType.KEYCHAIN;
}
}
@@ -3,7 +3,7 @@ package com.intellij.credentialStore;
import org.jetbrains.annotations.NotNull;
public interface PasswordSafeSettingsListener {
default void typeChanged(@NotNull PasswordSafeSettings.ProviderType oldValue, @NotNull PasswordSafeSettings.ProviderType newValue) {
default void typeChanged(@NotNull ProviderType oldValue, @NotNull ProviderType newValue) {
}
default void credentialStoreCleared() {
@@ -112,7 +112,7 @@ internal class PasswordDatabaseConvertor : ApplicationLoadListener {
val oldDbFile = Paths.get(PathManager.getConfigPath(), "options", "security.xml")
if (oldDbFile.exists()) {
val settings = ServiceManager.getService(PasswordSafeSettings::class.java)
if (settings.providerType != PasswordSafeSettings.ProviderType.MASTER_PASSWORD) {
if (settings.providerType != ProviderType.KEYCHAIN) {
return
}
@@ -71,7 +71,7 @@ class FileCredentialStoreTest {
credentialMap.put(attributes, credentials)
}
provider.setMasterPassword("foo")
provider.setMasterPassword("foo".toByteArray())
val pdbFile = baseDir.resolve("c.kdbx")
val pdbPwdFile = baseDir.resolve("pdb.pwd")
@@ -16,6 +16,7 @@
package com.intellij.ui.layout
import com.intellij.ui.components.Label
import java.awt.event.ActionListener
import javax.swing.ButtonGroup
import javax.swing.JLabel
@@ -38,4 +39,15 @@ class LayoutBuilder(val `$`: LayoutBuilderImpl, val buttonGroup: ButtonGroup? =
inline fun buttonGroup(init: LayoutBuilder.() -> 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
}
}
@@ -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<Component, CC> = 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<out LCFlags>) {
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<Component, CC>, 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<Component, CC>,
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<Component>()
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<out CCFlags>, gapLeft: Int) {
private fun addComponent(component: Component, constraints: Array<out CCFlags>, 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<Componen
}
rightIndex = components.size
}
override fun row(label: String, init: Row.() -> Unit): Row {
val row = builder.newRow(Label(label), indented = true)
row.init()
return row
}
}
private fun createComponentConstraints(constraints: Array<out CCFlags>? = null,
@@ -198,7 +235,8 @@ private fun createComponentConstraints(constraints: Array<out CCFlags>? = 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<out CCFlags>? = null,
if (split != -1) {
cc().split = split
}
if (growPolicy == GrowPolicy.SHORT_TEXT) {
// cc().minWidth("210")
cc().maxWidth("210")
}
return _cc
}
@@ -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
}
@@ -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<out LCFlags>)