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
- Read authentication and the threat model.
- Use the source archive; helpers are in security/storage.
- Test only generated disposable values, not a customer's token.
What the SDKs actually do
| SDK | Current token storage | Important limitation |
|---|---|---|
| Python1.0.0 | Optional positive-priority keyring backend; permission-protected file fallback | prefer_keyring=true is a preference, not a fail-closed native-vault requirement; inspect storage_security |
| PHP1.0.0 | Namespaced files;0700 directory /0600 files where supported | No automatic Keychain, DPAPI or Secret Service |
| JavaScript1.0.0 | Namespaced permission-protected files in Node/Electron main process | No built-in Electron safeStorage adapter |
| C#1.0.0 | Per-user files, atomic replacement and Unix modes where supported | No 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 / runtime | Recommended protection | Deployment behavior |
|---|---|---|
| Windows desktop | Credential Manager or DPAPI CurrentUser, plus user-only file ACLs | DPAPI ciphertext belongs to that Windows user context; do not assume roaming/reinstall recovery |
| macOS desktop | Generic-password Keychain item scoped to your signed application/service | Vault may lock or ask for permission; retain consistent publisher identity |
| Linux desktop | Secret Service via libsecret or a verified desktop vault | Headless sessions may have no unlocked service; do not silently use plaintext |
| Electron | safeStorage in the main process after app readiness | Reject Linux basic_text; avoid exposing raw credentials over renderer IPC |
| Backend/CI | Restricted secret manager or private filesystem under a dedicated service account | Interactive desktop keychains may not exist |
| Android | Android Keystore-backed encryption for the token file | A token is arbitrary data; encrypt it with a protected key rather than claiming Keystore stores any string directly |
| iOS/iPadOS | Keychain with the appropriate accessibility/access group | Review 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.
| Language | Windows | macOS | Linux / relevant alternative |
|---|---|---|---|
| Python | Reviewed keyring backend or DPAPI bridge | Reviewed keyring backend or Keychain helper | keyring/libsecret; explicitly inspect fallback |
| PHP | Server secret store; DPAPI helper for a desktop host | Keychain helper through proc_open with stdin | libsecret helper through proc_open; not browser storage |
| JavaScript | Electron safeStorage / native helper | Electron safeStorage / native helper | safeStorage with non-basic backend; no renderer secret |
| TypeScript | Typed wrapper over the JS/native boundary | Typed Keychain/helper wrapper | Typed Secret Service/helper wrapper |
| C# | CurrentUser DPAPI or Credential Manager | Native Keychain bridge | Secret Service binding/helper; MAUI SecureStorage on supported mobile |
| Java | Reviewed JNA/native Credential Manager binding | Native Keychain binding/helper | D-Bus/libsecret binding/helper; Android Keystore when mobile |
| Kotlin | JVM native binding on desktop | JVM Keychain bridge | JVM Secret Service; Android Keystore on Android |
| Swift | Native helper/Windows crypto boundary requires a port | Security framework Keychain | Reviewed Secret Service bridge; CryptoKit sample alone does not store tokens |
| Go | Native syscall/binding for Credential Manager/DPAPI | Native Keychain bridge | D-Bus Secret Service binding/helper |
| Rust | Audited OS-vault crate with pinned backend features | Keychain-native backend | Secret Service backend; verify that no plaintext fallback is selected |
| Ruby | FFI/native helper with stdin | Keychain helper with stdin | Secret Service helper with stdin |
| Dart | Platform channel to native vault | Platform channel to Keychain | Desktop Secret Service plugin/channel; mobile keystore-backed secure storage |
| C | WinCred / CryptProtectData | SecItemAdd/CopyMatching/Update/Delete | libsecret C API |
| C++ | RAII wrapper around Windows APIs | RAII wrapper around Security APIs | RAII 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.
// 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.
<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>
// 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.
"""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 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
- Generate a random disposable test value in memory.
- Store it in a unique test item/directory.
- Read it back into memory and compare exact bytes.
- Confirm a missing/locked vault returns a clear error and never writes a plaintext fallback.
- Delete only the uniquely named test item.
- 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
| Symptom | Cause | Action |
|---|---|---|
| Vault access fails | Locked keychain, unavailable service or denied access | Ask the user to unlock/allow the vault; fail clearly |
| Linux reports basic_text | No usable desktop secret backend | Refuse native-storage claims and configure a real vault |
| Token vanished after reinstall | User profile/vault changed | Reactivate under the device-limit recovery policy |
Next steps
Read signed responses and the threat model, then validate your own integration in Sandbox.