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
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.Map;
Expand All @@ -47,6 +48,14 @@ public class PasswordUtils {
public static final int DEFAULT_ITERATION_COUNT = 17;
public static final byte[] DEFAULT_INITIAL_VECTOR = new byte[16];
private static final String LEN_SEPARATOR_STR = ":";
private static final String MSG_ENCRYPT_KEY_STILL_DEFAULT = "ranger.password.encryption.key is not set — it is still the default, publicly-known " +
"value that ships with every Ranger install (in source control and every tarball), so encrypting with it would protect nothing. Set " +
"ranger.password.encryption.key to a unique, secret value in ranger-admin-site.xml — the exact same value on every Admin node in this " +
"cluster — before service config passwords can be encrypted.";

private static final String V2_FORMAT_PREFIX = "v2,";
private static final String LEGACY_ENV_KEY_OVERRIDE_NAME = "lEncryptKey";
private static final String LEGACY_ENV_SALT_OVERRIDE_NAME = "ENCRYPT_SALT";

private final RangerSupportedCryptoAlgo cryptAlgo;
private final int iterationCount;
Expand Down Expand Up @@ -112,6 +121,15 @@ public class PasswordUtils {
}
}

private PasswordUtils(RangerSupportedCryptoAlgo cryptAlgo, char[] key, byte[] salt, int iterationCount, byte[] iv, String password) {
this.cryptAlgo = cryptAlgo;
this.encryptKey = key;
this.salt = salt;
this.iterationCount = iterationCount;
this.iv = iv;
this.password = password;
}

public static String encryptPassword(String aPassword) throws IOException {
return build(aPassword).encrypt();
}
Expand All @@ -124,6 +142,126 @@ public static String decryptPassword(String aPassword) throws IOException {
return build(aPassword).decrypt();
}

public static boolean isV2Format(String storedValue) {
if (storedValue == null || !storedValue.startsWith(V2_FORMAT_PREFIX)) {
return false;
}
String[] fields = storedValue.substring(V2_FORMAT_PREFIX.length()).split(",", 2);
if (fields.length == 0 || StringUtils.isEmpty(fields[0])) {
return false;
}
try {
RangerSupportedCryptoAlgo.getValueOf(fields[0]);
return true;
} catch (Exception e) {
return false;
}
}

public static String getCryptAlgoV2(String storedValue) {
if (!isV2Format(storedValue)) {
throw new IllegalArgumentException("value is not in v2 format (missing '" + V2_FORMAT_PREFIX + "' prefix)");
}
return storedValue.substring(V2_FORMAT_PREFIX.length()).split(",", 3)[0];
}

public static boolean isLegacyFormat(String storedValue) {
if (StringUtils.isEmpty(storedValue) || isV2Format(storedValue) || !storedValue.contains(",")) {
return false;
}
String[] fields = storedValue.split(",", -1);
if (fields.length <= 4) {
return false;
}
try {
RangerSupportedCryptoAlgo.getValueOf(fields[0]);
return true;
} catch (Exception e) {
return false;
}
}

public static void validateEncryptionKeyConfigured(char[] key) {
if (key == null || key.length == 0) {
throw new IllegalArgumentException("ranger.password.encryption.key is not set.");
}
if (Arrays.equals(key, DEFAULT_ENCRYPT_KEY.toCharArray())) {
throw new IllegalStateException(MSG_ENCRYPT_KEY_STILL_DEFAULT);
}
}

public static void checkNoLegacyEnvKeyOverride() {
Map<String, String> env = System.getenv();
if (env.get(LEGACY_ENV_KEY_OVERRIDE_NAME) != null || env.get(LEGACY_ENV_SALT_OVERRIDE_NAME) != null) {
throw new IllegalStateException("This node has the legacy '" + LEGACY_ENV_KEY_OVERRIDE_NAME + "' and/or '" + LEGACY_ENV_SALT_OVERRIDE_NAME +
"' environment variable set, which silently overrides the key/salt used by legacy (v1) password encryption. The v2 format and this " +
"migration intentionally do not honor this override — they use only ranger.password.encryption.key — so migrating now would decrypt " +
"existing data under the env-var key but re-encrypt it under the configured property, silently changing which secret protects it. " +
"Confirm whether this environment variable is actually relied on before proceeding: if it is, reconcile ranger.password.encryption.key " +
"to match it first; if it is unused/vestigial, unset it, then re-run this migration.");
}
}

public static String encryptPasswordV2(String plainText, RangerSupportedCryptoAlgo cryptAlgo, char[] key, byte[] salt, int iterationCount) throws IOException {
if (key == null || key.length == 0) {
throw new IllegalArgumentException("encryptPasswordV2() requires a non-empty key");
}
try {
String ivStr = generateIvIfNeeded(cryptAlgo.getAlgoName());
byte[] ivBytes = ivStr != null ? Base64.getDecoder().decode(ivStr) : DEFAULT_INITIAL_VECTOR;
String password = new PasswordUtils(cryptAlgo, key, salt, iterationCount, ivBytes, plainText).encrypt();
List<String> fields = new ArrayList<>();
fields.add(cryptAlgo.getAlgoName());
fields.add(Base64.getEncoder().encodeToString(salt));
fields.add(String.valueOf(iterationCount));
if (ivStr != null) {
fields.add(ivStr);
}
fields.add(password);
return V2_FORMAT_PREFIX + String.join(",", fields);
} catch (NoSuchAlgorithmException e) {
throw new IOException("Unable to generate IV for v2 password encryption", e);
}
}

public static String decryptPasswordV2(String storedValue, char[] key) throws IOException {
if (key == null || key.length == 0) {
throw new IllegalArgumentException("decryptPasswordV2() requires a non-empty key — refusing to fall back to a default");
}
if (!isV2Format(storedValue)) {
throw new IllegalArgumentException("value is not in v2 format (missing '" + V2_FORMAT_PREFIX + "' prefix)");
}
String[] fields = Lists.newArrayList(Splitter.on(",").split(storedValue.substring(V2_FORMAT_PREFIX.length()))).toArray(new String[0]);
if (fields.length < 4) {
throw new IOException("Malformed v2 password value (expected at least 4 fields, found " + fields.length + ")");
}

int index = 0;
RangerSupportedCryptoAlgo cryptAlgo = RangerSupportedCryptoAlgo.getValueOf(fields[index++]);
byte[] salt = Base64.getDecoder().decode(fields[index++]);
int iterationCount = Integer.parseInt(fields[index++]);
byte[] iv;
if (needsIv(cryptAlgo.getAlgoName())) {
if (fields.length < 5) {
throw new IOException("Malformed v2 password value (algorithm " + cryptAlgo.getAlgoName() + " requires an IV; expected at least 5 fields, found " + fields.length + ")");
}
iv = Base64.getDecoder().decode(fields[index++]);
} else {
iv = DEFAULT_INITIAL_VECTOR;
}
StringBuilder cipherText = new StringBuilder(fields[index++]);
// defensive: a stray comma inside the ciphertext (shouldn't happen — Base64's alphabet
// has none) would otherwise silently truncate the value instead of failing loudly.
for (int i = index; i < fields.length; i++) {
cipherText.append(",").append(fields[i]);
}
String result = new PasswordUtils(cryptAlgo, key, salt, iterationCount, iv, cipherText.toString()).decrypt();
if (result == null) {
throw new IOException("decryptPasswordV2() failed — wrong key or corrupted value (decrypted output was not in the expected format)");
}
return result;
}

public static boolean needsIv(String cryptoAlgo) {
if (StringUtils.isEmpty(cryptoAlgo)) {
return false;
Expand Down
Loading