MacOsCredentialStore, disabled by default

This commit is contained in:
Vladimir Krivosheev
2016-08-05 19:24:25 +02:00
parent f00bd00339
commit d6fb88eb25
12 changed files with 190 additions and 124 deletions
@@ -38,7 +38,7 @@ import javax.crypto.spec.SecretKeySpec
internal val LOG = Logger.getInstance(FilePasswordSafeProvider::class.java)
class FilePasswordSafeProvider @JvmOverloads constructor(keyToValue: Map<String, String>? = null, baseDirectory: Path = Paths.get(PathManager.getConfigPath()), var memoryOnly: Boolean = false) : PasswordSafeProvider() {
class FilePasswordSafeProvider @JvmOverloads constructor(keyToValue: Map<String, String>? = null, baseDirectory: Path = Paths.get(PathManager.getConfigPath()), var memoryOnly: Boolean = false) : PasswordSafeProvider {
private val db = ContainerUtil.newConcurrentMap<String, String>()
private val dbFile = baseDirectory.resolve("pdb")
@@ -156,11 +156,11 @@ class FilePasswordSafeProvider @JvmOverloads constructor(keyToValue: Map<String,
needToSave.set(true)
}
}
override fun getName() = "File PasswordSafe"
}
private fun getRawKey(key: String?, requestor: Class<*>?) = "${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)
@@ -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)
}
}
}
}
@@ -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()
}
@@ -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 {
@@ -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();
}
@@ -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 {
/**
* <p>Get secret key for the provider.</p>
@@ -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();
}
@@ -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<Pointer?>(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<Pointer?>(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<Pointer?>(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<Pointer?>(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<Pointer?>(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<Pointer?>(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())
}
}
@@ -163,7 +163,7 @@
serviceImplementation="com.intellij.ide.passwordSafe.config.PasswordSafeSettings"/>
<applicationService serviceInterface="com.intellij.ide.passwordSafe.PasswordSafe"
serviceImplementation="com.intellij.ide.passwordSafe.impl.PasswordSafeImpl"/>
<ApplicationLoadListener implementation="com.intellij.ide.passwordSafe.masterKey.PasswordDatabaseConvertor"/>
<ApplicationLoadListener implementation="com.intellij.ide.passwordSafe.PasswordDatabaseConvertor"/>
<applicationConfigurable parentId="preferences.general" instance="com.intellij.ide.passwordSafe.config.PasswordSafeConfigurable" id="application.passwordSafe"
displayName="Passwords"/>
@@ -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<CredentialsStore>() {
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")
}
@@ -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<Cre
}
else {
// we open password protected SSH key file using OS X keychain - "git credentials" is pointless in this case
if (sshKeyFile == null || !isOSXCredentialsStoreSupported) {
if (sshKeyFile == null || !isMacOsCredentialsStoreSupported) {
if (credentialsFromGit == null) {
credentialsFromGit = getCredentialsUsingGit(uri, repository)
}
@@ -15,7 +15,9 @@
*/
package org.jetbrains.keychain
import com.intellij.ide.passwordSafe.MacOsKeychainLibrary
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.util.PasswordUtil
import gnu.trove.THashMap
@@ -39,7 +41,7 @@ class OsXCredentialsStore(serviceName: String) : CredentialsStore {
return credentials
}
val data = MacOsKeychainLibrary.findGenericPassword(getServiceName(sshKeyFile), accountName) ?: return null
val data = findGenericPassword(getServiceName(sshKeyFile), accountName) ?: return null
if (sshKeyFile == null) {
val separatorIndex = data.indexOf('@')
if (separatorIndex > 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)
}
}
}