Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ae40837
Add cross-platform App Hardening (DexGuard-class), Enterprise-gated
shai-almog Aug 6, 2026
010fe08
CI: complete copyright headers, /// docs, and force engine build order
shai-almog Aug 6, 2026
fc8822a
cn1-hardening: guard ProGuard against JDK 21+ class files
shai-almog Aug 6, 2026
efa163b
Address Codex P1 review: frame hierarchy, FQ main class, reactor dep
shai-almog Aug 6, 2026
605f722
Address Codex round-2 review + fix core/CI build breaks
shai-almog Aug 6, 2026
0fffb4c
Address Codex round-3 review (verifier hierarchy, service descriptors…
shai-almog Aug 6, 2026
8899127
docs: satisfy the developer-guide prose gate (Vale + xref + LanguageT…
shai-almog Aug 6, 2026
d2ebbef
Address Codex round-4 + Copilot review
shai-almog Aug 6, 2026
5009358
Address Codex round-5 review
shai-almog Aug 6, 2026
fdc77f2
Address Codex round-6 review (interface literals, empty-config)
shai-almog Aug 6, 2026
6efa3ef
Address Codex round-7 review
shai-almog Aug 6, 2026
d354db3
Address Codex round-8 review
shai-almog Aug 6, 2026
71b9c68
Address Codex round-9 review
shai-almog Aug 6, 2026
95a7fdb
CI: fix illegal '--' inside an XML comment in the plugin POM
shai-almog Aug 6, 2026
495a599
Address Codex round-10 review
shai-almog Aug 6, 2026
c94e6dd
Address Codex round-11 review
shai-almog Aug 6, 2026
b625567
Address Codex round-12 review
shai-almog Aug 6, 2026
df007c4
Address Codex round-13 review + fix developer-guide LanguageTool gate
shai-almog Aug 6, 2026
9fa7384
Fix JS launcher compile: import Display for the hardening stamp
shai-almog Aug 6, 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
30 changes: 28 additions & 2 deletions CodenameOne/src/com/codename1/crash/CrashProtection.java
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,8 @@ public void exception(Throwable t) {
"Process terminated by native fault",
new ArrayList<Frame>(0),
null,
pendingNative);
pendingNative,
null);
persistJson(synthetic.toJson());
}
installed = true;
Expand Down Expand Up @@ -240,8 +241,33 @@ static CrashReportPayload build(Throwable t) {
String message = scrubber.scrubMessage(t.getMessage());
List<Frame> frames = extractFrames(t);
String nativeLog = safeNativeLog();
String rawStack = scrubber.scrubRawStack(safeRawStack(t));
return new CrashReportPayload(newEventId(), exClass, message,
frames, nativeLog, null);
frames, nativeLog, null, rawStack);
}

/// Renders the throwable (and its cause chain) as a pre-rendered stack
/// string via `printStackTrace`. This is the one trace API that behaves
/// identically on every port, and on the ParparVM C targets -- where
/// `getStackTrace()` may return the trace only as a formatted string --
/// it is what keeps a Java crash readable, especially once obfuscated.
/// Swallows any failure: capturing a crash report must never itself crash.
private static String safeRawStack(Throwable t) {
try {
// Capture the platform's own rendering via printStackTrace(PrintStream) -- PrintStream
// (unlike PrintWriter) is in the restricted CLDC core API. This is what preserves the
// real trace on the ParparVM ports: the pre-rendered C shadow-call-stack text, or the
// JavaScript engine's Error().stack on the JS port (where getStackTrace() has no
// structured frames to offer). On the JVM ports it is the standard full trace.
java.io.ByteArrayOutputStream bout = new java.io.ByteArrayOutputStream();
java.io.PrintStream ps = new java.io.PrintStream(bout);
t.printStackTrace(ps);
ps.flush();
String s = bout.toString();
return s.length() == 0 ? null : s;
} catch (Throwable ignored) {
return null;
}
}

