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
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ Import Maven dependency:

The normal usage is to import the dependency of the Stringprep profile to use, and lookup the provider service that contains the profile.

### Example:
### Example

Import the `SASLprep` dependency, this transitively imports the `Stringprep` dependency.

```xml
<dependency>
<groupId>com.ongres.stringprep</groupId>
Expand All @@ -63,6 +65,7 @@ Import the `SASLprep` dependency, this transitively imports the `Stringprep` dep
```

Get the `SASLprep` provider service:

```java
Profile saslPrep = Stringprep.getProvider("SASLprep");
String prepared = saslPrep.prepareStored("I\u00ADX \u2168");
Expand All @@ -72,6 +75,7 @@ prepared.equals("IX IX"); // true
You could also (only) use the stringprep dependency to create your own profiles by implementing the `Profile` interface, just override the `profile()` method with the set of options.

Anonymous on-the-fly profile usage:

```java
Profile saslPrep = () -> EnumSet.of(Option.NORMALIZE_KC, Option.MAP_TO_NOTHING);
String prepared = saslPrep.prepareStored("I\u00ADX ⑳");
Expand All @@ -80,7 +84,8 @@ prepared.equals("IX 20"); // true

> Please note that when two protocols that use different profiles of stringprep interoperate, there may be conflict about what characters are and are not allowed in the final string. Thus, protocol developers should strongly consider re-using existing profiles of stringprep.

### Java Modules (JPMS):
### Java Modules (JPMS)

The Stringprep and profiles implementation are explicit Java modules with the names:

* `com.ongres.stringprep`
Expand All @@ -90,6 +95,7 @@ The Stringprep and profiles implementation are explicit Java modules with the na
If you depend on a specific profile (`saslprep` or `nameprep`) there is an implied readability on `stringprep`, so you will only need to declare in your `module-info.java` the profile module and get the service from the provider.

Example `module-info.java`:

```java
module test.app {
requires com.ongres.saslprep;
Expand Down
26 changes: 26 additions & 0 deletions saslprep/src/test/java/test/saslprep/SaslPrepTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,22 @@
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.io.IOException;
import java.util.EnumSet;
import java.util.Locale;
import java.util.stream.Collectors;

import com.ongres.saslprep.SASLprep;
import com.ongres.stringprep.Option;
import com.ongres.stringprep.Profile;
import com.ongres.stringprep.Stringprep;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EmptySource;
import org.junit.jupiter.params.provider.ValueSource;

class SaslPrepTest {

Expand Down Expand Up @@ -159,4 +165,24 @@ void unassigned() {
assertEquals("Unassigned code point \"0x0588\"", e.getMessage());
}
}

@ParameterizedTest
@ValueSource(strings = { "\u200B\u200C\u200D\u034F", "\uFEFF" })
@EmptySource
void testEmptyMap(String string) {
String stored = saslPrep.prepareStored(string);
assertTrue(stored.isEmpty(), () -> stored.codePoints()
.mapToObj(cp -> String.format(Locale.ROOT, "0x%04X", cp))
.collect(Collectors.joining(", ")));
}

@Test
void testAdditionalMappingOptions() {
String stored = saslPrep.prepareStored("\uFEFF\u2000\u3000\u00A0\uFEFF");
assertEquals(" ", stored,
stored.codePoints()
.mapToObj(cp -> String.format(Locale.ROOT, "0x%04X", cp))
.collect(Collectors.joining(", ")));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright (c) 2026 OnGres, Inc.
* SPDX-License-Identifier: BSD-2-Clause
*/

package com.ongres.stringprep;

import java.util.Arrays;

/**
* A memory-safe string builder alternative for cryptographic operations.
* It ensures that resized arrays and the final buffer are securely wiped with zeros.
*/
final class SecureCharBuffer implements AutoCloseable {
private char[] buffer;
private int length;

SecureCharBuffer(int initialCapacity) {
this.buffer = new char[initialCapacity];
this.length = 0;
}

void append(char[] chars, int offset, int len) {
ensureCapacity(this.length + len);
System.arraycopy(chars, offset, this.buffer, this.length, len);
this.length += len;
}

void appendCodePoint(int codePoint) {
int charCount = Character.charCount(codePoint);
ensureCapacity(this.length + charCount);
Character.toChars(codePoint, this.buffer, this.length);
this.length += charCount;
}

private void ensureCapacity(int minCapacity) {
if (minCapacity > buffer.length) {
int newCapacity = Math.max(buffer.length * 2, minCapacity);
char[] newBuffer = new char[newCapacity];
System.arraycopy(buffer, 0, newBuffer, 0, length);

// SECURE WIPE: Zero out the old array before abandoning it to the GC
Arrays.fill(buffer, '\0');
buffer = newBuffer;
}
}

/**
* Extracts the exact-sized array and leaves the internal buffer intact.
* The caller is now responsible for wiping the returned array.
*/
char[] toCharArray() {
char[] result = new char[length];
System.arraycopy(buffer, 0, result, 0, length);
return result;
}

@Override
public void close() {
if (buffer != null) {
Arrays.fill(buffer, '\0');
}
length = 0;
}
}
Loading
Loading