Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 15 additions & 8 deletions .github/workflows/test.java.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,38 @@ name: Test-Java
on:
pull_request:
branches: [ master ]
paths:
- 'sdk/java/core/**'
- '.github/workflows/test.java.yml'

permissions:
contents: read

jobs:
test-java:
runs-on: ubuntu-latest
strategy:
max-parallel: 1
matrix:
java-version: [ '8', '11', '16', '17', '18' ]
java-version: [ '8', '11', '17', '21' ]
name: KSM test with Java ${{ matrix.java-version }}
defaults:
run:
working-directory: ./sdk/java/core
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Java ${{ matrix.java-version }}
uses: actions/setup-java@v4
uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4
with:
distribution: 'zulu'
java-version: ${{ matrix.java-version }}

- name: Setup, Build and Test
uses: gradle/actions/setup-gradle@v3
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
gradle-version: '8.14'
arguments: build test
build-root-directory: ./sdk/java/core


- name: Build and Test
run: gradle build test
10 changes: 10 additions & 0 deletions sdk/java/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ For more information see our official documentation page https://docs.keeper.io/

# Change Log

## 17.2.1
- KSM-902 - Add IL5 (DoD Impact Level 5) region mapping (`IL5` → `il5.keepersecurity.us`)
- KSM-823 - Fix `custom` field omitted from record create payload when no custom fields are set
- `KeeperRecordData.custom` now defaults to `mutableListOf()` instead of `null` — `kotlinx-serialization` previously skipped null fields, causing `"custom"` to be absent from the V3 API payload
- Consistent with Commander and Vault which always include `"custom": []`
- KSM-854 - Fix `KeeperFileData` crash when `lastModified` field is absent from file metadata
- Files uploaded by non-SDK Keeper clients (iOS, Android, Web Vault) may omit `lastModified`
- Previously threw `MissingFieldException` and silently skipped the file attachment
- Now defaults to `0` when the field is absent, consistent with .NET SDK behavior (KSM-674)

## 17.2.0
- **SECURITY (KSM-699)** - Fix file permissions for config.json and cache.dat
- Config and cache files now created with 0600 permissions (owner read/write only)
Expand Down
2 changes: 1 addition & 1 deletion sdk/java/core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "com.keepersecurity.secrets-manager"

// During publishing, If version ends with '-SNAPSHOT' then it will be published to Maven snapshot repository
version = "17.2.0"
version = "17.2.1"

plugins {
`java-library`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

package com.keepersecurity.secretsManager.core

import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
Expand All @@ -12,20 +14,21 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class KeeperRecordData @JvmOverloads constructor(
var title: String,
val type: String,
val fields: MutableList<KeeperRecordField>,
var custom: MutableList<KeeperRecordField>? = null,
@EncodeDefault var custom: MutableList<KeeperRecordField> = mutableListOf(),
var notes: String? = null
) {
inline fun <reified T> getField(): T? {
return (fields + (custom ?: listOf())).find { x -> x is T } as? T
return (fields + custom).find { x -> x is T } as? T
}

fun getField(clazz: Class<out KeeperRecordField>): KeeperRecordField? {
return (fields + (custom ?: listOf())).find { x -> x.javaClass == clazz }
return (fields + custom).find { x -> x.javaClass == clazz }
}
}

Expand Down Expand Up @@ -57,7 +60,7 @@ data class KeeperFileData(
val type: String? = null,
val size: Long,
@Serializable(with = FlexibleLongSerializer::class)
val lastModified: Long
val lastModified: Long = 0
)

@Serializable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import java.util.*
import java.util.concurrent.*
import javax.net.ssl.*

const val KEEPER_CLIENT_VERSION = "mj17.2.0"
const val KEEPER_CLIENT_VERSION = "mj17.2.1"

const val KEY_HOSTNAME = "hostname" // base url for the Secrets Manager service
const val KEY_SERVER_PUBIC_KEY_ID = "serverPublicKeyId"
Expand Down Expand Up @@ -706,6 +706,7 @@ fun initializeStorage(storage: KeyValueStorage, oneTimeToken: String, hostName:
"GOV" -> "govcloud.keepersecurity.us"
"JP" -> "keepersecurity.jp"
"CA" -> "keepersecurity.ca"
"IL5" -> "il5.keepersecurity.us"
else -> tokenParts[0]
}
clientKey = tokenParts[1]
Expand Down Expand Up @@ -920,7 +921,7 @@ fun getNotationResults(options: SecretsManagerOptions, notation: String): List<S

val fields = when(selector.lowercase()) {
"field" -> record.data.fields
"custom_field" -> record.data.custom ?: mutableListOf<KeeperRecordField>()
"custom_field" -> record.data.custom
else -> throw Exception("Notation error - Expected /field or /custom_field but found /$selector")
}

Expand Down Expand Up @@ -991,9 +992,7 @@ fun completeTransaction(options: SecretsManagerOptions, recordUid: String, rollb
@ExperimentalSerializationApi
fun addCustomField(record: KeeperRecord, field: KeeperRecordField) {
if (field.javaClass.superclass == KeeperRecordField::class.java) {
if (record.data.custom == null)
record.data.custom = mutableListOf()
record.data.custom!!.add(field)
record.data.custom.add(field)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ internal class SecretsManagerTest {
initializeStorage(storage, "eu:ONE_TIME_TOKEN")
assertEquals("keepersecurity.eu", storage.getString("hostname"))
storage = InMemoryStorage()
initializeStorage(storage, "IL5:ONE_TIME_TOKEN")
assertEquals("il5.keepersecurity.us", storage.getString("hostname"))
storage = InMemoryStorage()
initializeStorage(storage, "fake.keepersecurity.com:ONE_TIME_TOKEN")
assertEquals("fake.keepersecurity.com", storage.getString("hostname"))
}
Expand All @@ -122,6 +125,44 @@ internal class SecretsManagerTest {
assertEquals("fake.keepersecurity.com", storage.getString("hostname"))
}

@Test
fun testRecordCreateEmptyCustomSerialized() {
// KSM-823: RecordCreate with no custom fields must include "custom": [] in JSON payload
val recordData = KeeperRecordData(
title = "Test Record",
type = "login",
fields = mutableListOf()
)
val json = Json.encodeToString(recordData)
assertTrue(json.contains("\"custom\":[]") || json.contains("\"custom\": []"),
"Serialized payload must include custom:[] even when no custom fields are set. Got: $json")
}

@Test
fun testKeeperFileDataMissingLastModified() {
// GH-973 / KSM-854: lastModified entirely absent — must deserialize without throwing
val json = """{"title":"test.txt","name":"test.txt","type":"text/plain","size":1024}"""
val result = Json.decodeFromString<KeeperFileData>(json)
assertEquals(0L, result.lastModified)
assertEquals("test.txt", result.name)
}

@Test
fun testKeeperFileDataIntegerLastModified() {
// Regression guard: normal integer lastModified
val json = """{"title":"test.txt","name":"test.txt","size":1024,"lastModified":1700000000000}"""
val result = Json.decodeFromString<KeeperFileData>(json)
assertEquals(1700000000000L, result.lastModified)
}

@Test
fun testKeeperFileDataFractionalLastModified() {
// Regression guard for KSM-673: fractional lastModified (iOS client format)
val json = """{"title":"test.txt","name":"test.txt","size":1024,"lastModified":1760646182.790214}"""
val result = Json.decodeFromString<KeeperFileData>(json)
assertEquals(1760646182L, result.lastModified)
}

// @Test // uncomment to debug the integration test
fun integrationTest() {
val trustAllPostFunction: (
Expand Down
Loading