/// Pulls the platform log snapshot, swallowing any exception the
Expand Down
66 changes: 65 additions & 1 deletion CodenameOne/src/com/codename1/crash/CrashReportPayload.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,20 @@ final class CrashReportPayload {
/// signal handlers are usually compact (~64 frames * ~120 chars),
/// but a corrupt stack can produce arbitrarily long output.
static final int MAX_NATIVE_STACK_LEN = 16 * 1024;
/// Hard cap on the raw (pre-rendered) Java stack string captured via
/// `printStackTrace` -- the verbatim platform rendering plus the cause
/// chain. It complements the structured {@link #frames} (populated on
/// every port), and on the JS port, where there are no structured
/// frames, it carries the JavaScript engine stack. Mirrors
/// {@link #MAX_NATIVE_STACK_LEN}.
static final int MAX_RAW_STACK_LEN = 16 * 1024;

/// Trace-format discriminator values. Tells the server how to parse
/// {@link #rawStack} for this build.
static final String TRACE_STRUCTURED = "structured";
static final String TRACE_PARPARVM = "parparvm-text";
static final String TRACE_JS = "js-error";
static final String TRACE_NONE = "none";

final String eventId;
final String buildKey;
Expand All @@ -56,6 +70,23 @@ final class CrashReportPayload {
final String exceptionClass;
final String messageScrubbed;
final List<Frame> frames;
/// The pre-rendered Java stack captured via `printStackTrace`, which
/// works identically on every port. On the ParparVM C targets this is
/// the only readable Java trace once obfuscated; the server parses it
/// with the mapping. `null` when no stack was available.
final String rawStack;
/// One of {@link #TRACE_STRUCTURED}, {@link #TRACE_PARPARVM},
/// {@link #TRACE_JS} or {@link #TRACE_NONE}: how the server should read
/// {@link #rawStack}. Derived, never guessed.
final String traceFormat;
/// SHA-256 of the obfuscation mapping this build was hardened with,
/// stamped into the app so a report can be tied to the exact mapping.
/// Empty for unhardened builds.
final String mappingId;
/// The hardening level the build shipped with (`off` / `standard` /
/// `aggressive` / `paranoid`); lets the server answer "why can't I
/// retrace this?" with the honest reason.
final String hardenLevel;
/// Recent platform-log output captured at crash time. Provides
/// context the Java stack frame alone can't (NSLog/os_log on iOS,
/// logcat on Android). `null` if the platform has no readable log
Expand All @@ -71,25 +102,54 @@ final class CrashReportPayload {

CrashReportPayload(String eventId, String exceptionClass,
String messageScrubbed, List<Frame> frames,
String nativeLog, String nativeStack) {
String nativeLog, String nativeStack, String rawStack) {
this.eventId = eventId;
this.exceptionClass = exceptionClass;
this.messageScrubbed = trim(messageScrubbed, MAX_MESSAGE_LEN);
this.frames = capFrames(frames);
this.nativeLog = trim(nativeLog, MAX_NATIVE_LOG_LEN);
this.nativeStack = trim(nativeStack, MAX_NATIVE_STACK_LEN);
this.rawStack = trim(rawStack, MAX_RAW_STACK_LEN);
this.traceFormat = deriveTraceFormat(this.frames, this.rawStack);
Display d = Display.getInstance();
this.buildKey = d.getProperty("build_key", "");
this.packageName = d.getProperty("package_name", "");
this.appName = d.getProperty("AppName", "");
this.appVersion = d.getProperty("AppVersion", "");
this.platform = d.getPlatformName();
this.osVersion = d.getProperty("OSVer", "");
this.mappingId = d.getProperty("cn1.mappingId", "");
this.hardenLevel = d.getProperty("cn1.hardenLevel", "");
Locale loc = Locale.getDefault();
this.locale = loc == null ? "" : loc.toString();
this.clientTs = System.currentTimeMillis();
}

/// Derives the trace format from what we actually have. Structured
/// frames win; otherwise a raw stack whose first frame line begins
/// `" at "` is the ParparVM text format, and anything else with a
/// body is a JavaScript engine stack. Never a guess -- the server
/// relies on this to pick a parser.
private static String deriveTraceFormat(List<Frame> frames, String rawStack) {
if (frames != null && !frames.isEmpty()) {
return TRACE_STRUCTURED;
}
if (rawStack == null || rawStack.length() == 0) {
return TRACE_NONE;
}
// A ParparVM frame line is exactly " at <fqcn>.<method>:<line>"; a V8/JS
// frame carries a '(' or a URL. Look at the first " at " line.
int at = rawStack.indexOf(" at ");
if (at >= 0) {
int lineEnd = rawStack.indexOf('\n', at);
String body = lineEnd < 0 ? rawStack.substring(at + 7) : rawStack.substring(at + 7, lineEnd);
if (body.indexOf('(') < 0 && body.indexOf('/') < 0 && body.indexOf('@') < 0) {
return TRACE_PARPARVM;
}
}
return TRACE_JS;
}

static final class Frame {
final String className;
final String methodName;
Expand Down Expand Up @@ -124,6 +184,10 @@ String toJson() {
appendString(b, "locale", locale, false);
appendString(b, "nativeLog", nativeLog, false);
appendString(b, "nativeStack", nativeStack, false);
appendString(b, "rawStack", rawStack, false);
appendString(b, "traceFormat", traceFormat, false);
appendString(b, "mappingId", mappingId, false);
appendString(b, "hardenLevel", hardenLevel, false);
b.append(",\"clientTs\":").append(clientTs);
b.append(",\"frames\":[");
for (int i = 0; i < frames.size(); i++) {
Expand Down
17 changes: 17 additions & 0 deletions CodenameOne/src/com/codename1/crash/PiiScrubber.java
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,23 @@ public String scrubFrame(String className, String methodName) {
return methodName;
}

/// Scrubs a pre-rendered stack string. On the ParparVM ports the whole
/// Java trace arrives as one string rather than structured frames, so a
/// stricter application can override this to redact aggressively. The
/// default applies the same message scrubbing (emails, long digit runs),
/// which is harmless on class/method/line text.
///
/// #### Parameters
///
/// - `rawStack`: the pre-rendered stack string; may be `null`.
///
/// #### Returns
///
/// the scrubbed stack string, or `null` if `rawStack` is `null`.
public String scrubRawStack(String rawStack) {
return scrubMessage(rawStack);
}

/// Replaces all occurrences of an email-like substring with the form
/// `<first-three>***@<domain>`. Local parts shorter than three
/// characters are not padded; the original prefix is preserved and
Expand Down
68 changes: 68 additions & 0 deletions CodenameOne/src/com/codename1/security/hardening/Hardening.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.security.hardening;

import com.codename1.ui.Display;

/// Read-only reporting of whether this build was hardened, and with what.
///
/// App Hardening is an Enterprise, build-server transform: it renames classes,
/// encrypts strings and obfuscates control flow in the shipped binary across every
/// port. This class does not perform any of that -- it only reports what the build
/// server stamped into the app, so app code (and the crash reporter) can tell an
/// honestly-hardened build apart from an unhardened one such as a local or
/// simulator build.
///
/// The values are stamped as display properties by the build; in the simulator
/// and in local builds they report `false` / `"off"`, because those are never
/// obfuscated.
///
/// @author Shai Almog
public final class Hardening {

private Hardening() {
}

/// Whether the shipped binary was hardened. Always `false` in the simulator and in
/// local or source-project builds, which are never obfuscated.
///
/// @return true if the build server applied hardening to this build
public static boolean isHardened() {
return "true".equals(Display.getInstance().getProperty("cn1.hardened", "false"));
}

/// The hardening level the build shipped with.
///
/// @return one of `"off"`, `"standard"`, `"aggressive"`, `"paranoid"`
public static String getLevel() {
return Display.getInstance().getProperty("cn1.hardenLevel", "off");
}

/// The id of the obfuscation mapping this build was hardened with, matching the mapping the
/// build server retained for crash symbolication. Empty when the build was not hardened.
///
/// @return the mapping id, or an empty string
public static String getMappingId() {
return Display.getInstance().getProperty("cn1.mappingId", "");
}
}
32 changes: 32 additions & 0 deletions CodenameOne/src/com/codename1/security/hardening/package-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/

/// Read-only reporting of Codename One App Hardening status for the current build.
///
/// App Hardening is an Enterprise, build-server transform that renames classes,
/// encrypts strings and obfuscates control flow in the shipped binary across every
/// port, integrated with Crash Protection so obfuscated stack traces are still
/// symbolicated. The engine runs on the build server; this package only lets app
/// code observe whether the current build was hardened. See the App Hardening
/// chapter of the developer guide.
package com.codename1.security.hardening;
7 changes: 7 additions & 0 deletions Ports/CLDC11/src/java/lang/Throwable.java
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ public void printStackTrace(){
return; //TODO codavaj!!
}

/// Prints this throwable and its backtrace to the given stream. On the ParparVM ports this
/// writes the pre-rendered native stack (the C shadow-call-stack text, or the JavaScript
/// engine's Error().stack on the JS port), which the crash reporter captures as the raw stack.
public void printStackTrace(java.io.PrintStream s){
return; //TODO codavaj!!
}

/// Returns a short description of this Throwable object. If this Throwable object was
/// with an error message string, then the result is the concatenation of three strings: The name of the actual class of this object ": " (a colon and a space) The result of the
/// method for this object If this Throwable object was
Expand Down
36 changes: 35 additions & 1 deletion Ports/JavaSE/src/com/codename1/impl/javase/BuildHintEditor.java
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.impl.javase;

import javax.swing.*;
Expand Down Expand Up @@ -164,7 +186,19 @@ private void loadBuildHintModels() {
model.type = BuildHintValueType.Checkbox;
} else if ("select".equalsIgnoreCase(propertyValue)) {
model.type = BuildHintValueType.Select;
String valuesString = System.getProperty("codename1.arg.{{ "+model.name+" }}.values");
// Resolve the sibling ".values" property using the *exact* brace content of
// the ".type" property we're processing. model.name has already been stripped
// of its group prefix (a grouped hint registered as {{#group#name}} leaves
// model.name == "name"), and the registration side uses no spaces inside the
// braces, so the old "{{ "+model.name+" }}" lookup missed every grouped Select
// and every space-sensitive key. Deriving the key from propName keeps the two
// in lockstep regardless of grouping or spacing. Fall back to the historical
// spaced form for any cn1lib that registered its values key that way.
String valuesKey = propName.substring(0, propName.indexOf("}}.")+3) + "values";
String valuesString = System.getProperty(valuesKey);
if (valuesString == null) {
valuesString = System.getProperty("codename1.arg.{{ "+model.name+" }}.values");
}
if (valuesString != null) {
String separator = ""+valuesString.charAt(valuesString.length()-1);
ArrayList<String> values = new ArrayList<String>();
Expand Down
Loading
Loading