org.wildfly.security.sasl.util.StringPrep Maven / Gradle / Ivy
The newest version!
/*
* JBoss, Home of Professional Open Source.
* Copyright 2014 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.wildfly.security.sasl.util;
import static org.wildfly.security.sasl._private.ElytronMessages.log;
import java.text.Normalizer;
/**
* Preparation of Internationalized Strings ("stringprep") by RFC 3454
*
* @author David M. Lloyd
* @author Jan Kalina
*/
public final class StringPrep {
// these flags must keep their numeric values permanently, and must not conflict
// mappings
public static final long MAP_TO_NOTHING = 1L << 0;
public static final long MAP_TO_SPACE = 1L << 1;
// XXX case folding would go here
public static final long MAP_SCRAM_LOGIN_CHARS = 1L << 30;
public static final long MAP_GS2_LOGIN_CHARS = 1L << 31;
public static final long UNMAP_SCRAM_LOGIN_CHARS = 1L << 28;
public static final long UNMAP_GS2_LOGIN_CHARS = 1L << 29;
// normalizations
public static final long NORMALIZE_KC = 1L << 2;
// prohibitions
public static final long FORBID_NON_ASCII_SPACES = 1L << 3;
public static final long FORBID_ASCII_CONTROL = 1L << 4;
public static final long FORBID_NON_ASCII_CONTROL = 1L << 5;
public static final long FORBID_PRIVATE_USE = 1L << 6;
public static final long FORBID_NON_CHARACTER = 1L << 7;
public static final long FORBID_SURROGATE = 1L << 8;
public static final long FORBID_INAPPROPRIATE_FOR_PLAIN_TEXT = 1L << 9;
public static final long FORBID_INAPPROPRIATE_FOR_CANON_REP = 1L << 10;
public static final long FORBID_CHANGE_DISPLAY_AND_DEPRECATED = 1L << 11;
public static final long FORBID_TAGGING = 1L << 12;
public static final long FORBID_UNASSIGNED = 1L << 13;
public static final long PROFILE_SASL_QUERY = 0
| MAP_TO_NOTHING
| MAP_TO_SPACE
| NORMALIZE_KC
| FORBID_NON_ASCII_SPACES
| FORBID_ASCII_CONTROL
| FORBID_NON_ASCII_CONTROL
| FORBID_PRIVATE_USE
| FORBID_NON_CHARACTER
| FORBID_SURROGATE
| FORBID_INAPPROPRIATE_FOR_PLAIN_TEXT
| FORBID_INAPPROPRIATE_FOR_CANON_REP
| FORBID_CHANGE_DISPLAY_AND_DEPRECATED
| FORBID_TAGGING;
public static final long PROFILE_SASL_STORED = 0
| PROFILE_SASL_QUERY
| FORBID_UNASSIGNED;
public static final long PROFILE_SASL_STORED_NON_NORMALIZED = (0
| PROFILE_SASL_STORED)
& ~NORMALIZE_KC;
// StringPrep section 3 - Mapping
public static boolean mapCodePointToNothing(int input) {
return input == 0xAD
|| input == 0x034F
|| input == 0x1806
|| input >= 0x180B && input <= 0x180D
|| input >= 0x200B && input <= 0x200D
|| input == 0x2060
|| input >= 0xFE00 && input <= 0xFE0F
|| input == 0xFEFF;
}
public static boolean mapCodePointToSpace(int input) {
return input == 0xA0
|| input == 0x1680
|| input >= 0x2000 && input <= 0x200B
|| input == 0x202F
|| input == 0x205F
|| input == 0x3000;
}
// StringPrep section 5 - Prohibited I/O
public static void forbidNonAsciiSpaces(int input) {
if (mapCodePointToSpace(input)) {
throw log.invalidNonAsciiSpace(input);
}
}
public static void forbidAsciiControl(int input) {
if (input < 0x20 || input == 0x7F) {
throw log.invalidAsciiControl(input);
}
}
public static void forbidNonAsciiControl(int input) {
if (input >= 0x80 && input <= 0x9F
|| input == 0x06DD
|| input == 0x070F
|| input == 0x180E
|| input >= 0x200C && input <= 0x200D
|| input >= 0x2028 && input <= 0x2029
|| input >= 0x2060 && input <= 0x2063
|| input >= 0x206A && input <= 0x206F
|| input == 0xFEFF
|| input >= 0xFFF9 && input <= 0xFFFC
|| input >= 0x01D173 && input <= 0x01D17A
) {
throw log.invalidNonAsciiControl(input);
}
}
public static void forbidPrivateUse(int input) {
if (input >= 0xE000 && input <= 0xF8FF || input >= 0xF0000 && input <= 0xFFFFD || input >= 0x100000 && input <= 0x10FFFD) {
throw log.invalidPrivateUseCharacter(input);
}
}
public static void forbidNonCharacter(int input) {
if ((input & 0xFFFE) == 0xFFFE || input >= 0xFDD0 && input <= 0xFDEF) {
throw log.invalidNonCharacterCodePoint(input);
}
}
public static void forbidSurrogate(int input) {
if (input >= 0xD800 && input <= 0xDFFF) {
throw log.invalidSurrogateCodePoint(input);
}
}
public static void forbidInappropriateForPlainText(int input) {
if (input >= 0xFFF9 && input <= 0xFFFD) {
throw log.invalidPlainTextCodePoint(input);
}
}
public static void forbidInappropriateForCanonicalRepresentation(int input) {
if (input >= 0x2FF0 && input <= 0x2FFB) {
throw log.invalidNonCanonicalCodePoint(input);
}
}
public static void forbidChangeDisplayPropertiesOrDeprecated(int input) {
if (input >= 0x0340 && input <= 0x0341
|| input >= 0x200E && input <= 0x200F
|| input >= 0x202A && input <= 0x202E
|| input >= 0x206A && input <= 0x206F) {
throw log.invalidControlCharacter(input);
}
}
public static void forbidTagging(int input) {
if (input == 0x0E0001 || input >= 0x0E0020 && input <= 0x0E007F) {
throw log.invalidTaggingCharacter(input);
}
}
public static void forbidUnassigned(int input) {
if (Character.getType(input) == Character.UNASSIGNED) {
throw log.unassignedCodePoint(input);
}
}
private static boolean isSet(long test, long bit) {
return (test & bit) != 0L;
}
// Encoding
public static void encode(char[] string, org.wildfly.common.bytes.ByteStringBuilder target, long profile) {
encode(new String(string), target, profile);
}
public static void encode(String string, org.wildfly.common.bytes.ByteStringBuilder target, long profile) {
// technically we're supposed to normalize after mapping, but it should be equivalent if we don't
if (isSet(profile, NORMALIZE_KC)) string = Normalizer.normalize(string, Normalizer.Form.NFKC);
final int len = string.length();
boolean isRALString = false;
boolean first = true;
int i = 0;
while (i < len) {
char ch = string.charAt(i++);
int cp;
if (Character.isHighSurrogate(ch)) {
if (i == len) {
throw log.invalidSurrogatePairHightAtEnd(ch);
}
char low = string.charAt(i++);
if (!Character.isLowSurrogate(low)) {
throw log.invalidSurrogatePairSecondIsNotLow(ch, low);
}
cp = Character.toCodePoint(ch, low);
} else if (Character.isLowSurrogate(ch)) {
throw log.invalidSurrogatePairLowWithoutHigh(ch);
} else {
cp = ch;
}
if (!Character.isValidCodePoint(cp)) {
throw log.invalidCodePoint(cp);
}
assert Character.MIN_CODE_POINT <= cp && cp <= Character.MAX_CODE_POINT;
// StringPrep 6 - Bidirectional Characters
switch (Character.getDirectionality(cp)) {
case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC:
case Character.DIRECTIONALITY_RIGHT_TO_LEFT: // R/AL character
if (first) {
isRALString = true;
} else if (!isRALString) {
throw log.disallowedRalDirectionalityInL();
}
break;
case Character.DIRECTIONALITY_LEFT_TO_RIGHT: // L character
if (isRALString) {
throw log.disallowedLDirectionalityInRal();
}
break;
default: // neutral character
if (i == len && isRALString) {
throw log.missingTrailingRal();
}
}
if (first) {
first = false;
}
// StringPrep 3 - Mapping
if (isSet(profile, MAP_TO_NOTHING) && mapCodePointToNothing(cp)) continue;
if (isSet(profile, MAP_TO_SPACE) && mapCodePointToSpace(cp)) {
target.append(' ');
continue;
}
// Escaping used in GS2 and SCRAM username/authzid (RFC 5801,5802)
if (isSet(profile, MAP_SCRAM_LOGIN_CHARS) || isSet(profile, MAP_GS2_LOGIN_CHARS)) {
if (cp == '=') {
target.append('=').append('3').append('D');
continue;
} else if (cp == ',') {
target.append('=').append('2').append('C');
continue;
}
}
// Unescaping used in GS2 and SCRAM username/authzid (RFC 5801,5802)
else if (isSet(profile, UNMAP_SCRAM_LOGIN_CHARS) || isSet(profile, UNMAP_GS2_LOGIN_CHARS)) {
if (cp == '=') {
if (i + 1 >= len) {
throw log.invalidEscapeSequence();
}
char ch1 = string.charAt(i++);
char ch2 = string.charAt(i++);
if (ch1 == '3' && ch2 == 'D') {
target.append('=');
continue;
} else if (ch1 == '2' && ch2 == 'C') {
target.append(',');
continue;
} else {
throw log.invalidEscapeSequence();
}
}
}
// StringPrep 5 - Prohibition
if (isSet(profile, FORBID_NON_ASCII_SPACES)) forbidNonAsciiSpaces(cp);
if (isSet(profile, FORBID_ASCII_CONTROL)) forbidAsciiControl(cp);
if (isSet(profile, FORBID_NON_ASCII_CONTROL)) forbidNonAsciiControl(cp);
if (isSet(profile, FORBID_PRIVATE_USE)) forbidPrivateUse(cp);
if (isSet(profile, FORBID_NON_CHARACTER)) forbidNonCharacter(cp);
if (isSet(profile, FORBID_SURROGATE)) forbidSurrogate(cp);
if (isSet(profile, FORBID_INAPPROPRIATE_FOR_PLAIN_TEXT)) forbidInappropriateForPlainText(cp);
if (isSet(profile, FORBID_INAPPROPRIATE_FOR_CANON_REP)) forbidInappropriateForCanonicalRepresentation(cp);
if (isSet(profile, FORBID_CHANGE_DISPLAY_AND_DEPRECATED)) forbidChangeDisplayPropertiesOrDeprecated(cp);
if (isSet(profile, FORBID_TAGGING)) forbidTagging(cp);
if (isSet(profile, FORBID_UNASSIGNED)) forbidUnassigned(cp);
// Now, encode that one
target.appendUtf8Raw(cp);
}
}
}