KDPika Licensing Sign inStart free
API v1
Show all languages
Browse documentation

Choose a page in the navigation drawer.

KDPika Licensing · API v1 Advanced

Secure token storage

Choose native token storage by operating system and language, with explicit SDK fallbacks and tested helper examples.

On this page
Updated 2026-10-17 UTCDownload Markdown

Store activation credentials privately and fail clearly when a required native vault is unavailable.

What you will learn

  • Compare actual SDK storage with native-vault recommendations.
  • Use complete macOS, Windows, Linux and Electron helper examples.
  • Understand language bridges, mobile keystores and testing limits.

Prerequisites

What the SDKs actually do

SDKCurrent token storageImportant limitation
Python1.0.0Optional positive-priority keyring backend; permission-protected file fallbackprefer_keyring=true is a preference, not a fail-closed native-vault requirement; inspect storage_security
PHP1.0.0Namespaced files;0700 directory /0600 files where supportedNo automatic Keychain, DPAPI or Secret Service
JavaScript1.0.0Namespaced permission-protected files in Node/Electron main processNo built-in Electron safeStorage adapter
C#1.0.0Per-user files, atomic replacement and Unix modes where supportedNo automatic Windows Credential Manager/DPAPI or MAUI secure storage

These helpers do not override unsupported SDK storage hooks. PHP, JavaScript and C# have no public token-store injection option. Do not encrypt a second copy while leaving the SDK's original plaintext credential and call that secure integration. A required native-vault SDK adapter needs a separately reviewed product change. A REST client can call a helper at its own token persistence boundary.

Choose a native backend

OS / runtimeRecommended protectionDeployment behavior
Windows desktopCredential Manager or DPAPI CurrentUser, plus user-only file ACLsDPAPI ciphertext belongs to that Windows user context; do not assume roaming/reinstall recovery
macOS desktopGeneric-password Keychain item scoped to your signed application/serviceVault may lock or ask for permission; retain consistent publisher identity
Linux desktopSecret Service via libsecret or a verified desktop vaultHeadless sessions may have no unlocked service; do not silently use plaintext
ElectronsafeStorage in the main process after app readinessReject Linux basic_text; avoid exposing raw credentials over renderer IPC
Backend/CIRestricted secret manager or private filesystem under a dedicated service accountInteractive desktop keychains may not exist
AndroidAndroid Keystore-backed encryption for the token fileA token is arbitrary data; encrypt it with a protected key rather than claiming Keystore stores any string directly
iOS/iPadOSKeychain with the appropriate accessibility/access groupReview device-only, backup, lock-state and reinstall behavior for your app

The Microsoft data-protection guide explains user/computer scope. Apple Keychain services and libsecret describe their native services. A local administrator or compromised same-user process remains a meaningful threat; native storage is not remote attestation.

Language integration matrix

Use native APIs or an audited binding. The helper CLI contract is put/get/delete; send secret bytes through stdin, never a shell command string or argv. Capture get output in memory, never print it in a support log.

LanguageWindowsmacOSLinux / relevant alternative
PythonReviewed keyring backend or DPAPI bridgeReviewed keyring backend or Keychain helperkeyring/libsecret; explicitly inspect fallback
PHPServer secret store; DPAPI helper for a desktop hostKeychain helper through proc_open with stdinlibsecret helper through proc_open; not browser storage
JavaScriptElectron safeStorage / native helperElectron safeStorage / native helpersafeStorage with non-basic backend; no renderer secret
TypeScriptTyped wrapper over the JS/native boundaryTyped Keychain/helper wrapperTyped Secret Service/helper wrapper
C#CurrentUser DPAPI or Credential ManagerNative Keychain bridgeSecret Service binding/helper; MAUI SecureStorage on supported mobile
JavaReviewed JNA/native Credential Manager bindingNative Keychain binding/helperD-Bus/libsecret binding/helper; Android Keystore when mobile
KotlinJVM native binding on desktopJVM Keychain bridgeJVM Secret Service; Android Keystore on Android
SwiftNative helper/Windows crypto boundary requires a portSecurity framework KeychainReviewed Secret Service bridge; CryptoKit sample alone does not store tokens
GoNative syscall/binding for Credential Manager/DPAPINative Keychain bridgeD-Bus Secret Service binding/helper
RustAudited OS-vault crate with pinned backend featuresKeychain-native backendSecret Service backend; verify that no plaintext fallback is selected
RubyFFI/native helper with stdinKeychain helper with stdinSecret Service helper with stdin
DartPlatform channel to native vaultPlatform channel to KeychainDesktop Secret Service plugin/channel; mobile keystore-backed secure storage
CWinCred / CryptProtectDataSecItemAdd/CopyMatching/Update/Deletelibsecret C API
C++RAII wrapper around Windows APIsRAII wrapper around Security APIsRAII wrapper around libsecret

These are architecture choices, not claims that every third-party language binding was installed or audited. Pin and review the binding you select; test its exact OS and lock/fallback behavior.

macOS complete helper

