-
Notifications
You must be signed in to change notification settings - Fork 154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Do not persist plaintext secrets in the metastore #438
Changes from 14 commits
94f7a03
5bc2530
8cde7a5
54f01ce
cfae372
0d085b3
3cc38a8
18d5377
3e2b0f8
7f490b0
0c054f5
8e0a752
203d534
b856e9b
3236172
bf648ec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,7 @@ | |
import com.fasterxml.jackson.annotation.JsonCreator; | ||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import java.security.SecureRandom; | ||
import org.apache.commons.codec.digest.DigestUtils; | ||
|
||
/** | ||
* Simple class to represent the secrets used to authenticate a catalog principal, These secrets are | ||
|
@@ -37,12 +38,20 @@ public class PolarisPrincipalSecrets { | |
// the client id for that principal | ||
private final String principalClientId; | ||
|
||
// the main secret for that principal | ||
// the main secret hash for that principal | ||
private String mainSecret; | ||
|
||
// the secondary secret for that principal | ||
private String secondarySecret; | ||
|
||
// Hash of mainSecret | ||
private String mainSecretHash; | ||
|
||
// Hash of secondarySecret | ||
private String secondarySecretHash; | ||
|
||
private String secretSalt; | ||
|
||
/** | ||
* Generate a secure random string | ||
* | ||
|
@@ -64,40 +73,78 @@ private String generateRandomHexString(int stringLength) { | |
return sb.toString(); | ||
} | ||
|
||
private String hashSecret(String secret) { | ||
return DigestUtils.sha256Hex(secret + ":" + secretSalt); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is probably advisable to use different salt values for each secret (for the same reason why different salt values are used for different users). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am actually not clear on why/if we need two secrets per user. If we turn out to only want to use one, then just having the one salt is fine. If we are really using both secrets, then probably having two salts for the two secrets is better. |
||
} | ||
|
||
@JsonCreator | ||
public PolarisPrincipalSecrets( | ||
@JsonProperty("principalId") long principalId, | ||
@JsonProperty("principalClientId") String principalClientId, | ||
@JsonProperty("mainSecret") String mainSecret, | ||
@JsonProperty("secondarySecret") String secondarySecret) { | ||
@JsonProperty("secondarySecret") String secondarySecret, | ||
@JsonProperty("secretSalt") String secretSalt, | ||
@JsonProperty("mainSecretHash") String mainSecretHash, | ||
@JsonProperty("secondarySecretHash") String secondarySecretHash) { | ||
this.principalId = principalId; | ||
this.principalClientId = principalClientId; | ||
this.mainSecret = mainSecret; | ||
this.secondarySecret = secondarySecret; | ||
|
||
this.secretSalt = secretSalt; | ||
if (this.secretSalt == null) { | ||
this.secretSalt = generateRandomHexString(16); | ||
} | ||
this.mainSecretHash = mainSecretHash; | ||
if (this.mainSecretHash == null) { | ||
this.mainSecretHash = hashSecret(mainSecret); | ||
} | ||
this.secondarySecretHash = secondarySecretHash; | ||
if (this.secondarySecretHash == null) { | ||
this.secondarySecretHash = hashSecret(secondarySecret); | ||
} | ||
} | ||
|
||
public PolarisPrincipalSecrets( | ||
long principalId, String principalClientId, String mainSecret, String secondarySecret) { | ||
this.principalId = principalId; | ||
this.principalClientId = principalClientId; | ||
this.mainSecret = mainSecret; | ||
this.secondarySecret = secondarySecret; | ||
|
||
this.secretSalt = generateRandomHexString(16); | ||
this.mainSecretHash = hashSecret(mainSecret); | ||
this.secondarySecretHash = hashSecret(secondarySecret); | ||
} | ||
|
||
public PolarisPrincipalSecrets(PolarisPrincipalSecrets principalSecrets) { | ||
this.principalId = principalSecrets.getPrincipalId(); | ||
this.principalClientId = principalSecrets.getPrincipalClientId(); | ||
this.mainSecret = principalSecrets.getMainSecret(); | ||
this.secondarySecret = principalSecrets.getSecondarySecret(); | ||
this.secretSalt = principalSecrets.getSecretSalt(); | ||
this.mainSecretHash = principalSecrets.getMainSecretHash(); | ||
this.secondarySecretHash = principalSecrets.getSecondarySecretHash(); | ||
} | ||
|
||
public PolarisPrincipalSecrets(long principalId) { | ||
this.principalId = principalId; | ||
this.principalClientId = this.generateRandomHexString(16); | ||
this.mainSecret = this.generateRandomHexString(32); | ||
this.secondarySecret = this.generateRandomHexString(32); | ||
|
||
this.secretSalt = this.generateRandomHexString(16); | ||
this.mainSecretHash = hashSecret(mainSecret); | ||
this.secondarySecretHash = hashSecret(secondarySecret); | ||
} | ||
|
||
/** | ||
* Rotate the main secrets | ||
* | ||
* @param mainSecretToRotate the main secrets to rotate | ||
*/ | ||
public void rotateSecrets(String mainSecretToRotate) { | ||
this.secondarySecret = mainSecretToRotate; | ||
/** Rotate the main secrets */ | ||
public void rotateSecrets(String newSecondaryHash) { | ||
this.secondarySecret = null; | ||
this.secondarySecretHash = newSecondaryHash; | ||
|
||
this.mainSecret = this.generateRandomHexString(32); | ||
this.mainSecretHash = hashSecret(mainSecret); | ||
} | ||
|
||
public long getPrincipalId() { | ||
|
@@ -108,11 +155,29 @@ public String getPrincipalClientId() { | |
return principalClientId; | ||
} | ||
|
||
public boolean matchesSecret(String potentialSecret) { | ||
String potentialSecretHash = hashSecret(potentialSecret); | ||
return potentialSecretHash.equals(this.mainSecretHash) | ||
|| potentialSecretHash.equals(this.secondarySecretHash); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to support rolling upgrades (when requests may submit the old plain text secrets for validation)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1, I am not clear on this. I put a note in the description, but I basically copied what I saw as the existing behavior. I am open to keeping/changing this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds like an easy enough thing to add for now to prevent a backwards incompatibility, but maybe we should just only do so if the "hash" is not set? Then the moment hash is set only accept a hashed comparison? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, clients will take time to update their configuration. How about we allow plain test password even when a hash is set, but until the next password rotation? Meaning: remove plain text passwords during rotation if hash is set. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (This is just an idea from my side. I do not mean to say that supporting plain text passwords and rolling upgrades are required from my POV :) ) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The way it works right now is this:
I think this is the correct behavior iff we want to have secrets be valid after one There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @eric-maynard : Apologies, I misinterpreted the code. You're right in how it works. I think the PR is good to merge. |
||
} | ||
|
||
public String getMainSecret() { | ||
return mainSecret; | ||
} | ||
|
||
public String getSecondarySecret() { | ||
return secondarySecret; | ||
} | ||
|
||
public String getMainSecretHash() { | ||
return mainSecretHash; | ||
} | ||
|
||
public String getSecondarySecretHash() { | ||
return secondarySecretHash; | ||
} | ||
|
||
public String getSecretSalt() { | ||
return secretSalt; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, the reason we passed in the original secret was for idempotence. It's imperative to make sure that if a rotate request is sent twice accidentally (e.g., client retry), the keys aren't rotated twice
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is a good idea, we should probably document that on the method if it isn't already