// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.credentialStore import com.intellij.util.concurrency.AppExecutorUtil import com.intellij.util.io.jna.DisposableMemory import com.sun.jna.Library import com.sun.jna.Native import com.sun.jna.Pointer import com.sun.jna.Structure import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit import java.util.function.Supplier private const val SECRET_SCHEMA_NONE = 0 private const val SECRET_SCHEMA_ATTRIBUTE_STRING = 0 // explicitly create pointer to be explicitly dispose it to avoid sensitive data in the memory internal fun stringPointer(data: ByteArray, clearInput: Boolean = false): DisposableMemory { val pointer = DisposableMemory(data.size + 1L) pointer.write(0, data, 0, data.size) pointer.setByte(data.size.toLong(), 0.toByte()) if (clearInput) { data.fill(0) } return pointer } // we use default collection, it seems no way to use custom internal class SecretCredentialStore(schemeName: String) : CredentialStore { private val serviceAttributeNamePointer by lazy { stringPointer("service".toByteArray()) } private val accountAttributeNamePointer by lazy { stringPointer("account".toByteArray()) } companion object { // no need to load lazily - if store created, then it will be used // and for clients better to get error earlier, in creation place private val library = Native.loadLibrary("secret-1", SecretLibrary::class.java) } private val scheme by lazy { library.secret_schema_new(schemeName, SECRET_SCHEMA_NONE, serviceAttributeNamePointer, SECRET_SCHEMA_ATTRIBUTE_STRING, accountAttributeNamePointer, SECRET_SCHEMA_ATTRIBUTE_STRING, null) } override fun get(attributes: CredentialAttributes): Credentials? { return CompletableFuture.supplyAsync(Supplier { checkError("secret_password_lookup_sync") { errorRef -> val serviceNamePointer = stringPointer(attributes.serviceName.toByteArray()) if (attributes.userName == null) { library.secret_password_lookup_sync(scheme, null, errorRef, serviceAttributeNamePointer, serviceNamePointer, null)?.let { // Secret Service doesn't allow to get attributes, so, we store joined data return@Supplier splitData(it) } } else { library.secret_password_lookup_sync(scheme, null, errorRef, serviceAttributeNamePointer, serviceNamePointer, accountAttributeNamePointer, stringPointer(attributes.userName!!.toByteArray()), null)?.let { return@Supplier splitData(it) } } } }, AppExecutorUtil.getAppExecutorService()) .get(30 /* on Linux first access to keychain can cause system unlock dialog, so, allow user to input data */, TimeUnit.SECONDS) } override fun set(attributes: CredentialAttributes, credentials: Credentials?) { val serviceNamePointer = stringPointer(attributes.serviceName.toByteArray()) val accountName = attributes.userName ?: credentials?.userName if (credentials.isEmpty()) { checkError("secret_password_store_sync") { errorRef -> if (accountName == null) { library.secret_password_clear_sync(scheme, null, errorRef, serviceAttributeNamePointer, serviceNamePointer, null) } else { library.secret_password_clear_sync(scheme, null, errorRef, serviceAttributeNamePointer, serviceNamePointer, accountAttributeNamePointer, stringPointer(accountName.toByteArray()), null) } } return } val passwordPointer = stringPointer(credentials!!.serialize(!attributes.isPasswordMemoryOnly), true) checkError("secret_password_store_sync") { errorRef -> try { if (accountName == null) { library.secret_password_store_sync(scheme, null, serviceNamePointer, passwordPointer, null, errorRef, serviceAttributeNamePointer, serviceNamePointer, null) } else { library.secret_password_store_sync(scheme, null, serviceNamePointer, passwordPointer, null, errorRef, serviceAttributeNamePointer, serviceNamePointer, accountAttributeNamePointer, stringPointer(accountName.toByteArray()), null) } } finally { passwordPointer.dispose() } } } } private inline fun checkError(method: String, task: (errorRef: Array) -> T): T { val errorRef = arrayOf(null) val result = task(errorRef) val error = errorRef[0] if (error != null && error.code != 0) { if (error.code == 32584 || error.code == 32618 || error.code == 32606 || error.code == 32642) { LOG.warn("gnome-keyring not installed or kde doesn't support Secret Service API. $method error code ${error.code}, error message ${error.message}") } else { LOG.error("$method error code ${error.code}, error message ${error.message}") } } return result } // we use sync API to simplify - client will use postponed write @Suppress("FunctionName") private interface SecretLibrary : Library { fun secret_schema_new(name: String, flags: Int, vararg attributes: Any?): Pointer fun secret_password_store_sync(scheme: Pointer, collection: Pointer?, label: Pointer, password: Pointer, cancellable: Pointer?, error: Array, vararg attributes: Pointer?) fun secret_password_lookup_sync(scheme: Pointer, cancellable: Pointer?, error: Array, vararg attributes: Pointer?): String? fun secret_password_clear_sync(scheme: Pointer, cancellable: Pointer?, error: Array, vararg attributes: Pointer?) } @Suppress("unused") internal class GErrorStruct : Structure() { @JvmField var domain = 0 @JvmField var code = 0 @JvmField var message: String? = null override fun getFieldOrder() = listOf("domain", "code", "message") }