Build with swiftc storage/keychain.swift -o keychain. Run keychain put ITEM with the generated test secret on stdin, keychain get ITEM with stdout captured privately, and keychain delete ITEM. The service namespace is reserved for this documentation example; use your own reviewed service identity in an application.

keychain.swiftswift
// macOS Keychain helper. Swift6/macOS13+, Foundation/Security. No dependency.
// Run: keychain put|get|delete ITEM; put reads secret from stdin, get writes stdout.
// Expected exit0 on success; generic error on failure. Never pass tokens in argv.
import Foundation
import Security
let service = "com.kdpika.documentation.storage-example"
func query(_ item: String) -> [String: Any] {
    [kSecClass as String:kSecClassGenericPassword, kSecAttrService as String:service,
     kSecAttrAccount as String:item]
}
do {
    guard CommandLine.arguments.count == 3 else { throw NSError(domain: "Usage",code:1) }
    let action = CommandLine.arguments[1], item = CommandLine.arguments[2]
    guard item.range(of: #"^[A-Za-z0-9._-]{8,128}$"#,options:.regularExpression) != nil else { throw NSError(domain:"Item",code:1) }
    let base = query(item); var status: OSStatus
    switch action {
    case "put":
        let data = FileHandle.standardInput.readData(ofLength: 16385)
        guard !data.isEmpty && data.count <= 16384 else { throw NSError(domain:"Size",code:1) }
        status = SecItemUpdate(base as CFDictionary,[kSecValueData as String:data] as CFDictionary)
        if status == errSecItemNotFound {
            var add = base; add[kSecValueData as String] = data
            add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
            status = SecItemAdd(add as CFDictionary,nil)
        }
    case "get":
        var request = base; request[kSecReturnData as String] = true; request[kSecMatchLimit as String] = kSecMatchLimitOne
        var result: CFTypeRef?; status = SecItemCopyMatching(request as CFDictionary,&result)
        if status == errSecSuccess, let data = result as? Data { FileHandle.standardOutput.write(data) }
    case "delete":
        status = SecItemDelete(base as CFDictionary)
        if status == errSecItemNotFound { status = errSecSuccess }
    default: throw NSError(domain:"Usage",code:1)
    }
    guard status == errSecSuccess else { throw NSError(domain:"Keychain",code:Int(status)) }
} catch {
    FileHandle.standardError.write(Data("Secure storage is unavailable.\n".utf8)); exit(1)
}

Windows complete helper

Build dotnet build storage/Dpapi.csproj -o build/dpapi; run dotnet build/dpapi/Dpapi.dll put, get or delete. It uses a fixed per-user example directory and CurrentUser protection. Set appropriate Windows ACLs on the application directory; chmod is not a Windows ACL policy.

Dpapi.csprojxml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="System.Security.Cryptography.ProtectedData" Version="7.0.0" />
  </ItemGroup>
</Project>
Program.cscsharp
// Windows CurrentUser DPAPI helper. .NET7+, ProtectedData7.0.0; Windows only.
// Run built Dpapi.dll put|get|delete. put reads stdin; get writes raw secret stdout.
// Exit0 on success; unavailable/error exits1. This is not an SDK storage adapter.
using System.Security.Cryptography;
try {
    if (!OperatingSystem.IsWindows() || args.Length != 1) throw new PlatformNotSupportedException();
    var directory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"KDPikaSecureStorageExample");
    var path = Path.Combine(directory,"activation-token.dpapi");
    switch (args[0]) {
        case "put":
            using (var input = Console.OpenStandardInput()) {
                var clear = new byte[16385]; var count = 0;
                while (count < clear.Length) { var n = input.Read(clear,count,clear.Length-count); if(n==0) break; count+=n; }
                if (count == 0 || count > 16384) throw new ArgumentException();
                var protectedBytes = ProtectedData.Protect(clear.AsSpan(0,count).ToArray(),null,DataProtectionScope.CurrentUser);
                CryptographicOperations.ZeroMemory(clear);
                Directory.CreateDirectory(directory);
                var temporary = path + "." + Guid.NewGuid().ToString("N");
                File.WriteAllBytes(temporary,protectedBytes); File.Move(temporary,path,true);
            }
            break;
        case "get":
            var secret = ProtectedData.Unprotect(File.ReadAllBytes(path),null,DataProtectionScope.CurrentUser);
            using(var output=Console.OpenStandardOutput()) output.Write(secret);
            CryptographicOperations.ZeroMemory(secret); break;
        case "delete": File.Delete(path); break;
        default: throw new ArgumentException();
    }
    return 0;
} catch { Console.Error.WriteLine("Secure storage is unavailable."); return 1; }

Linux complete helper

Install libsecret's secret-tool and an unlocked Secret Service implementation. Run python3 storage/secret_service.py put ITEM, get ITEM or delete ITEM. The helper has no plaintext fallback.

secret_service.pypython
"""Linux Secret Service helper. Python3.9+, libsecret secret-tool, unlocked vault.
Run: python3 secret_service.py put|get|delete ITEM. put reads stdin; get writes stdout.
Exit0 on success; no plaintext fallback. Never pass the token in an argument.
"""
import re
import subprocess
import sys

