001/*
002 * SPDX-License-Identifier: Apache-2.0
003 *
004 * Copyright 2025-2026 The Enola <https://enola.dev> Authors
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License");
007 * you may not use this file except in compliance with the License.
008 * You may obtain a copy of the License at
009 *
010 *     https://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package dev.enola.common.secret.gnome;
019
020import de.swiesend.secretservice.simple.SimpleCollection;
021
022import dev.enola.common.secret.Secret;
023import dev.enola.common.secret.SecretManager;
024
025import java.io.Closeable;
026import java.io.IOException;
027import java.util.Arrays;
028import java.util.Optional;
029
030/**
031 * GnomeSecretManager is a {@link SecretManager} implementation that uses the GNOME keyring via
032 * D-Bus. (An alternative would be to "shell out" and execute the external "secret-tool" command.)
033 */
034public class GnomeSecretManager implements SecretManager, Closeable {
035
036    // TODO This does not actually work; see https://github.com/swiesend/secret-service/issues/52
037
038    private final SimpleCollection dbus;
039
040    public GnomeSecretManager() throws IOException {
041        if (!SimpleCollection.isGnomeKeyringAvailable())
042            throw new IllegalStateException("Gnome keyring is not available.");
043        this.dbus = new SimpleCollection();
044    }
045
046    @Override
047    public void store(String key, char[] value) throws IOException {
048        if (dbus.createItem(key, new ArrayCharSequence(value)) == null) {
049            Arrays.fill(value, '\0');
050            throw new IOException("Failed to store secret.");
051        }
052    }
053
054    @Override
055    public Optional<Secret> getOptional(String key) {
056        char[] secret = dbus.getSecret(key);
057        return secret != null ? Optional.of(new Secret(secret)) : Optional.empty();
058    }
059
060    @Override
061    public void delete(String key) {
062        dbus.deleteItem(key);
063    }
064
065    @Override
066    public void close() {
067        dbus.close();
068    }
069
070    public static void main(String[] args) throws IOException {
071        try (SimpleCollection collection = new SimpleCollection()) {
072            String item = collection.createItem("My Item", "secret");
073
074            char[] actual = collection.getSecret(item);
075            // assertEquals("secret", new String(actual));
076            // assertEquals("My Item", collection.getLabel(item));
077
078            collection.deleteItem(item);
079        }
080
081        var secret = "hello, world".toCharArray();
082        var secretManager = new GnomeSecretManager();
083        secretManager.store("test", secret);
084        secretManager
085                .get("test")
086                .process(
087                        it -> {
088                            if (Arrays.compare(it, secret) != 0) throw new IllegalStateException();
089                        });
090        secretManager.delete("test");
091        secretManager.close();
092    }
093}