Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
54775f4
fix(java): KSM-1081 skip undecryptable folders in getFolders instead …
stas-schaller Jul 7, 2026
7f3d971
test(java): KSM-1081 verify getFolders skips undecryptable folders
stas-schaller Jul 7, 2026
182892a
fix(java): KSM-1086 fix deleteFolder response type and surface per-it…
stas-schaller Jul 7, 2026
8c028be
chore(java): humanize comments in core SDK on the release branch (#1080)
stas-schaller Jul 31, 2026
16dc995
docs(java): STE pass on 17.4.0 changelog and Android/hello-secret exa…
stas-schaller Aug 17, 2026
2a46337
fix(java): validate server key_id range and secure config file writes…
stas-schaller Aug 18, 2026
ee7ecd0
fix(java): improve config write error message and guard POSIX-only test
stas-schaller Aug 18, 2026
3a9f51a
Fix generatePassword using non-cryptographic shuffle (KSM-1203) (#1114)
stas-schaller Aug 19, 2026
2b35ca0
Expose isEditable on KeeperRecord (KSM-1176) (#1115)
stas-schaller Aug 19, 2026
6941518
Set connect/read timeouts on all HttpsURLConnection calls (KSM-1207) …
stas-schaller Aug 19, 2026
b6041ff
fix(java): restore Java API compatibility and unblock the 17.4.0 rele…
mgallego-keeper Aug 19, 2026
5a075b0
docs(java): document the 17.4.0 breaking changes, and three review fo…
mgallego-keeper Aug 19, 2026
1b1e4dc
fix(ci): KSM-1269 run Java test matrix on release-branch PRs (#1119)
stas-schaller Aug 19, 2026
2f2f5dd
KSM-1270 Guard getSharedFolderKey against parent-cycle infinite loop …
stas-schaller Aug 19, 2026
86ce1cf
Add HTTP/HTTPS proxy support for Java SDK (KSM-531) (#1072)
stas-schaller Aug 19, 2026
f06a529
test(java): close the untested paths in the 17.4.0 security fixes, an…
mgallego-keeper Aug 19, 2026
3f0f0ed
docs(java): note cachingPostFunction unconditional stderr warning in …
stas-schaller Aug 19, 2026
b2ad4f0
fix(java): proxy credential validation and timeout reporting follow-u…
mgallego-keeper Aug 20, 2026
9a48560
chore(java): drop inert JvmOverloads and isolate proxy test global st…
mgallego-keeper Aug 20, 2026
bf8f5f2
fix(ci): KSM-1301 stop test.java.yml double-running on release branch…
mgallego-keeper Aug 20, 2026
90ab9f4
fix(java): gate getSecrets skip-path stderr on loggingEnabled (KSM-1081)
stas-schaller Aug 20, 2026
234a60d
test(java): add deleteSecret/deleteFolder coverage for KSM-1086
stas-schaller Aug 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion .github/workflows/test.java.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,43 @@
name: Test-Java

on:
# The gate. Runs on the merge result (head merged into base), so it can block
# a bad change before it lands, on release branches as well as master.
pull_request:
branches:
- master
- 'release/sdk/java/core/**'
paths:
- 'sdk/java/core/**'
- '.github/workflows/test.java.yml'
# master only. Two purposes: catch a direct push that bypassed a PR, and seed
# the writable Gradle cache that every release-branch PR run then restores.
# Deliberately NOT on release/**, where it would only re-test a commit the
# pull_request run already tested.
push:
branches: [ master ]
paths:
- 'sdk/java/core/**'
- '.github/workflows/test.java.yml'
# Manual re-run after a transient infrastructure failure, no empty commit needed.
workflow_dispatch:

permissions:
contents: read

# One in-flight run per ref. A new push supersedes the previous run rather than
# racing it, which also keeps simultaneous Maven Central requests down. Never
# cancel a master run, since that is what populates the cache.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
test-java:
runs-on: ubuntu-latest
strategy:
# One flaky JDK must not hide the results of the other three.
fail-fast: false
matrix:
java-version: [ '8', '11', '17', '21' ]
name: KSM test with Java ${{ matrix.java-version }}
Expand All @@ -34,6 +58,26 @@ jobs:
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: '8.14'
# Writable on master only, so one seeded cache serves every branch.
# Without this the cache is never written and every job resolves the
# full dependency graph over the network.
cache-read-only: ${{ github.event_name != 'push' }}

- name: Build and Test
run: gradle build test
shell: bash
run: |
log="${RUNNER_TEMP}/gradle-output.log"
for attempt in 1 2 3; do
if gradle build test 2>&1 | tee "${log}"; then
exit 0
fi
if ! grep -qE 'Too Many Requests|Could not (resolve|GET|download)' "${log}"; then
echo "::error::Build or tests failed; not a dependency-resolution error, so not retrying"
exit 1
fi
delay=$((attempt * 30))
echo "::warning::Dependency resolution failed on attempt ${attempt}; retrying in ${delay}s"
sleep "${delay}"
done
echo "::error::Dependency resolution still failing after 3 attempts"
exit 1
212 changes: 80 additions & 132 deletions examples/java/android-example/README.md
Original file line number Diff line number Diff line change
@@ -1,75 +1,63 @@
# Android Example using KSM Java SDK

**The absolute simplest Android app to prove the Keeper Secrets Manager Java SDK works on Android.**
This is a minimal Android app that shows the Keeper Secrets Manager Java SDK works on Android.

> **WARNING: This is a DEMO application for educational purposes only.
> Do NOT use this code in production without implementing proper security measures.**
> **WARNING**: This is a demo application for educational purposes only.
> Do not use this code in production without implementing proper security measures.

## Prerequisites

Before running this example, ensure you have:
Before running this example, make sure you have:

1. **Android Studio** (Arctic Fox or newer recommended)
2. **Android SDK** with API level 26+ (minSdk requirement)
3. **Keeper Secrets Manager Account** - [Sign up here](https://www.keepersecurity.com/)
4. **One-Time Access Token** - Generate from Keeper Secrets Manager:
- Log into Keeper Secrets Manager
- Navigate to your application
- Generate a one-time access token
- The token format is: `US:XXXXXX` or `EU:XXXXXX` (region prefix + token)
1. **Android Studio** (Arctic Fox or newer)
2. **Android SDK** with API level 26 or higher (minSdk requirement)
3. **Keeper Secrets Manager Account**: [Sign up here](https://www.keepersecurity.com/)
4. **One-Time Access Token**: Generate from Keeper Secrets Manager:
- Log in to Keeper Secrets Manager.
- Go to your application.
- Generate a one-time access token.
- The token format is `US:XXXXXX` or `EU:XXXXXX` (region prefix followed by the token).

## 🎯 Purpose
## Purpose

This is the **minimal working example** mentioned in the Android Compatibility Analysis. It demonstrates that with just proper threading, the SDK works as-is on Android.
This example shows that with proper threading, the SDK works as-is on Android. It uses `InMemoryStorage` and the default `HttpsURLConnection` to keep the configuration as simple as possible.

## ⚡ What This Proves
## What This Example Shows

✅ **SDK works on Android** with minimal changes
✅ **InMemoryStorage works** (no file I/O issues)
✅ **Crypto operations work** (AES/GCM, ECDH, ECDSA)
✅ **Network communication works** (HttpsURLConnection)
✅ **No ANR with proper threading** (Coroutines)
- The SDK initializes on Android.
- `InMemoryStorage` works (no file I/O required).
- Crypto operations work (AES/GCM, ECDH, ECDSA).
- Network communication works (`HttpsURLConnection`).
- Running SDK calls on a background thread prevents ANR errors.

## 📦 What's Included
## Limitations

**This is intentionally minimal:**
- ❌ No encrypted storage (uses `InMemoryStorage`)
- ❌ No OkHttp (uses default `HttpsURLConnection`)
- ❌ No fancy UI (simple XML layout)
- ❌ Config not persisted (lost on app restart)
- ❌ Minimal error handling
This example is intentionally minimal. It does not include:

## 🚀 Quick Start
- Encrypted storage (uses `InMemoryStorage`)
- OkHttp (uses the default `HttpsURLConnection`)
- Persisted configuration (config is lost when the app restarts)
- Full error handling

### 1. Open Project in Android Studio (30 seconds)
## Quick Start

### 2. Wait for Gradle Sync (2 minutes)
1. Open the project in Android Studio.
2. Wait for Gradle sync to complete.
3. Click **Run**.
4. Enter your Keeper one-time token.
5. Tap **Initialize**.
6. Wait 2-5 seconds.
7. Tap **Load Secrets**.

Let Android Studio download dependencies.
## Expected Output

### 3. Run (30 seconds)

Click the green ▶️ Run button.

### 4. Test (1 minute)

1. Enter your Keeper one-time token
2. Tap "1️⃣ Initialize"
3. Wait 2-5 seconds
4. Tap "2️⃣ Load Secrets"
5. See your secrets!

**Total time: ~4 minutes** ⚡

## 📋 What You'll See

### After Initialize:
After initialization:
```
✅ Initialized successfully!
Now tap 'Load Secrets'
```

### After Load Secrets:
After loading secrets:
```
✅ Secrets loaded successfully!

Expand All @@ -91,140 +79,100 @@ Now tap 'Load Secrets'
(no password)
```

## 🔍 Code Overview
## Code Overview

### MainActivity.kt (~150 lines)

The entire app in one file:
`MainActivity.kt` (~150 lines) contains the entire app. The SDK calls require only a background thread:

```kotlin
// Initialize KSM
private fun initializeKsm(token: String) {
lifecycleScope.launch {
withContext(Dispatchers.IO) {
// SDK call - works as-is!
initializeStorage(storage, token)
}
statusText.text = "✅ Initialized!"
}
}

// Load secrets
private fun loadSecrets() {
lifecycleScope.launch {
val secrets = withContext(Dispatchers.IO) {
val options = SecretsManagerOptions(storage)
getSecrets(options) // SDK call - works!
getSecrets(options)
}
displaySecrets(secrets)
}
}
```

**That's it!** The SDK works with just proper threading.

## 📊 Project Structure
## Project Structure

```
android-example/
├── build.gradle.kts # Root config
├── settings.gradle.kts # Project settings
├── build.gradle.kts
├── settings.gradle.kts
├── gradle.properties
├── .gitignore
│
└── app/
├── build.gradle.kts # Dependencies (minimal!)
├── src/main/
│ ├── AndroidManifest.xml # Permissions
│ ├── java/com/keeper/minimal/
│ │ └── MainActivity.kt # THE ENTIRE APP (150 lines)
│ └── res/
│ ├── layout/
│ │ └── activity_main.xml # Simple UI
│ └── values/
│ └── strings.xml
├── build.gradle.kts
└── src/main/
├── AndroidManifest.xml
├── java/com/keeper/minimal/
│ └── MainActivity.kt
└── res/
├── layout/
│ └── activity_main.xml
└── values/
└── strings.xml
```

**Total files: 10**
**Total code: ~300 lines**

## 🔧 Dependencies
Total: 10 files, ~300 lines of code.

**Minimal - only what's needed:**
## Dependencies

```kotlin
dependencies {
// The SDK - REQUIRED
implementation("com.keepersecurity.secrets-manager:keeper-secrets-manager-core:17.1.2")

// Basic Android UI
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")

// Coroutines for background threading
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
}
```

**That's all!** No OkHttp, no encryption libraries, no compose.

## ✅ What Works

- ✅ SDK initialization
- ✅ Fetching secrets
- ✅ Displaying secrets
- ✅ Password retrieval
- ✅ All crypto operations
- ✅ Network communication
- ✅ Runs on Android 8.0-16 (API 26-36)

## ❌ What Doesn't Work / Limitations

Since this is **intentionally minimal**:

1. **No persistence** - Config lost on app restart (uses `InMemoryStorage`)
2. **Not optimized** - Uses `HttpsURLConnection` (battery drain)
3. **No encryption** - Storage not encrypted (just in-memory)
4. **Minimal error handling** - Basic try/catch only
5. **Simple UI** - No Material3, no fancy design
6. **No offline support** - Requires network for everything

The example does not use OkHttp, encryption libraries, or Compose.

## Security Considerations for Production

This example intentionally uses simplified implementations for clarity. For production apps:
This example uses simplified implementations for educational purposes. For production apps:

- **Token Storage**: Use Android Keystore or `EncryptedSharedPreferences` instead of in-memory storage.
- **Sensitive Data**: Use biometric authentication before the app displays sensitive data.
- **Network Security**: Implement certificate pinning.
- **Error Handling**: Do not expose internal error details to users.
- **Logging**: Remove all sensitive data from logs before release.
- **Code Obfuscation**: Enable ProGuard/R8 with the appropriate keep rules for the SDK.

- **Token Storage**: Use Android Keystore or EncryptedSharedPreferences instead of in-memory storage
- **Token Input**: Consider using biometric authentication before displaying sensitive data
- **Network Security**: Implement certificate pinning
- **Error Handling**: Never expose internal error details to users
- **Logging**: Remove all sensitive data from logs
- **Code Obfuscation**: Enable ProGuard/R8 with appropriate keep rules for the SDK
## Troubleshooting

## 🐛 Troubleshooting
### Gradle sync fails

### "Gradle sync failed"
```bash
# File → Invalidate Caches → Restart
# File > Invalidate Caches > Restart
```

### "SDK location not found"
### SDK location not found

```bash
echo "sdk.dir=$HOME/Library/Android/sdk" > local.properties
```

### "App crashes on initialization"
**Check Logcat:**
- Look for network errors
- Verify token format (starts with US:, EU:, etc.)
- Check internet connection

### "Loading takes forever"
**This is expected on first run:**
- `SecureRandom.getInstanceStrong()` can take 3-5 seconds
- Subsequent runs are faster
- This is a known issue (see compatibility analysis)

### "Config lost after restart"
**This is by design:**
- Using `InMemoryStorage` (not persisted)
### App crashes on initialization

Check Logcat for network errors. Make sure the token format starts with a region prefix (for example, `US:` or `EU:`). Make sure the device has an internet connection.

### Loading takes a long time on first run

`SecureRandom.getInstanceStrong()` can take 3-5 seconds on the first call. Subsequent calls are faster. This is expected behavior.

### Config is lost after restart

This is by design. The example uses `InMemoryStorage`, which does not persist the configuration to disk.
6 changes: 3 additions & 3 deletions examples/java/hello-secret/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Keeper Secrets Manager Java SDK Example

Sample project demonstrating how to extract shared secrets from Keeper.
Sample project that shows how to extract shared secrets from Keeper.

Prerequisites:

- Java 8 or higher
- One or more one-time access tokens obtained from the owner of the secret.
- One or more one-time access tokens from the owner of the shared secret.

Usage:

Expand All @@ -18,6 +18,6 @@ For example:
./gradlew run --args="config.json US:EvdTdbH1xbHuRcja7QG3wMOyLUbvoQgF9WkkrHTdkh8"
```

The One-Time Access Token is used once to initialize the SDK configuration. After the SDK configuration is initialized, the One-Time Access Token can be removed.
The SDK uses the One-Time Access Token once to initialize its configuration. After initialization, you can remove the token.

For more information see our official documentation page https://docs.keeper.io/secrets-manager/secrets-manager/developer-sdk-library/java-sdk
Loading
Loading