Skip to content
Merged
242 changes: 242 additions & 0 deletions CodenameOne/src/com/codename1/components/OtpField.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
/*
* Copyright (c) 2008-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.components;

import com.codename1.ui.Container;
import com.codename1.ui.TextField;
import com.codename1.ui.events.ActionEvent;
import com.codename1.ui.events.ActionListener;
import com.codename1.ui.events.DataChangedListener;
import com.codename1.ui.layouts.BoxLayout;

import java.util.ArrayList;

/// Segmented one-time-password input -- one box per digit, auto-advances to
/// the next box on input and steps back on backspace. Standard pattern for
/// SMS / authenticator code entry screens.
///
/// #### Example
///
/// ```java
/// OtpField otp = new OtpField(6);
/// otp.addCompleteListener(new ActionListener() {
/// public void actionPerformed(ActionEvent evt) {
/// String code = otp.getText();
/// // verify code...
/// }
/// });
/// form.add(otp);
/// ```
///
/// Style the individual boxes with the UIID "OtpDigit"; the field itself uses
/// "OtpField".
public class OtpField extends Container {

private final int length;
private final boolean numericOnly;
private final TextField[] boxes;
private final ArrayList<ActionListener> completeListeners = new ArrayList<ActionListener>();
private boolean updating;

/// Builds a 6-digit numeric OTP field -- the common case.
public OtpField() {
this(6, true);
}

/// Builds an OTP field of the given length, numeric only.
///
/// #### Parameters
///
/// - `length`: number of digits / characters (e.g. 4, 6, 8)
public OtpField(int length) {
this(length, true);
}

/// Full constructor.
///
/// #### Parameters
///
/// - `length`: number of digits / characters
///
/// - `numericOnly`: true to restrict input to digits; false to allow any
/// character (alphanumeric OTP codes are sometimes used)
public OtpField(int length, boolean numericOnly) {
super(BoxLayout.x());
if (length < 2 || length > 16) {
throw new IllegalArgumentException("OTP length must be between 2 and 16");
}
this.length = length;
this.numericOnly = numericOnly;
this.boxes = new TextField[length];
setUIID("OtpField");
buildBoxes();
}

private void buildBoxes() {
for (int i = 0; i < length; i++) {
final int index = i;
final TextField tf = new TextField();
tf.setUIID("OtpDigit");
tf.setColumns(1);
tf.setMaxSize(1);
tf.setSingleLineTextArea(true);
if (numericOnly) {
tf.setConstraint(TextField.NUMERIC);
}
tf.addDataChangedListener(new DataChangedListener() {
@Override
public void dataChanged(int type, int idx) {
if (updating) {
return;
}
handleChange(index, tf);
}
});
boxes[i] = tf;
add(tf);
}
}

private void handleChange(int index, TextField source) {
String text = source.getText();
if (text == null) {
text = "";
}
// If multiple chars were pasted, distribute across boxes.
if (text.length() > 1) {
distributePaste(index, text);
return;
}
if (text.length() == 1) {
// advance focus to next box if not last
if (index < length - 1) {
boxes[index + 1].startEditingAsync();
} else {
fireCompleteIfFull();
}
} else {
// empty -- step back to previous box on backspace
if (index > 0) {
boxes[index - 1].startEditingAsync();
}
}
}

private void distributePaste(int startIndex, String text) {
updating = true;
try {
int p = startIndex;
for (int i = 0; i < text.length() && p < length; i++) {
char c = text.charAt(i);
if (numericOnly && (c < '0' || c > '9')) {
continue;
}
boxes[p].setText(String.valueOf(c));
p++;
}
// clear any remaining cells past where we wrote
if (p > startIndex) {
// last cell to focus is the one after the last written, or
// the last box if we wrote to the end
int focus = p < length ? p : length - 1;
boxes[focus].startEditingAsync();
}
} finally {
updating = false;
}
fireCompleteIfFull();
}

private void fireCompleteIfFull() {
String code = getText();
if (code.length() == length) {
ActionEvent evt = new ActionEvent(this);
for (ActionListener listener : completeListeners) {
listener.actionPerformed(evt);
if (evt.isConsumed()) {
break;
}
}
}
}

/// Returns the current value, in order from the first box to the last.
/// Empty boxes are omitted, so a partial entry returns a shorter string.
public String getText() {
StringBuilder b = new StringBuilder(length);
for (int i = 0; i < length; i++) {
String t = boxes[i].getText();
if (t != null) {
b.append(t);
}
}
return b.toString();
}

/// Sets the value, distributing one character per box. Excess characters
/// are silently dropped; shorter strings leave the remaining boxes empty.
public void setText(String code) {
updating = true;
try {
for (int i = 0; i < length; i++) {
if (code != null && i < code.length()) {
boxes[i].setText(String.valueOf(code.charAt(i)));
} else {
boxes[i].setText("");
}
}
} finally {
updating = false;
}
}

/// Clears all boxes.
public void clear() {
setText("");
boxes[0].startEditingAsync();
}

/// Adds a listener fired when the field becomes completely filled. Useful
/// to trigger automatic verification.
public void addCompleteListener(ActionListener l) {
if (l != null) {
completeListeners.add(l);
}
}

/// Removes a previously-registered listener.
public void removeCompleteListener(ActionListener l) {
completeListeners.remove(l);
}

/// Returns the underlying [TextField] for the box at `index`. Useful for
/// custom theming / focus management.
public TextField getBox(int index) {
return boxes[index];
}

/// Returns the configured length (number of boxes).
public int getLength() {
return length;
}
}
73 changes: 73 additions & 0 deletions CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java
Original file line number Diff line number Diff line change
Expand Up @@ -10149,4 +10149,77 @@ public void run() {
}
}
}

// ================================================================
// Crypto bridge -- see com.codename1.security package.
//
// The default implementations below all throw -- each platform port
// (JavaSEPort, AndroidImplementation, IOSImplementation) overrides them
// with the real native-backed implementation. The core stays free of
// java.security / javax.crypto references because the core compiles
// against the CLDC11 stub where those classes (and full Class reflection)
// are not available.

private static RuntimeException cryptoUnsupported(String op) {
return new RuntimeException("Crypto operation " + op + " is not supported on this platform. "
+ "If you are running in a fresh CodenameOneImplementation subclass, override the matching method.");
}

/// Fills `out` with cryptographically secure random bytes. Override in the
/// port to route to the platform's native CSPRNG.
public void secureRandomBytes(byte[] out) {
throw cryptoUnsupported("secureRandomBytes");
}

/// Encrypts with AES. Modes / paddings supported: AES/CBC/PKCS5Padding,
/// AES/CBC/NoPadding, AES/GCM/NoPadding (recommended -- authenticated;
/// the auth tag is appended to the ciphertext per the JCE convention) and
/// AES/ECB/PKCS5Padding (legacy interop only). `iv` may be null for ECB.
/// `aad` is associated data for GCM (may be null).
public byte[] aesEncrypt(String transformation, byte[] key, byte[] iv, byte[] aad, byte[] plaintext) {
throw cryptoUnsupported("aesEncrypt");
}

/// Decrypts with AES. Same parameters as `aesEncrypt`.
public byte[] aesDecrypt(String transformation, byte[] key, byte[] iv, byte[] aad, byte[] ciphertext) {
throw cryptoUnsupported("aesDecrypt");
}

/// Encrypts with RSA using an X.509 (SubjectPublicKeyInfo) DER-encoded
/// public key. `transformation` is typically
/// "RSA/ECB/OAEPWithSHA-256AndMGF1Padding" or "RSA/ECB/PKCS1Padding".
public byte[] rsaEncrypt(String transformation, byte[] publicKeyX509, byte[] plaintext) {
throw cryptoUnsupported("rsaEncrypt");
}

/// Decrypts with RSA using a PKCS#8 DER-encoded private key.
public byte[] rsaDecrypt(String transformation, byte[] privateKeyPkcs8, byte[] ciphertext) {
throw cryptoUnsupported("rsaDecrypt");
}

/// Computes a signature. `algorithm` is e.g. "SHA256withRSA",
/// "SHA256withECDSA". `keyAlgorithm` is "RSA" or "EC".
public byte[] cryptoSign(String algorithm, String keyAlgorithm, byte[] privateKeyPkcs8, byte[] data) {
throw cryptoUnsupported("cryptoSign");
}

/// Verifies a signature with an X.509 public key.
public boolean cryptoVerify(String algorithm, String keyAlgorithm, byte[] publicKeyX509, byte[] data, byte[] signature) {
throw cryptoUnsupported("cryptoVerify");
}

/// Generates a fresh RSA key pair of the given size in bits. Returns
/// `{publicKeyX509, privateKeyPkcs8}`.
public byte[][] generateRsaKeyPair(int bits) {
throw cryptoUnsupported("generateRsaKeyPair");
}

/// Generates `bytes` of fresh symmetric key material. The default just
/// delegates to [#secureRandomBytes(byte[])] (no structure is required
/// for AES keys).
public byte[] generateSymmetricKey(int bytes) {
byte[] out = new byte[bytes];
secureRandomBytes(out);
return out;
}
}
65 changes: 65 additions & 0 deletions CodenameOne/src/com/codename1/io/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -1799,6 +1799,71 @@ public static void setImplementation(CodenameOneImplementation impl) {
implInstance = impl;
}

// ----------------------------------------------------------------
// Narrow crypto bridge -- used by com.codename1.security to talk to the
// platform's native crypto provider without exposing the impl instance
// itself. Each method here is a one-to-one delegate to a method on
// CodenameOneImplementation; see that class for the parameter contract.

private static CodenameOneImplementation cryptoImpl() {
if (implInstance == null) {
throw new IllegalStateException("Codename One is not initialised");
}
return implInstance;
}

/// Fills `out` with cryptographically secure random bytes. Used by
/// [com.codename1.security.SecureRandom].
public static void secureRandomBytes(byte[] out) {
cryptoImpl().secureRandomBytes(out);
}

/// AES encryption. See
/// [com.codename1.impl.CodenameOneImplementation#aesEncrypt] for the
/// parameter contract. Used by [com.codename1.security.Cipher].
public static byte[] aesEncrypt(String transformation, byte[] key, byte[] iv,
byte[] aad, byte[] plaintext) {
return cryptoImpl().aesEncrypt(transformation, key, iv, aad, plaintext);
}

/// AES decryption -- counterpart to [#aesEncrypt].
public static byte[] aesDecrypt(String transformation, byte[] key, byte[] iv,
byte[] aad, byte[] ciphertext) {
return cryptoImpl().aesDecrypt(transformation, key, iv, aad, ciphertext);
}

/// RSA encryption.
public static byte[] rsaEncrypt(String transformation, byte[] publicKeyX509, byte[] plaintext) {
return cryptoImpl().rsaEncrypt(transformation, publicKeyX509, plaintext);
}

/// RSA decryption.
public static byte[] rsaDecrypt(String transformation, byte[] privateKeyPkcs8, byte[] ciphertext) {
return cryptoImpl().rsaDecrypt(transformation, privateKeyPkcs8, ciphertext);
}

/// Computes a signature.
public static byte[] cryptoSign(String algorithm, String keyAlgorithm,
byte[] privateKeyPkcs8, byte[] data) {
return cryptoImpl().cryptoSign(algorithm, keyAlgorithm, privateKeyPkcs8, data);
}

/// Verifies a signature.
public static boolean cryptoVerify(String algorithm, String keyAlgorithm,
byte[] publicKeyX509, byte[] data, byte[] signature) {
return cryptoImpl().cryptoVerify(algorithm, keyAlgorithm, publicKeyX509, data, signature);
}

/// Generates an RSA key pair. Returns `{publicKeyX509, privateKeyPkcs8}`.
public static byte[][] generateRsaKeyPair(int bits) {
return cryptoImpl().generateRsaKeyPair(bits);
}

/// Generates `bytes` of fresh symmetric key material.
public static byte[] generateSymmetricKey(int bytes) {
return cryptoImpl().generateSymmetricKey(bytes);
}

/// Merges arrays into one larger array
public static void mergeArrays(Object[] arr1, Object[] arr2, Object[] destinationArray) {
System.arraycopy(arr1, 0, destinationArray, 0, arr1.length);
Expand Down
Loading
Loading