mirror of
https://codeberg.org/Mercury-IM/Smack
synced 2024-11-22 14:22:05 +01:00
Add secure(OnlineAttackSafe|Unique|OfflineAttackSafe)RandomString()
and replace usages of java.util.UUID in Smack with secureUniqueRandomString() because it uses a thread-local secure random number generator.
This commit is contained in:
parent
58fc39714f
commit
619b8e6f4a
8 changed files with 89 additions and 60 deletions
|
@ -17,6 +17,8 @@
|
||||||
|
|
||||||
package org.jivesoftware.smack.util;
|
package org.jivesoftware.smack.util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.CharBuffer;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
@ -264,13 +266,9 @@ public class StringUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Array of numbers and letters of mixed case. Numbers appear in the list
|
* 24 upper case characters from the latin alphabet and numbers without '0' and 'O'.
|
||||||
* twice so that there is a more equal chance that a number will be picked.
|
|
||||||
* We can use the array to get a random number or letter by picking a random
|
|
||||||
* array index.
|
|
||||||
*/
|
*/
|
||||||
private static final char[] numbersAndLetters = ("0123456789abcdefghijklmnopqrstuvwxyz" +
|
private static final char[] UNAMBIGUOUS_NUMBERS_AND_LETTER = "123456789ABCDEFGHIJKLMNPQRSTUVWXYZ".toCharArray();
|
||||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ").toCharArray();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a random String of numbers and letters (lower and upper case)
|
* Returns a random String of numbers and letters (lower and upper case)
|
||||||
|
@ -289,6 +287,74 @@ public class StringUtils {
|
||||||
return randomString(length, RandomUtil.RANDOM.get());
|
return randomString(length, RandomUtil.RANDOM.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String secureOnlineAttackSafeRandomString() {
|
||||||
|
// 34^10 = 2.06e15 possible combinations. Which is enough to protect against online brute force attacks.
|
||||||
|
// See also https://www.grc.com/haystack.htm
|
||||||
|
final int REQUIRED_LENGTH = 10;
|
||||||
|
|
||||||
|
return randomString(RandomUtil.SECURE_RANDOM.get(), UNAMBIGUOUS_NUMBERS_AND_LETTER, REQUIRED_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String secureUniqueRandomString() {
|
||||||
|
// 34^13 = 8.11e19 possible combinations, which is > 2^64.
|
||||||
|
final int REQUIRED_LENGTH = 13;
|
||||||
|
|
||||||
|
return randomString(RandomUtil.SECURE_RANDOM.get(), UNAMBIGUOUS_NUMBERS_AND_LETTER, REQUIRED_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a secure random string with is human readable. The resulting string consists of 24 upper case characters
|
||||||
|
* from the Latin alphabet and numbers without '0' and 'O', grouped into 4-characters chunks, e.g.
|
||||||
|
* "TWNK-KD5Y-MT3T-E1GS-DRDB-KVTW". The characters are randomly selected by a cryptographically secure pseudorandom
|
||||||
|
* number generator (CSPRNG).
|
||||||
|
* <p>
|
||||||
|
* The string can be used a backup "code" for secrets, and is in fact the same as the one backup code specified in
|
||||||
|
* XEP-0373 and the one used by the <a href="https://github.com/open-keychain/open-keychain/wiki/Backups">Backup
|
||||||
|
* Format v2 of OpenKeychain</a>.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @see <a href="https://xmpp.org/extensions/xep-0373.html#backup-encryption"> XEP-0373 §5.4 Encrypting the Secret
|
||||||
|
* Key Backup</a>
|
||||||
|
* @return a human readable secure random string.
|
||||||
|
*/
|
||||||
|
public static String secureOfflineAttackSafeRandomString() {
|
||||||
|
// 34^24 = 2^122.10 possible combinations. Which is enough to protect against offline brute force attacks.
|
||||||
|
// See also https://www.grc.com/haystack.htm
|
||||||
|
final int REQUIRED_LENGTH = 24;
|
||||||
|
|
||||||
|
return randomString(RandomUtil.SECURE_RANDOM.get(), UNAMBIGUOUS_NUMBERS_AND_LETTER, REQUIRED_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int RANDOM_STRING_CHUNK_SIZE = 4;
|
||||||
|
|
||||||
|
private static String randomString(Random random, char[] alphabet, int numRandomChars) {
|
||||||
|
// The buffer most hold the size of the requested number of random chars and the chunk separators ('-').
|
||||||
|
int bufferSize = numRandomChars + ((numRandomChars - 1) / RANDOM_STRING_CHUNK_SIZE);
|
||||||
|
CharBuffer charBuffer = CharBuffer.allocate(bufferSize);
|
||||||
|
|
||||||
|
try {
|
||||||
|
randomString(charBuffer, random, alphabet, numRandomChars);
|
||||||
|
} catch (IOException e) {
|
||||||
|
// This should never happen if we calcuate the buffer size correctly.
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return charBuffer.flip().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void randomString(Appendable appendable, Random random, char[] alphabet, int numRandomChars)
|
||||||
|
throws IOException {
|
||||||
|
for (int randomCharNum = 1; randomCharNum <= numRandomChars; randomCharNum++) {
|
||||||
|
int randomIndex = random.nextInt(alphabet.length);
|
||||||
|
char randomChar = alphabet[randomIndex];
|
||||||
|
appendable.append(randomChar);
|
||||||
|
|
||||||
|
if (randomCharNum % RANDOM_STRING_CHUNK_SIZE == 0 && randomCharNum < numRandomChars) {
|
||||||
|
appendable.append('-');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static String randomString(final int length) {
|
public static String randomString(final int length) {
|
||||||
return randomString(length, RandomUtil.SECURE_RANDOM.get());
|
return randomString(length, RandomUtil.SECURE_RANDOM.get());
|
||||||
}
|
}
|
||||||
|
@ -298,24 +364,14 @@ public class StringUtils {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] randomBytes = new byte[length];
|
|
||||||
random.nextBytes(randomBytes);
|
|
||||||
char[] randomChars = new char[length];
|
char[] randomChars = new char[length];
|
||||||
for (int i = 0; i < length; i++) {
|
for (int i = 0; i < length; i++) {
|
||||||
randomChars[i] = getPrintableChar(randomBytes[i]);
|
int index = random.nextInt(UNAMBIGUOUS_NUMBERS_AND_LETTER.length);
|
||||||
|
randomChars[i] = UNAMBIGUOUS_NUMBERS_AND_LETTER[index];
|
||||||
}
|
}
|
||||||
return new String(randomChars);
|
return new String(randomChars);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static char getPrintableChar(byte indexByte) {
|
|
||||||
assert (numbersAndLetters.length < Byte.MAX_VALUE * 2);
|
|
||||||
|
|
||||||
// Convert indexByte as it where an unsigned byte by promoting it to int
|
|
||||||
// and masking it with 0xff. Yields results from 0 - 254.
|
|
||||||
int index = indexByte & 0xff;
|
|
||||||
return numbersAndLetters[index % numbersAndLetters.length];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if CharSequence is not null and is not empty, false otherwise.
|
* Returns true if CharSequence is not null and is not empty, false otherwise.
|
||||||
* Examples:
|
* Examples:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* Copyright © 2017-2018 Florian Schmaus, 2016-2017 Fernando Ramirez
|
* Copyright © 2017-2019 Florian Schmaus, 2016-2017 Fernando Ramirez
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -23,7 +23,6 @@ import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.WeakHashMap;
|
import java.util.WeakHashMap;
|
||||||
|
|
||||||
import org.jivesoftware.smack.ConnectionCreationListener;
|
import org.jivesoftware.smack.ConnectionCreationListener;
|
||||||
|
@ -456,7 +455,7 @@ public final class MamManager extends Manager {
|
||||||
|
|
||||||
public MamQuery queryArchive(MamQueryArgs mamQueryArgs) throws NoResponseException, XMPPErrorException,
|
public MamQuery queryArchive(MamQueryArgs mamQueryArgs) throws NoResponseException, XMPPErrorException,
|
||||||
NotConnectedException, NotLoggedInException, InterruptedException {
|
NotConnectedException, NotLoggedInException, InterruptedException {
|
||||||
String queryId = UUID.randomUUID().toString();
|
String queryId = StringUtils.secureUniqueRandomString();
|
||||||
String node = mamQueryArgs.node;
|
String node = mamQueryArgs.node;
|
||||||
DataForm dataForm = mamQueryArgs.getDataForm();
|
DataForm dataForm = mamQueryArgs.getDataForm();
|
||||||
|
|
||||||
|
@ -515,7 +514,7 @@ public final class MamManager extends Manager {
|
||||||
public List<FormField> retrieveFormFields(String node)
|
public List<FormField> retrieveFormFields(String node)
|
||||||
throws NoResponseException, XMPPErrorException, NotConnectedException,
|
throws NoResponseException, XMPPErrorException, NotConnectedException,
|
||||||
InterruptedException, NotLoggedInException {
|
InterruptedException, NotLoggedInException {
|
||||||
String queryId = UUID.randomUUID().toString();
|
String queryId = StringUtils.secureUniqueRandomString();
|
||||||
MamQueryIQ mamQueryIq = new MamQueryIQ(queryId, node, null);
|
MamQueryIQ mamQueryIq = new MamQueryIQ(queryId, node, null);
|
||||||
mamQueryIq.setTo(archiveAddress);
|
mamQueryIq.setTo(archiveAddress);
|
||||||
|
|
||||||
|
@ -601,7 +600,8 @@ public final class MamManager extends Manager {
|
||||||
|
|
||||||
private List<Message> page(RSMSet requestRsmSet) throws NoResponseException, XMPPErrorException,
|
private List<Message> page(RSMSet requestRsmSet) throws NoResponseException, XMPPErrorException,
|
||||||
NotConnectedException, NotLoggedInException, InterruptedException {
|
NotConnectedException, NotLoggedInException, InterruptedException {
|
||||||
MamQueryIQ mamQueryIQ = new MamQueryIQ(UUID.randomUUID().toString(), node, form);
|
String queryId = StringUtils.secureUniqueRandomString();
|
||||||
|
MamQueryIQ mamQueryIQ = new MamQueryIQ(queryId, node, form);
|
||||||
mamQueryIQ.setType(IQ.Type.set);
|
mamQueryIQ.setType(IQ.Type.set);
|
||||||
mamQueryIQ.setTo(archiveAddress);
|
mamQueryIQ.setTo(archiveAddress);
|
||||||
mamQueryIQ.addExtension(requestRsmSet);
|
mamQueryIQ.addExtension(requestRsmSet);
|
||||||
|
|
|
@ -16,8 +16,6 @@
|
||||||
*/
|
*/
|
||||||
package org.jivesoftware.smackx.sid.element;
|
package org.jivesoftware.smackx.sid.element;
|
||||||
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import org.jivesoftware.smack.packet.ExtensionElement;
|
import org.jivesoftware.smack.packet.ExtensionElement;
|
||||||
import org.jivesoftware.smack.util.StringUtils;
|
import org.jivesoftware.smack.util.StringUtils;
|
||||||
|
|
||||||
|
@ -28,7 +26,7 @@ public abstract class StableAndUniqueIdElement implements ExtensionElement {
|
||||||
private final String id;
|
private final String id;
|
||||||
|
|
||||||
public StableAndUniqueIdElement() {
|
public StableAndUniqueIdElement() {
|
||||||
this.id = UUID.randomUUID().toString();
|
this.id = StringUtils.secureUniqueRandomString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getId() {
|
public String getId() {
|
||||||
|
|
|
@ -65,7 +65,7 @@ public class StableUniqueStanzaIdTest extends SmackTestSuite {
|
||||||
OriginIdElement element = new OriginIdElement();
|
OriginIdElement element = new OriginIdElement();
|
||||||
assertNotNull(element);
|
assertNotNull(element);
|
||||||
assertEquals(StableUniqueStanzaIdManager.NAMESPACE, element.getNamespace());
|
assertEquals(StableUniqueStanzaIdManager.NAMESPACE, element.getNamespace());
|
||||||
assertEquals(36, element.getId().length());
|
assertEquals(16, element.getId().length());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -20,7 +20,6 @@ import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Random;
|
|
||||||
import java.util.WeakHashMap;
|
import java.util.WeakHashMap;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
@ -36,6 +35,7 @@ import org.jivesoftware.smack.XMPPException;
|
||||||
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
|
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
|
||||||
import org.jivesoftware.smack.packet.IQ;
|
import org.jivesoftware.smack.packet.IQ;
|
||||||
import org.jivesoftware.smack.packet.StanzaError;
|
import org.jivesoftware.smack.packet.StanzaError;
|
||||||
|
import org.jivesoftware.smack.util.StringUtils;
|
||||||
|
|
||||||
import org.jivesoftware.smackx.bytestreams.BytestreamListener;
|
import org.jivesoftware.smackx.bytestreams.BytestreamListener;
|
||||||
import org.jivesoftware.smackx.bytestreams.BytestreamManager;
|
import org.jivesoftware.smackx.bytestreams.BytestreamManager;
|
||||||
|
@ -138,9 +138,6 @@ public final class InBandBytestreamManager extends Manager implements Bytestream
|
||||||
/* prefix used to generate session IDs */
|
/* prefix used to generate session IDs */
|
||||||
private static final String SESSION_ID_PREFIX = "jibb_";
|
private static final String SESSION_ID_PREFIX = "jibb_";
|
||||||
|
|
||||||
/* random generator to create session IDs */
|
|
||||||
private static final Random randomGenerator = new Random();
|
|
||||||
|
|
||||||
/* stores one InBandBytestreamManager for each XMPP connection */
|
/* stores one InBandBytestreamManager for each XMPP connection */
|
||||||
private static final Map<XMPPConnection, InBandBytestreamManager> managers = new WeakHashMap<>();
|
private static final Map<XMPPConnection, InBandBytestreamManager> managers = new WeakHashMap<>();
|
||||||
|
|
||||||
|
@ -490,7 +487,7 @@ public final class InBandBytestreamManager extends Manager implements Bytestream
|
||||||
private static String getNextSessionID() {
|
private static String getNextSessionID() {
|
||||||
StringBuilder buffer = new StringBuilder();
|
StringBuilder buffer = new StringBuilder();
|
||||||
buffer.append(SESSION_ID_PREFIX);
|
buffer.append(SESSION_ID_PREFIX);
|
||||||
buffer.append(randomGenerator.nextInt(Integer.MAX_VALUE) + randomGenerator.nextInt(Integer.MAX_VALUE));
|
buffer.append(StringUtils.secureOnlineAttackSafeRandomString());
|
||||||
return buffer.toString();
|
return buffer.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,6 @@ import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Random;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.WeakHashMap;
|
import java.util.WeakHashMap;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
@ -44,6 +43,7 @@ import org.jivesoftware.smack.XMPPException.XMPPErrorException;
|
||||||
import org.jivesoftware.smack.packet.IQ;
|
import org.jivesoftware.smack.packet.IQ;
|
||||||
import org.jivesoftware.smack.packet.Stanza;
|
import org.jivesoftware.smack.packet.Stanza;
|
||||||
import org.jivesoftware.smack.packet.StanzaError;
|
import org.jivesoftware.smack.packet.StanzaError;
|
||||||
|
import org.jivesoftware.smack.util.StringUtils;
|
||||||
|
|
||||||
import org.jivesoftware.smackx.bytestreams.BytestreamListener;
|
import org.jivesoftware.smackx.bytestreams.BytestreamListener;
|
||||||
import org.jivesoftware.smackx.bytestreams.BytestreamManager;
|
import org.jivesoftware.smackx.bytestreams.BytestreamManager;
|
||||||
|
@ -114,9 +114,6 @@ public final class Socks5BytestreamManager extends Manager implements Bytestream
|
||||||
/* prefix used to generate session IDs */
|
/* prefix used to generate session IDs */
|
||||||
private static final String SESSION_ID_PREFIX = "js5_";
|
private static final String SESSION_ID_PREFIX = "js5_";
|
||||||
|
|
||||||
/* random generator to create session IDs */
|
|
||||||
private static final Random randomGenerator = new Random();
|
|
||||||
|
|
||||||
/* stores one Socks5BytestreamManager for each XMPP connection */
|
/* stores one Socks5BytestreamManager for each XMPP connection */
|
||||||
private static final Map<XMPPConnection, Socks5BytestreamManager> managers = new WeakHashMap<>();
|
private static final Map<XMPPConnection, Socks5BytestreamManager> managers = new WeakHashMap<>();
|
||||||
|
|
||||||
|
@ -759,7 +756,7 @@ public final class Socks5BytestreamManager extends Manager implements Bytestream
|
||||||
private static String getNextSessionID() {
|
private static String getNextSessionID() {
|
||||||
StringBuilder buffer = new StringBuilder();
|
StringBuilder buffer = new StringBuilder();
|
||||||
buffer.append(SESSION_ID_PREFIX);
|
buffer.append(SESSION_ID_PREFIX);
|
||||||
buffer.append(randomGenerator.nextInt(Integer.MAX_VALUE) + randomGenerator.nextInt(Integer.MAX_VALUE));
|
buffer.append(StringUtils.secureOnlineAttackSafeRandomString());
|
||||||
return buffer.toString();
|
return buffer.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,6 @@ package org.jivesoftware.smack.chat;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.WeakHashMap;
|
import java.util.WeakHashMap;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.CopyOnWriteArraySet;
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
|
@ -42,6 +41,7 @@ import org.jivesoftware.smack.filter.ThreadFilter;
|
||||||
import org.jivesoftware.smack.packet.Message;
|
import org.jivesoftware.smack.packet.Message;
|
||||||
import org.jivesoftware.smack.packet.Message.Type;
|
import org.jivesoftware.smack.packet.Message.Type;
|
||||||
import org.jivesoftware.smack.packet.Stanza;
|
import org.jivesoftware.smack.packet.Stanza;
|
||||||
|
import org.jivesoftware.smack.util.StringUtils;
|
||||||
|
|
||||||
import org.jxmpp.jid.EntityBareJid;
|
import org.jxmpp.jid.EntityBareJid;
|
||||||
import org.jxmpp.jid.EntityJid;
|
import org.jxmpp.jid.EntityJid;
|
||||||
|
@ -407,7 +407,7 @@ public final class ChatManager extends Manager{
|
||||||
* @return the next id.
|
* @return the next id.
|
||||||
*/
|
*/
|
||||||
private static String nextID() {
|
private static String nextID() {
|
||||||
return UUID.randomUUID().toString();
|
return StringUtils.secureUniqueRandomString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setDefaultMatchMode(MatchMode mode) {
|
public static void setDefaultMatchMode(MatchMode mode) {
|
||||||
|
|
|
@ -18,9 +18,9 @@ package org.jivesoftware.smackx.ox.util;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.jivesoftware.smack.util.StringUtils;
|
||||||
import org.jivesoftware.smack.util.stringencoder.Base64;
|
import org.jivesoftware.smack.util.stringencoder.Base64;
|
||||||
|
|
||||||
import org.jivesoftware.smackx.ox.crypto.OpenPgpProvider;
|
import org.jivesoftware.smackx.ox.crypto.OpenPgpProvider;
|
||||||
|
@ -52,26 +52,7 @@ public class SecretKeyBackupHelper {
|
||||||
* @return backup code
|
* @return backup code
|
||||||
*/
|
*/
|
||||||
public static String generateBackupPassword() {
|
public static String generateBackupPassword() {
|
||||||
final String alphabet = "123456789ABCDEFGHIJKLMNPQRSTUVWXYZ";
|
return StringUtils.secureOfflineAttackSafeRandomString();
|
||||||
final int len = alphabet.length();
|
|
||||||
SecureRandom random = new SecureRandom();
|
|
||||||
StringBuilder code = new StringBuilder(29);
|
|
||||||
|
|
||||||
// 6 blocks
|
|
||||||
for (int i = 0; i < 6; i++) {
|
|
||||||
|
|
||||||
// of 4 chars
|
|
||||||
for (int j = 0; j < 4; j++) {
|
|
||||||
char c = alphabet.charAt(random.nextInt(len));
|
|
||||||
code.append(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
// dash after every block except the last one
|
|
||||||
if (i != 5) {
|
|
||||||
code.append('-');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return code.toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in a new issue