def operate(action, item, value=None):
    if not re.fullmatch(r'[A-Za-z0-9._-]{8,128}', item):
        raise ValueError('Invalid item')
    attributes = ['application', 'com.kdpika.documentation.storage-example', 'installation', item]
    if action == 'put':
        if not isinstance(value, bytes) or not 0 < len(value) <= 16384:
            raise ValueError('Invalid secret size')
        command = ['secret-tool', 'store', '--label=KDPika activation token'] + attributes
    elif action == 'get': command = ['secret-tool', 'lookup'] + attributes
    elif action == 'delete': command = ['secret-tool', 'clear'] + attributes
    else: raise ValueError('Unknown action')
    result = subprocess.run(command, input=value if action == 'put' else None,
                            capture_output=True, timeout=30, check=True)
    return result.stdout.rstrip(b'\n') if action == 'get' else b''

if __name__ == '__main__':
    try:
        if len(sys.argv) != 3: raise ValueError('Expected action and item')
        action, item = sys.argv[1:]
        value = sys.stdin.buffer.read(16385) if action == 'put' else None
        sys.stdout.buffer.write(operate(action,item,value))
    except Exception:
        print('Secure storage is unavailable.',file=sys.stderr)
        sys.exit(1)

Electron complete helper

Import tokenStore into the Electron main process, after app.whenReady(). Pass Electron's safeStorage and a private per-user directory, then call put(token), get() or remove(). Do not accept arbitrary file paths from an untrusted renderer.

electron.mjsjavascript
// Electron main-process safeStorage helper. Electron>=28, Node18+, built-in fs.
// Pass Electron's safeStorage after app.whenReady(), then put/get/remove.
// Reject unavailable encryption and Linux basic_text; never plaintext fallback.
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
export function tokenStore(safeStorage, directory, platform = process.platform) {
  const file = path.join(directory,'activation-token.encrypted');
  function requireVault() {
    if (!safeStorage.isEncryptionAvailable() ||
        (platform === 'linux' && (!safeStorage.getSelectedStorageBackend || safeStorage.getSelectedStorageBackend() === 'basic_text'))) {
      throw new Error('Secure storage is unavailable.');
    }
  }
  return {
    put(token) {
      requireVault(); if (typeof token !== 'string' || !token || Buffer.byteLength(token)>16384) throw Error('Invalid token size');
      fs.mkdirSync(directory,{recursive:true,mode:0o700});
      const encrypted=safeStorage.encryptString(token), temporary=file+'.'+crypto.randomUUID();
      fs.writeFileSync(temporary,encrypted,{mode:0o600,flag:'wx'}); fs.renameSync(temporary,file);
    },
    get() { requireVault(); return safeStorage.decryptString(fs.readFileSync(file)); },
    remove() { if(fs.existsSync(file)) fs.unlinkSync(file); }
  };
}

The sample uses the established synchronous safeStorage API for compatibility; it can block on vault prompts. Current Electron also offers an asynchronous API, which should be evaluated with its availability/rotation semantics when you adopt a specific newer runtime. See Electron's storage security notes. Never enable its plaintext-encryption fallback to claim native protection.

Verify and recover

  1. Generate a random disposable test value in memory.
  2. Store it in a unique test item/directory.
  3. Read it back into memory and compare exact bytes.
  4. Confirm a missing/locked vault returns a clear error and never writes a plaintext fallback.
  5. Delete only the uniquely named test item.
  6. Confirm a later read fails.

Expected result: round-trip succeeds only under the intended user/vault, and cleanup affects no real token. macOS Keychain execution, Windows compilation/refusal on macOS, Linux subprocess contract and mocked Electron fallback/refusal are recorded separately. Native Windows/Linux and real Electron vault execution are not claimed.

Expected result

A chosen storage boundary protects the actual token copy, has a visible fallback policy and handles locked/missing vaults without granting access.

Common mistakes

  • Storing credentials in browser localStorage or shipping a developer key.
  • Assuming prefer_keyring guarantees native encryption.
  • Keeping both an encrypted helper copy and an SDK plaintext copy unnoticed.

Security notes

Never log tokens, put them in command-line arguments or expose them to renderer JavaScript. Restrict cache/identity files too: they can contain personal data and influence trust. Store a public-key pin through a trusted deployment policy.

Troubleshooting

SymptomCauseAction
Vault access failsLocked keychain, unavailable service or denied accessAsk the user to unlock/allow the vault; fail clearly
Linux reports basic_textNo usable desktop secret backendRefuse native-storage claims and configure a real vault
Token vanished after reinstallUser profile/vault changedReactivate under the device-limit recovery policy

Next steps

Read signed responses and the threat model, then validate your own integration in Sandbox.

Was this page helpful?

Choose an answer

Do not include personal data, license keys or credentials. Retained for up to 30 days. This is not a support inbox.

Search documentation

Arrow keys to move, Enter to open, Escape to close. Search stays in your browser.

Cookie settings