mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2026-05-23 08:55:50 +08:00
Merge remote-tracking branch 'origin/patch'
This commit is contained in:
@@ -351,12 +351,22 @@ public class Repository implements FileSystemListener, RepositoryLogger {
|
||||
validate();
|
||||
validateAdminPrivilege(currentUser);
|
||||
|
||||
Set<String> allUsers = Set.of(mgr.getAllUsers(currentUser));
|
||||
Set<String> updatedUsers = new HashSet<>();
|
||||
|
||||
LinkedHashMap<String, User> newUserMap = new LinkedHashMap<>();
|
||||
for (User user : users) {
|
||||
String userName = user.getName();
|
||||
if (UserManager.ANONYMOUS_USERNAME.equals(userName)) {
|
||||
if (UserManager.ANONYMOUS_USERNAME.equals(userName) ||
|
||||
!allUsers.contains(userName)) {
|
||||
continue; // ignore
|
||||
}
|
||||
if (!user.hasWritePermission() && !user.isReadOnly() && !user.isAdmin()) {
|
||||
throw new IOException("User specified with invalid permission: " + userName);
|
||||
}
|
||||
if (!updatedUsers.add(userName)) {
|
||||
throw new IOException("Duplicate user entry specified: " + userName);
|
||||
}
|
||||
newUserMap.put(userName, user);
|
||||
}
|
||||
User user = newUserMap.get(currentUser);
|
||||
@@ -508,7 +518,8 @@ public class Repository implements FileSystemListener, RepositoryLogger {
|
||||
* @throws UserAccessException if currentUser does not have admin priviledge
|
||||
* @throws IOException if an IO error occurs
|
||||
*/
|
||||
private void writeUserList(LinkedHashMap<String, User> newUserMap, boolean allowAnonymous)
|
||||
private synchronized void writeUserList(LinkedHashMap<String, User> newUserMap,
|
||||
boolean allowAnonymous)
|
||||
throws IOException {
|
||||
|
||||
File temp = new File(userAccessFile.getParentFile(), "tempAccess.tmp");
|
||||
|
||||
@@ -751,10 +751,10 @@ public class UserManager {
|
||||
}
|
||||
|
||||
/*
|
||||
* Regex: matches if the entire string is alpha, digit, ".", "-", "_", fwd or back slash.
|
||||
* Regex: matches if the entire string is alpha, digit, ".", "-", "_".
|
||||
*/
|
||||
private static final Pattern VALID_USERNAME_REGEX =
|
||||
Pattern.compile("[a-zA-Z0-9][a-zA-Z0-9.\\-_/\\\\]*");
|
||||
Pattern.compile("[a-zA-Z0-9][a-zA-Z0-9.\\-_]*");
|
||||
|
||||
/**
|
||||
* Ensures a name only contains valid characters.
|
||||
|
||||
+1
-1
@@ -718,7 +718,7 @@ public class RepositoryHandleImpl extends UnicastRemoteObject
|
||||
public void terminateCheckout(String parentPath, String itemName, long checkoutId,
|
||||
boolean notify) throws IOException {
|
||||
synchronized (syncObject) {
|
||||
validate(); // relax read-only restriction
|
||||
validate();
|
||||
try {
|
||||
RepositoryFile rf = getFile(parentPath, itemName);
|
||||
if (rf != null) {
|
||||
|
||||
+5
-26
@@ -39,10 +39,8 @@ import ghidra.server.remote.RemoteLoggingUtil;
|
||||
* use of a dual-signed token.
|
||||
*/
|
||||
public class PKIAuthenticationModule implements AuthenticationModule {
|
||||
static final Logger log = LogManager.getLogger(PKIAuthenticationModule.class);
|
||||
|
||||
private static final long MAX_TOKEN_TIME = 5 * 60000; // 5-minutes
|
||||
private static final int TOKEN_SIZE = 64;
|
||||
static final Logger log = LogManager.getLogger(PKIAuthenticationModule.class);
|
||||
|
||||
private X500Principal[] authorities; // imposed on client certificate
|
||||
private boolean anonymousAllowed;
|
||||
@@ -70,7 +68,7 @@ public class PKIAuthenticationModule implements AuthenticationModule {
|
||||
public Callback[] getAuthenticationCallbacks() {
|
||||
SignatureCallback sigCb;
|
||||
try {
|
||||
byte[] token = TokenGenerator.getNewToken(TOKEN_SIZE);
|
||||
byte[] token = TokenGenerator.getNewToken();
|
||||
boolean usingSelfSignedCert =
|
||||
DefaultKeyManagerFactory.usingGeneratedSelfSignedCertificate();
|
||||
SignedToken signedToken = DefaultKeyManagerFactory
|
||||
@@ -88,27 +86,6 @@ public class PKIAuthenticationModule implements AuthenticationModule {
|
||||
return false;
|
||||
}
|
||||
|
||||
private void checkTokenIntegrity(byte[] token) throws LoginException {
|
||||
if (token.length != TOKEN_SIZE) {
|
||||
throw new FailedLoginException("Invalid Signature callback");
|
||||
}
|
||||
|
||||
boolean isZeroToken = true;
|
||||
for (byte b : token) {
|
||||
if (b != 0) {
|
||||
isZeroToken = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isZeroToken) {
|
||||
throw new FailedLoginException("Invalid Signature callback");
|
||||
}
|
||||
|
||||
if (!TokenGenerator.isRecentToken(token, MAX_TOKEN_TIME)) {
|
||||
throw new FailedLoginException("Stale Signature callback");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* @see ghidra.server.security.AuthenticationModule#authenticate(ghidra.server.UserManager, javax.security.auth.Subject, javax.security.auth.callback.Callback[])
|
||||
*/
|
||||
@@ -142,7 +119,9 @@ public class PKIAuthenticationModule implements AuthenticationModule {
|
||||
try {
|
||||
|
||||
byte[] token = sigCb.getToken();
|
||||
checkTokenIntegrity(token);
|
||||
if (!TokenGenerator.isValidToken(token)) {
|
||||
throw new FailedLoginException("Stale Signature callback");
|
||||
}
|
||||
|
||||
boolean usingSelfSignedCert =
|
||||
DefaultKeyManagerFactory.usingGeneratedSelfSignedCertificate();
|
||||
|
||||
+3
-4
@@ -45,8 +45,7 @@ import ghidra.server.UserManager;
|
||||
*/
|
||||
public class SSHAuthenticationModule {
|
||||
|
||||
private static final long MAX_TOKEN_TIME = 10000;
|
||||
private static final int TOKEN_SIZE = 64;
|
||||
|
||||
|
||||
private final boolean nameCallbackAllowed;
|
||||
|
||||
@@ -72,7 +71,7 @@ public class SSHAuthenticationModule {
|
||||
if (addNameCallback) {
|
||||
list.add(new NameCallback("User ID:"));
|
||||
}
|
||||
byte[] token = TokenGenerator.getNewToken(TOKEN_SIZE);
|
||||
byte[] token = TokenGenerator.getNewToken();
|
||||
try {
|
||||
boolean usingSelfSignedCert =
|
||||
DefaultKeyManagerFactory.usingGeneratedSelfSignedCertificate();
|
||||
@@ -190,7 +189,7 @@ public class SSHAuthenticationModule {
|
||||
}
|
||||
|
||||
byte[] token = sshCb.getToken();
|
||||
if (!TokenGenerator.isRecentToken(token, MAX_TOKEN_TIME)) {
|
||||
if (!TokenGenerator.isValidToken(token)) {
|
||||
throw new FailedLoginException("Stale SSH Signature callback");
|
||||
}
|
||||
|
||||
|
||||
+66
-9
@@ -4,9 +4,9 @@
|
||||
* 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.
|
||||
@@ -17,27 +17,52 @@ package ghidra.server.security;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
import generic.random.SecureRandomFactory;
|
||||
|
||||
public class TokenGenerator {
|
||||
|
||||
static byte[] getNewToken(int size) {
|
||||
private static final long MAX_TTL_MS = 60_000; // max token time-to-live 60s
|
||||
|
||||
private static final int TOKEN_SIZE = 64;
|
||||
|
||||
private static CachedTokenSet tokenCache = new CachedTokenSet();
|
||||
|
||||
/**
|
||||
* {@return a single-use token byte sequence with embedded timestamp}
|
||||
*/
|
||||
static byte[] getNewToken() {
|
||||
SecureRandom random = SecureRandomFactory.getSecureRandom();
|
||||
byte[] token = new byte[size - 8];
|
||||
byte[] token = new byte[TOKEN_SIZE - 8];
|
||||
random.nextBytes(token);
|
||||
byte[] stampedToken = new byte[token.length + 8];
|
||||
byte[] stampedToken = new byte[TOKEN_SIZE];
|
||||
System.arraycopy(token, 0, stampedToken, 8, token.length);
|
||||
putLong(stampedToken, 0, (new Date()).getTime());
|
||||
tokenCache.add(stampedToken);
|
||||
return stampedToken;
|
||||
}
|
||||
|
||||
static boolean isRecentToken(byte[] token, long maxTime) {
|
||||
if (token.length < 8) {
|
||||
/**
|
||||
* Determine if the specified token has not yet been consumed and is still valid.
|
||||
* <p>
|
||||
* NOTE: This method may only be invoked once per token after which the token will become
|
||||
* invalid.
|
||||
*
|
||||
* @param token token previously issued
|
||||
* @return true if token is valid and now consumed
|
||||
*/
|
||||
static boolean isValidToken(byte[] token) {
|
||||
if (token.length != TOKEN_SIZE || !tokenCache.consume(token)) {
|
||||
return false;
|
||||
}
|
||||
long diff = (new Date()).getTime() - getLong(token, 0);
|
||||
return (diff >= 0 && diff < maxTime);
|
||||
long issueTime = getLong(token, 0);
|
||||
if (issueTime <= 0) {
|
||||
return false;
|
||||
}
|
||||
long diff = (new Date()).getTime() - issueTime;
|
||||
return (diff >= 0 && diff < MAX_TTL_MS);
|
||||
}
|
||||
|
||||
private static long getLong(byte[] data, int offset) {
|
||||
@@ -59,4 +84,36 @@ public class TokenGenerator {
|
||||
return ++offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link CachedTokenSet} tracks timed token issuance and insures that they remain
|
||||
* valid for one-time consumption within limited life-span.
|
||||
*/
|
||||
private static class CachedTokenSet {
|
||||
|
||||
private final Map<byte[], Long> cache = new ConcurrentHashMap<>();
|
||||
private final ScheduledExecutorService scheduler =
|
||||
Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
CachedTokenSet() {
|
||||
// Perform token cleanup every 5-seconds
|
||||
scheduler.scheduleAtFixedRate(this::cleanup, 5, 5, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
void add(byte[] token) {
|
||||
cache.put(token, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
boolean consume(byte[] value) {
|
||||
Long storedAt = cache.remove(value); // remove on retrieval
|
||||
if (storedAt == null)
|
||||
return false;
|
||||
return (System.currentTimeMillis() - storedAt < MAX_TTL_MS);
|
||||
}
|
||||
|
||||
private void cleanup() {
|
||||
long now = System.currentTimeMillis();
|
||||
cache.entrySet().removeIf(e -> now - e.getValue() >= MAX_TTL_MS);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -353,6 +353,7 @@ public class RepositoryFile {
|
||||
throws IOException {
|
||||
synchronized (fileSystem) {
|
||||
validate();
|
||||
repository.validateWritePrivilege(user); // don't allow update if read-only
|
||||
folderItem.updateCheckoutVersion(checkoutId, checkoutVersion, user);
|
||||
}
|
||||
}
|
||||
@@ -367,6 +368,7 @@ public class RepositoryFile {
|
||||
public void terminateCheckout(long checkoutId, String user, boolean notify) throws IOException {
|
||||
synchronized (fileSystem) {
|
||||
validate();
|
||||
repository.validateWritePrivilege(user); // don't allow update if read-only
|
||||
ItemCheckoutStatus coStatus = folderItem.getCheckout(checkoutId);
|
||||
if (coStatus != null) {
|
||||
User userObj = repository.getUser(user);
|
||||
|
||||
+13
-10
@@ -4,9 +4,9 @@
|
||||
* 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.
|
||||
@@ -33,9 +33,12 @@ import org.bouncycastle.util.Strings;
|
||||
* by the server with a random token which must be signed using the
|
||||
* user's SSH private key.
|
||||
* <p>
|
||||
* It is the responsibility of the callback handler to invoke the
|
||||
* sign method and return this object in response
|
||||
* to the callback.
|
||||
* It is the responsibility of the callback handler to invoke the sign method and return this
|
||||
* object in response to the callback. This callback must be signed and returned to the server
|
||||
* in a short period of time or the authentication will fail.
|
||||
* <p>
|
||||
* The supplied token is validated by the server during authentication as one that it had issued
|
||||
* but is primarily intended as the basis for the client's signature.
|
||||
*/
|
||||
public class SSHSignatureCallback implements Callback, Serializable {
|
||||
|
||||
@@ -56,17 +59,17 @@ public class SSHSignatureCallback implements Callback, Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return token to be signed using user certificate.
|
||||
* {@return token to be signed using user certificate}
|
||||
*/
|
||||
public byte[] getToken() {
|
||||
return (token == null ? null : (byte[]) token.clone());
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return signed token bytes set by callback handler.
|
||||
* {@return signed token bytes set by callback handler}
|
||||
*/
|
||||
public byte[] getSignature() {
|
||||
return (signature == null ? null : (byte[]) signature.clone());
|
||||
return signature;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,7 +81,7 @@ public class SSHSignatureCallback implements Callback, Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if callback has been signed
|
||||
* {@return true if callback has been signed}
|
||||
*/
|
||||
public boolean isSigned() {
|
||||
return signature != null;
|
||||
|
||||
+15
-14
@@ -32,6 +32,10 @@ import javax.security.auth.x500.X500Principal;
|
||||
* It is the responsibility of the callback handler to invoke the
|
||||
* sign(X509Certificate[], byte[]) and return this object in response
|
||||
* to the callback.
|
||||
* <p>
|
||||
* The supplied token is validated by the server during authentication as one that it had issued
|
||||
* but is primarily intended as the basis for the client's signature. This callback must be
|
||||
* signed and returned to the server in a short period of time or the authentication will fail.
|
||||
*/
|
||||
public class SignatureCallback implements Callback, Serializable {
|
||||
|
||||
@@ -49,6 +53,7 @@ public class SignatureCallback implements Callback, Serializable {
|
||||
* @param recognizedAuthorities list of CA's from which one must occur
|
||||
* within the certificate chain of the signing certificate.
|
||||
* @param token random bytes to be signed
|
||||
* @param serverSignature servers signature of token at time of generation
|
||||
*/
|
||||
public SignatureCallback(X500Principal[] recognizedAuthorities, byte[] token,
|
||||
byte[] serverSignature) {
|
||||
@@ -58,35 +63,36 @@ public class SignatureCallback implements Callback, Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of approved certificate authorities.
|
||||
* {@return list of approved certificate authorities which constrains which user certificate is
|
||||
* used to authenticate.}
|
||||
*/
|
||||
public Principal[] getRecognizedAuthorities() {
|
||||
return (recognizedAuthorities == null ? null : (Principal[]) recognizedAuthorities.clone());
|
||||
return recognizedAuthorities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns token to be signed using user certificate.
|
||||
* {@return token to be signed using user certificate}
|
||||
*/
|
||||
public byte[] getToken() {
|
||||
return (token == null ? null : (byte[]) token.clone());
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns signed token bytes set by callback handler.
|
||||
* {@return signed token bytes set by callback handler}
|
||||
*/
|
||||
public byte[] getSignature() {
|
||||
return (signature == null ? null : (byte[]) signature.clone());
|
||||
return signature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the server's signature of the token bytes.
|
||||
* {@return the server's signature of the token bytes}
|
||||
*/
|
||||
public byte[] getServerSignature() {
|
||||
return serverSignature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns certificate chain used to sign token.
|
||||
* {@return certificate chain used to sign token}
|
||||
*/
|
||||
public X509Certificate[] getCertificateChain() {
|
||||
return certChain;
|
||||
@@ -100,12 +106,7 @@ public class SignatureCallback implements Callback, Serializable {
|
||||
*/
|
||||
public void sign(X509Certificate[] sigCertChain, byte[] certSignature) {
|
||||
this.certChain = sigCertChain;
|
||||
this.signature = (certSignature == null ? null : certSignature.clone());
|
||||
}
|
||||
|
||||
public String getSigAlg() {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
this.signature = certSignature;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
* 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.
|
||||
@@ -101,7 +101,8 @@ public class SystemUtilities {
|
||||
|
||||
/**
|
||||
* Clean the specified user name to eliminate any spaces or leading domain name
|
||||
* which may be present (e.g., "MyDomain\John Doe" becomes "JohnDoe").
|
||||
* which may be present (e.g., "MyDomain\John Doe" becomes "JohnDoe"). Treat '/' in
|
||||
* a similar fashion.
|
||||
* @param name user name string to be cleaned-up
|
||||
* @return the clean user name
|
||||
*/
|
||||
@@ -118,11 +119,15 @@ public class SystemUtilities {
|
||||
uname = nameBuf.toString();
|
||||
}
|
||||
|
||||
// Remove leading Domain Name if present
|
||||
// Remove leading Domain Name if present (treat / and \ in a similar fashion)
|
||||
int slashIndex = uname.lastIndexOf('\\');
|
||||
if (slashIndex >= 0) {
|
||||
uname = uname.substring(slashIndex + 1);
|
||||
}
|
||||
slashIndex = uname.lastIndexOf('/');
|
||||
if (slashIndex >= 0) {
|
||||
uname = uname.substring(slashIndex + 1);
|
||||
}
|
||||
return uname;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user