clientTable = new HashMap<>(); // TODO: create Client class
+ public final boolean encryptData = true;
+ public final boolean rejectUnencryptedAccess = true;
+ public final boolean requireMessageSigning = true;
+
+ public final boolean isMultiChannelCapable = false;
}
diff --git a/src/main/java/org/cryptomator/jsmb/smb2/LogoffRequest.java b/src/main/java/org/cryptomator/jsmb/smb2/LogoffRequest.java
new file mode 100644
index 0000000..3801797
--- /dev/null
+++ b/src/main/java/org/cryptomator/jsmb/smb2/LogoffRequest.java
@@ -0,0 +1,19 @@
+package org.cryptomator.jsmb.smb2;
+
+import org.cryptomator.jsmb.util.Layouts;
+
+import java.lang.foreign.MemorySegment;
+
+/**
+ * A SMB2 LOGOFF Request
+ *
+ * @see SMB2 LOGOFF Request Specification
+ */
+public record LogoffRequest(PacketHeader header, MemorySegment segment) implements SMB2Message {
+
+ public char structureSize() {
+ return segment.get(Layouts.LE_UINT16, 0); //Should always be 4
+ }
+
+ //Reserved: 2 bytes @ offset 2
+}
\ No newline at end of file
diff --git a/src/main/java/org/cryptomator/jsmb/smb2/LogoffResponse.java b/src/main/java/org/cryptomator/jsmb/smb2/LogoffResponse.java
new file mode 100644
index 0000000..26ac0f8
--- /dev/null
+++ b/src/main/java/org/cryptomator/jsmb/smb2/LogoffResponse.java
@@ -0,0 +1,24 @@
+package org.cryptomator.jsmb.smb2;
+
+import org.cryptomator.jsmb.util.Layouts;
+
+import java.lang.foreign.MemorySegment;
+
+/**
+ * A SMB2 LOGOFF Response
+ *
+ * @see SMB2 LOGOFF Response Specification
+ */
+public record LogoffResponse(PacketHeader header, MemorySegment segment) implements SMB2Message {
+
+ public static final char STRUCTURE_SIZE = 4;
+
+ public LogoffResponse {
+ segment.set(Layouts.LE_UINT16, 0, STRUCTURE_SIZE);
+ segment.set(Layouts.LE_UINT16, 2, (char) 0); //Reserved
+ }
+
+ public LogoffResponse(PacketHeader header) {
+ this(header, MemorySegment.ofArray(new byte[STRUCTURE_SIZE]));
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/cryptomator/jsmb/smb2/Negotiator.java b/src/main/java/org/cryptomator/jsmb/smb2/Negotiator.java
index 965e3ed..1c1283a 100644
--- a/src/main/java/org/cryptomator/jsmb/smb2/Negotiator.java
+++ b/src/main/java/org/cryptomator/jsmb/smb2/Negotiator.java
@@ -7,6 +7,7 @@
import org.cryptomator.jsmb.common.NTStatus;
import org.cryptomator.jsmb.common.NTStatusException;
import org.cryptomator.jsmb.ntlmv2.NtlmSession;
+import org.cryptomator.jsmb.smb2.crypto.NistSP800108KDF;
import org.cryptomator.jsmb.smb2.negotiate.CompressionCapabilities;
import org.cryptomator.jsmb.smb2.negotiate.EncryptionCapabilities;
import org.cryptomator.jsmb.smb2.negotiate.GlobalCapabilities;
@@ -22,10 +23,12 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
import static org.cryptomator.jsmb.smb2.negotiate.GlobalCapabilities.SMB2_GLOBAL_CAP_ENCRYPTION;
@@ -60,7 +63,7 @@ public SMB2Message negotiate(NegotiateRequest request) {
connection.negotiateDialect = Dialects.SMB3_1_1;
connection.clientSecurityMode = request.securityMode();
connection.supportsMultiCredit = true;
- connection.serverSecurityMode = (char) (SecurityMode.SIGNING_ENABLED | request.securityMode() & SecurityMode.SIGNING_REQUIRED);
+ connection.serverSecurityMode = (char) (SecurityMode.SIGNING_ENABLED | (connection.global.requireMessageSigning ? SecurityMode.SIGNING_REQUIRED : 0));
connection.serverCapabilities = GlobalCapabilities.SMB2_GLOBAL_CAP_LARGE_MTU;
LOG.debug("Client supports SMB 3.1.1");
@@ -79,7 +82,9 @@ public SMB2Message negotiate(NegotiateRequest request) {
connection.cipherId = UInt16.stream(requestedEncryptionCapabilities.ciphers()).anyMatch(c -> c == EncryptionCapabilities.AES_256_GCM)
? EncryptionCapabilities.AES_256_GCM
: EncryptionCapabilities.NO_COMMON_CIPHER;
- connection.serverCapabilities |= SMB2_GLOBAL_CAP_ENCRYPTION;
+ if (connection.cipherId != EncryptionCapabilities.NO_COMMON_CIPHER) {
+ connection.serverCapabilities |= SMB2_GLOBAL_CAP_ENCRYPTION;
+ }
}
// SMB2_COMPRESSION_CAPABILITIES TODO
@@ -91,9 +96,9 @@ public SMB2Message negotiate(NegotiateRequest request) {
// SMB2_SIGNING_CAPABILITIES
var requestedSigningCapabilities = request.negotiateContext(SigningCapabilities.class);
if (request.negotiateContext(SigningCapabilities.class) != null) {
- connection.signingAlgorithmId = UInt16.stream(requestedSigningCapabilities.signingAlgorithms()).anyMatch(c -> c == SigningCapabilities.AES_GMAC)
- ? SigningCapabilities.AES_GMAC
- : SigningCapabilities.AES_CMAC;
+ connection.signingAlgorithmId = UInt16.stream(requestedSigningCapabilities.signingAlgorithms()).anyMatch(c -> c == SigningCapabilities.Algorithm.AES_GMAC.getValue())
+ ? SigningCapabilities.Algorithm.AES_GMAC
+ : SigningCapabilities.Algorithm.AES_CMAC;
}
// SMB2_TRANSPORT_CAPABILITIES TODO
@@ -138,7 +143,7 @@ public SMB2Message negotiate(NegotiateRequest request) {
}
// SMB2_SIGNING_CAPABILITIES
if (request.negotiateContext(SigningCapabilities.class) != null) {
- contexts.add(SigningCapabilities.build(connection.signingAlgorithmId));
+ contexts.add(SigningCapabilities.build(connection.signingAlgorithmId.getValue()));
}
// SMB2_TRANSPORT_CAPABILITIES
if (request.negotiateContext(TransportCapabilities.class) != null) {
@@ -158,22 +163,38 @@ public SMB2Message negotiate(NegotiateRequest request) {
}
public SMB2Message sessionSetup(SessionSetupRequest request) {
- if (connection.negotiateDialect != Dialects.SMB3_1_1) {
+ var global = connection.global;
+ assert global.encryptData;
+ assert global.rejectUnencryptedAccess;
+ assert connection.dialect != null;
+
+ //connection.dialect should only be "3.1.1"
+ var dialect3x = connection.dialect.startsWith("3.");
+ if (/* assertions && */ !dialect3x) { //Step 1
return ErrorResponse.create(request, NTStatus.STATUS_ACCESS_DENIED);
}
- if ((connection.clientCapabilities & SMB2_GLOBAL_CAP_ENCRYPTION) == 0) {
+
+ assert dialect3x;
+ if (/* assertions && dialect3x && */ (connection.clientCapabilities & SMB2_GLOBAL_CAP_ENCRYPTION) == 0) { //Step 2
return ErrorResponse.create(request, NTStatus.STATUS_ACCESS_DENIED);
}
final Session session;
- if (request.header().sessionId() == 0L) {
+ if (request.header().sessionId() == 0L) { //Step 3
+ //See: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/ea10b7ae-b053-4e4c-ab31-a48f7d0a79af
session = Session.create(connection);
Thread.currentThread().setName("Session-" + session.sessionId);
session.state = Session.State.IN_PROGRESS;
session.preauthIntegrityHashValue = connection.preauthIntegrityHashValue;
- } else if ((request.flags() & SessionSetupRequest.FLAG_BINDING) != 0) {
+ return gssAuthenticate(request, session);
+ }
+
+ //Step 4
+ if (/* dialect3x && */ global.isMultiChannelCapable && (request.flags() & SessionSetupRequest.FLAG_BINDING) != 0) {
// TODO implement according to step 4:
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/e545352b-9f2b-4c5e-9350-db46e4f6755e
throw new UnsupportedOperationException("multi channel not yet supported");
+ } else if (!global.isMultiChannelCapable && (request.flags() & SessionSetupRequest.FLAG_BINDING) != 0) {
+ return ErrorResponse.create(request, NTStatus.STATUS_REQUEST_NOT_ACCEPTED);
} else {
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/b495e2da-8711-4772-b292-453be0394b49
// The server MUST look up the Session in Connection.SessionTable by using the SessionId in the SMB2 header of the request.
@@ -184,10 +205,26 @@ public SMB2Message sessionSetup(SessionSetupRequest request) {
}
}
assert session != null;
- if (session.state == Session.State.EXPIRED || session.state == Session.State.VALID) {
- // TODO reauthenticate according to https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/5ecc02fb-0e60-4cba-afeb-f13100a6e65e
+ if (session.state == Session.State.EXPIRED) { //Step 5
+ //See: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/5ecc02fb-0e60-4cba-afeb-f13100a6e65e
+ session.state = Session.State.IN_PROGRESS;
+ session.securityContext = null;
+ return gssAuthenticate(request, session); //TODO Handle reauthentication in gssAuthenticate
+ } else if (session.state == Session.State.VALID) { //Step 6
+ //See: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/5ecc02fb-0e60-4cba-afeb-f13100a6e65e
+ return gssAuthenticate(request, session); //TODO Handle reauthentication in gssAuthenticate
+ } else { //Step 7
+ return gssAuthenticate(request, session);
}
+ }
+ //https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/5ed93f06-a1d2-4837-8954-fa8b833c2654
+ private SMB2Message gssAuthenticate(SessionSetupRequest request, Session session) {
+ assert session.connection == connection;
+ if ((request.flags() & SessionSetupRequest.FLAG_BINDING) != 0) {
+ //Please mind TcpConnection#channelSigningKey
+ throw new UnsupportedOperationException("SMB2_SESSION_FLAG_BINDING not yet supported");
+ }
// create response
var header = PacketHeader.builder();
header.creditCharge((char) 0);
@@ -208,13 +245,29 @@ public SMB2Message sessionSetup(SessionSetupRequest request) {
header.status(NTStatus.STATUS_MORE_PROCESSING_REQUIRED);
var response = new SessionSetupResponse(header.build());
session.ntlmSession = awaitingAuthentication;
- return response.withSecurityBuffer(negTokenResp.negTokenResp().serialize());
+
+ var fullResponse = response.withSecurityBuffer(negTokenResp.negTokenResp().serialize());
+ var preAuthHashAlgorithm = HashAlgorithm.lookup(connection.preauthIntegrityHashId);
+ session.preauthIntegrityHashValue = preAuthHashAlgorithm.compute(Bytes.concat(session.preauthIntegrityHashValue, request.serialize()));
+ session.preauthIntegrityHashValue = preAuthHashAlgorithm.compute(Bytes.concat(session.preauthIntegrityHashValue, fullResponse.serialize()));
+ return fullResponse;
}
case NtlmSession.AwaitingAuthentication s -> {
+ var preAuthHashAlgorithm = HashAlgorithm.lookup(connection.preauthIntegrityHashId);
+ session.preauthIntegrityHashValue = preAuthHashAlgorithm.compute(Bytes.concat(session.preauthIntegrityHashValue, request.serialize()));
+
var authenticated = s.authenticate(gssToken.token(), "user", "password", "DOMAIN"); // FIXME hardcoded credentials
header.status(NTStatus.STATUS_SUCCESS);
+ header.creditResponse((char) 8192);
+ // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/5ed93f06-a1d2-4837-8954-fa8b833c2654
session.ntlmSession = authenticated;
- return new SessionSetupResponse(header.build()).withSecurityBuffer(NegTokenResp.acceptCompleted().negTokenResp().serialize());
+ session.sessionKey = authenticated.exportedSessionKey(); // step 6
+ session.fullSessionKey = session.sessionKey;
+ session.signingKey = NistSP800108KDF.withHmacSha256(session.sessionKey, "SMBSigningKey\0".getBytes(StandardCharsets.US_ASCII), session.preauthIntegrityHashValue, 16); // step 7
+ session.applicationKey = NistSP800108KDF.withHmacSha256(session.sessionKey, "SMBAppKey\0".getBytes(StandardCharsets.US_ASCII), session.preauthIntegrityHashValue, 16); // step 8
+ var response = new SessionSetupResponse(header.build()).withSecurityBuffer(NegTokenResp.acceptCompleted().negTokenResp().serialize());
+ assert Objects.equals(connection.dialect, "3.1.1");
+ return response.sign(session.signingKey, connection); // step 12
}
case NtlmSession.Authenticated _ -> throw new IllegalStateException("Session already authenticated");
}
diff --git a/src/main/java/org/cryptomator/jsmb/smb2/PacketHeader.java b/src/main/java/org/cryptomator/jsmb/smb2/PacketHeader.java
index c877a2b..7becfbe 100644
--- a/src/main/java/org/cryptomator/jsmb/smb2/PacketHeader.java
+++ b/src/main/java/org/cryptomator/jsmb/smb2/PacketHeader.java
@@ -80,4 +80,9 @@ public long sessionId() {
public byte[] signature() {
return segment.asSlice(48, 16).toArray(Layouts.BYTE);
}
+
+ public PacketHeaderBuilder copy() {
+ return new PacketHeaderBuilder(this);
+ }
+
}
diff --git a/src/main/java/org/cryptomator/jsmb/smb2/PacketHeaderBuilder.java b/src/main/java/org/cryptomator/jsmb/smb2/PacketHeaderBuilder.java
index fc0b632..31f8594 100644
--- a/src/main/java/org/cryptomator/jsmb/smb2/PacketHeaderBuilder.java
+++ b/src/main/java/org/cryptomator/jsmb/smb2/PacketHeaderBuilder.java
@@ -15,6 +15,11 @@ public PacketHeaderBuilder() {
this(MemorySegment.ofArray(new byte[PacketHeader.STRUCTURE_SIZE]));
}
+ public PacketHeaderBuilder(PacketHeader header) {
+ this();
+ segment.copyFrom(header.segment());
+ }
+
public PacketHeaderBuilder creditCharge(char creditCharge) {
segment.set(Layouts.LE_UINT16, 6, creditCharge);
return this;
@@ -66,7 +71,7 @@ public PacketHeaderBuilder sessionId(long sessionId) {
}
public PacketHeaderBuilder signature(byte[] signature) {
- segment.asSlice(44, 16).copyFrom(MemorySegment.ofArray(signature));
+ segment.asSlice(48, 16).copyFrom(MemorySegment.ofArray(signature));
return this;
}
diff --git a/src/main/java/org/cryptomator/jsmb/smb2/Runtime.java b/src/main/java/org/cryptomator/jsmb/smb2/Runtime.java
new file mode 100644
index 0000000..efc1ef9
--- /dev/null
+++ b/src/main/java/org/cryptomator/jsmb/smb2/Runtime.java
@@ -0,0 +1,26 @@
+package org.cryptomator.jsmb.smb2;
+
+import org.cryptomator.jsmb.common.NTStatus;
+
+public record Runtime(Connection connection) {
+
+ /**
+ * @see Receiving an SMB2 LOGOFF Request
+ */
+ public SMB2Message logoff(LogoffRequest request) {
+ //TODO Perform actual logoff and free resources
+
+ var header = PacketHeader.builder();
+ header.creditCharge((char) 0);
+ header.command(Command.LOGOFF.value());
+ header.creditResponse((char) 1);
+ header.flags(SMB2Message.Flags.SERVER_TO_REDIR);
+ header.nextCommand(0);
+ header.messageId(request.header().messageId());
+ header.treeId(0);
+ header.sessionId(request.header().sessionId());
+ header.status(NTStatus.STATUS_SUCCESS);
+
+ return new LogoffResponse(header.build());
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/cryptomator/jsmb/smb2/SMB2Message.java b/src/main/java/org/cryptomator/jsmb/smb2/SMB2Message.java
index 8a44b91..92b795c 100644
--- a/src/main/java/org/cryptomator/jsmb/smb2/SMB2Message.java
+++ b/src/main/java/org/cryptomator/jsmb/smb2/SMB2Message.java
@@ -1,10 +1,13 @@
package org.cryptomator.jsmb.smb2;
import org.cryptomator.jsmb.common.SMBMessage;
+import org.cryptomator.jsmb.smb2.crypto.MessageSigner;
import org.cryptomator.jsmb.util.Bytes;
import org.cryptomator.jsmb.util.Layouts;
+import org.jetbrains.annotations.Range;
import java.lang.foreign.MemorySegment;
+import java.util.Objects;
/**
* A SMB 2 Message
@@ -23,6 +26,15 @@ interface Flags {
int PRIORITY_MASK = 0x00000070;
int DFS_OPERATIONS = 0x10000000;
int REPLAY_OPERATION = 0x20000000;
+
+ @Range(from = 0, to = 7)
+ static int priorityFrom(int flags) {
+ return (flags & PRIORITY_MASK) >>> 4;
+ }
+
+ static int withPriority(int flags, @Range(from = 0, to = 7) int priority) {
+ return (flags & ~PRIORITY_MASK) | (priority << 4);
+ }
}
PacketHeader header();
@@ -33,4 +45,10 @@ default byte[] serialize() {
return Bytes.concat(header().segment().toArray(Layouts.BYTE), segment().toArray(Layouts.BYTE));
}
+ default SMB2Message sign(byte[] key, Connection connection) {
+ assert Objects.equals(connection.dialect, "3.1.1");
+ record SignedMessage(PacketHeader header, MemorySegment segment) implements SMB2Message {}
+ return new SignedMessage(new MessageSigner().sign(this, key, connection), segment());
+ }
+
}
diff --git a/src/main/java/org/cryptomator/jsmb/smb2/SMB2MessageParser.java b/src/main/java/org/cryptomator/jsmb/smb2/SMB2MessageParser.java
index d88ad73..0072faa 100644
--- a/src/main/java/org/cryptomator/jsmb/smb2/SMB2MessageParser.java
+++ b/src/main/java/org/cryptomator/jsmb/smb2/SMB2MessageParser.java
@@ -4,7 +4,6 @@
import org.cryptomator.jsmb.util.Layouts;
import java.lang.foreign.MemorySegment;
-import java.util.HexFormat;
public class SMB2MessageParser {
@@ -27,6 +26,7 @@ public static SMB2Message parse(MemorySegment segment) {
return switch (Command.valueOf(header.command())) {
case NEGOATIATE -> new NegotiateRequest(header, bodySegment);
case SESSION_SETUP -> new SessionSetupRequest(header, bodySegment);
+ case LOGOFF -> new LogoffRequest(header, bodySegment);
default -> throw new MalformedMessageException("Unknown command: " + Integer.toHexString(header.command()));
};
}
diff --git a/src/main/java/org/cryptomator/jsmb/smb2/Session.java b/src/main/java/org/cryptomator/jsmb/smb2/Session.java
index bfc48a0..a521b74 100644
--- a/src/main/java/org/cryptomator/jsmb/smb2/Session.java
+++ b/src/main/java/org/cryptomator/jsmb/smb2/Session.java
@@ -1,6 +1,8 @@
package org.cryptomator.jsmb.smb2;
import org.cryptomator.jsmb.ntlmv2.NtlmSession;
+import org.cryptomator.jsmb.srvs.SrvsGlobal;
+import org.cryptomator.jsmb.srvs.SrvsSession;
import org.jetbrains.annotations.Range;
import java.time.Instant;
@@ -10,6 +12,11 @@
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
+/**
+ * An SMB2 session.
+ *
+ * @see Per Session
+ */
public class Session {
private static final AtomicLong SESSION_ID_GENERATOR = new AtomicLong(1);
@@ -21,7 +28,6 @@ public enum State {
}
public final long sessionId;
- public final long sessionGlobalId;
public final Connection connection;
public NtlmSession ntlmSession;
@@ -32,10 +38,10 @@ private Session(Connection connection, @Range(from = 1L, to = Long.MAX_VALUE) lo
}
this.connection = connection;
this.sessionId = sessionId;
- this.sessionGlobalId = sessionId;
this.ntlmSession = NtlmSession.create();
}
+ public int sessionGlobalId;
public State state;
public Object securityContext = null; // TODO adjust type
public byte[] sessionKey = null;
@@ -49,6 +55,8 @@ private Session(Connection connection, @Range(from = 1L, to = Long.MAX_VALUE) lo
public List> channelList = new ArrayList<>();
public byte[] preauthIntegrityHashValue;
public byte[] fullSessionKey = null;
+ public byte[] signingKey = null;
+ public byte[] applicationKey = null;
/**
* Creates a new session and registers it with the given connection.
@@ -57,9 +65,19 @@ private Session(Connection connection, @Range(from = 1L, to = Long.MAX_VALUE) lo
*/
public static Session create(Connection connection) {
var session = new Session(connection, SESSION_ID_GENERATOR.incrementAndGet());
- connection.global.sessionTable.put(session.sessionGlobalId, session);
+ connection.global.sessionTable.put(session.sessionId, session);
connection.sessionTable.put(session.sessionId, session);
+ session.sessionGlobalId = register();
return session;
}
+ /**
+ * @return the globalSessionId
+ * @see Server Registers a New Session
+ */
+ private static int register() {
+ var globalSessionId = SrvsSession.SRVS_SESSION_ID_GENERATOR.getAndIncrement();
+ SrvsGlobal.INSTANCE.sessionList.put(globalSessionId, new SrvsSession(globalSessionId));
+ return globalSessionId;
+ }
}
diff --git a/src/main/java/org/cryptomator/jsmb/smb2/crypto/MessageSigner.java b/src/main/java/org/cryptomator/jsmb/smb2/crypto/MessageSigner.java
new file mode 100644
index 0000000..bed82fe
--- /dev/null
+++ b/src/main/java/org/cryptomator/jsmb/smb2/crypto/MessageSigner.java
@@ -0,0 +1,109 @@
+package org.cryptomator.jsmb.smb2.crypto;
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.cryptomator.jsmb.smb2.Command;
+import org.cryptomator.jsmb.smb2.Connection;
+import org.cryptomator.jsmb.smb2.PacketHeader;
+import org.cryptomator.jsmb.smb2.SMB2Message;
+import org.cryptomator.jsmb.smb2.negotiate.SigningCapabilities;
+import org.cryptomator.jsmb.util.Bytes;
+import org.cryptomator.jsmb.util.Layouts;
+import org.jetbrains.annotations.VisibleForTesting;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.Mac;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.NoSuchAlgorithmException;
+import java.util.Objects;
+
+/**
+ * Signs an SMB2 message.
+ *
+ * @see Signing the Message
+ * @see Signing An Outgoing Message
+ */
+public class MessageSigner {
+
+ private static final int NONCE_FLAG_IS_SERVER = 0b1;
+ private static final int NONCE_FLAG_IS_CANCEL = 0b10;
+
+ private static final BouncyCastleProvider bcProvider = new BouncyCastleProvider();
+
+ public PacketHeader sign(SMB2Message message, byte[] signingKey, Connection connection) {
+ assert Objects.equals(connection.dialect, "3.1.1");
+ return sign(message, signingKey, connection.signingAlgorithmId);
+ }
+
+ /**
+ * @implNote Requires dialect 3.1.1
+ */
+ @VisibleForTesting
+ PacketHeader sign(SMB2Message message, byte[] signingKey, SigningCapabilities.Algorithm signingAlgorithm) {
+ if (signingKey == null) {
+ throw new IllegalStateException("Signing key not set");
+ }
+ var newHeader = message.header().copy().signature(new byte[16]); // zero out any existing signature
+ newHeader.flags(message.header().flags() | SMB2Message.Flags.SIGNED);
+
+ if (signingAlgorithm == null || signingAlgorithm == SigningCapabilities.Algorithm.AES_CMAC) {
+ byte[] data = Bytes.concat(newHeader.segment().toArray(Layouts.BYTE), message.segment().toArray(Layouts.BYTE));
+ byte[] signature = cmac(data, signingKey);
+ return newHeader.signature(signature).build();
+ } else if (signingAlgorithm == SigningCapabilities.Algorithm.AES_GMAC) {
+ byte[] nonce = new byte[12];
+ int flags = NONCE_FLAG_IS_SERVER;
+ if (message.header().command() == Command.CANCEL.value()) {
+ flags |= NONCE_FLAG_IS_CANCEL;
+ }
+ ByteBuffer.wrap(nonce) //
+ .order(ByteOrder.LITTLE_ENDIAN) //
+ .putLong(0, message.header().messageId()) //
+ .putInt(8, flags);
+ byte[] data = Bytes.concat(newHeader.segment().toArray(Layouts.BYTE), message.segment().toArray(Layouts.BYTE));
+ byte[] signature = gmac(data, nonce, signingKey);
+ return newHeader.signature(signature).build();
+ } else {
+ throw new UnsupportedOperationException("Unsupported algorithm: " + signingAlgorithm);
+ }
+ }
+
+ @VisibleForTesting
+ byte[] gmac(byte[] data, byte[] nonce, byte[] signingKeyBytes) {
+ try {
+ Key signingKey = new SecretKeySpec(signingKeyBytes, "AES");
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
+ GCMParameterSpec spec = new GCMParameterSpec(128, nonce);
+ cipher.init(Cipher.ENCRYPT_MODE, signingKey, spec);
+ cipher.updateAAD(data);
+ return cipher.doFinal(new byte[0]); // empty, as we just want the tag
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ throw new AssertionError("Every implementation of the Java platform is required to support AES/GCM/NoPadding", e);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
+ throw new AssertionError("Block size or padding irrelevant when encrypting with GCM", e);
+ } catch (InvalidAlgorithmParameterException | InvalidKeyException e) {
+ throw new IllegalArgumentException("Invalid key or algorithm parameter", e);
+ }
+ }
+
+ @VisibleForTesting
+ byte[] cmac(byte[] data, byte[] signingKeyBytes) {
+ try {
+ Mac mac = Mac.getInstance("AESCMAC", bcProvider);
+ mac.init(new SecretKeySpec(signingKeyBytes, "AES128"));
+ return mac.doFinal(data);
+ } catch (NoSuchAlgorithmException e) {
+ throw new AssertionError("AESCMAC should be provided by Bouncy Castle", e);
+ } catch (InvalidKeyException e) {
+ throw new IllegalArgumentException("Invalid key or algorithm parameter", e);
+ }
+ }
+}
diff --git a/src/main/java/org/cryptomator/jsmb/smb2/crypto/NistSP800108KDF.java b/src/main/java/org/cryptomator/jsmb/smb2/crypto/NistSP800108KDF.java
new file mode 100644
index 0000000..366495c
--- /dev/null
+++ b/src/main/java/org/cryptomator/jsmb/smb2/crypto/NistSP800108KDF.java
@@ -0,0 +1,64 @@
+package org.cryptomator.jsmb.smb2.crypto;
+
+import org.cryptomator.jsmb.util.Bytes;
+import org.jetbrains.annotations.VisibleForTesting;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.ByteBuffer;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+/**
+ * KDF in Counter Mode as specified in by NIST SP 800-108r1, Section 5.1, using HMAC-SHA256 as the PRF.
+ *
+ * Required to derive keys for SMB 3.x, as described in Generating Cryptographic Keys
+ *
+ * @see NIST SP 800-108r1
+ */
+public class NistSP800108KDF {
+
+ private NistSP800108KDF(){}
+
+ public static byte[] withHmacSha256(byte[] key, byte[] label, byte[] context, int outputLength) {
+ // fixedInputData = Label || 0x00 || Context || [L]_2
+ byte[] fixedInputData = new byte[label.length + 1 + context.length + Integer.BYTES];
+ var buf = ByteBuffer.wrap(fixedInputData);
+ buf.put(label);
+ buf.put(label.length, (byte) 0x00);
+ buf.put(label.length + 1, context);
+ buf.putInt(label.length + 1 + context.length, outputLength * Byte.SIZE);
+ return withHmacSha256(key, fixedInputData, outputLength);
+ }
+
+ @VisibleForTesting
+ static byte[] withHmacSha256(byte[] key, byte[] fixedInputData, int outputLength) {
+ Mac prf;
+ try {
+ prf = Mac.getInstance("HmacSHA256");
+ SecretKeySpec keySpec = new SecretKeySpec(key, "HmacSHA256");
+ prf.init(keySpec);
+ } catch (NoSuchAlgorithmException e) {
+ throw new AssertionError("Every implementation of the Java platform is required to support HmacSHA256", e);
+ } catch (InvalidKeyException e) {
+ throw new IllegalArgumentException("Unsuitable key", e);
+ }
+
+ int h = prf.getMacLength();
+ int n = Math.ceilDiv(outputLength, h);
+ assert n < Integer.MAX_VALUE : "since outputLength is an integer, n must be smaller. Thus n < 2^r - 1"; // required as per spec
+
+ byte[] result = new byte[0];
+ byte[] tmp = new byte[Integer.BYTES + fixedInputData.length];
+ ByteBuffer tmpBuf = ByteBuffer.wrap(tmp);
+ tmpBuf.put(Integer.BYTES, fixedInputData);
+ for (int i = 1; i <= n; i++) {
+ tmpBuf.putInt(0, i);
+ result = Bytes.concat(result, prf.doFinal(tmp));
+ }
+ assert result.length >= outputLength : "result length must be greater than or equal to outputLength by now";
+ return Arrays.copyOf(result, outputLength);
+
+ }
+}
diff --git a/src/main/java/org/cryptomator/jsmb/smb2/negotiate/SigningCapabilities.java b/src/main/java/org/cryptomator/jsmb/smb2/negotiate/SigningCapabilities.java
index 2c96d90..0704454 100644
--- a/src/main/java/org/cryptomator/jsmb/smb2/negotiate/SigningCapabilities.java
+++ b/src/main/java/org/cryptomator/jsmb/smb2/negotiate/SigningCapabilities.java
@@ -4,11 +4,26 @@
import java.lang.foreign.MemorySegment;
+/**
+ * @see SMB2_SIGNING_CAPABILITIES
+ */
public record SigningCapabilities(MemorySegment data) implements NegotiateContext {
- public static final char HMAC_SHA256 = 0x0000;
- public static final char AES_CMAC = 0x0001;
- public static final char AES_GMAC = 0x0002;
+ public enum Algorithm {
+ HMAC_SHA256(0x0000),
+ AES_CMAC(0x0001),
+ AES_GMAC(0x0002);
+
+ private final char value;
+
+ Algorithm(int value) {
+ this.value = (char) value;
+ }
+
+ public char getValue() {
+ return value;
+ }
+ }
public static SigningCapabilities build(char algId) {
var data = MemorySegment.ofArray(new byte[4]);
diff --git a/src/main/java/org/cryptomator/jsmb/srvs/SrvsGlobal.java b/src/main/java/org/cryptomator/jsmb/srvs/SrvsGlobal.java
new file mode 100644
index 0000000..94aa02e
--- /dev/null
+++ b/src/main/java/org/cryptomator/jsmb/srvs/SrvsGlobal.java
@@ -0,0 +1,22 @@
+package org.cryptomator.jsmb.srvs;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * [MS-SRVS] Global object
+ *
+ * @see [MS-SRVS] Global
+ */
+public class SrvsGlobal {
+
+ public static final SrvsGlobal INSTANCE = new SrvsGlobal();
+
+ //TODO Removing entries
+ /**
+ * @see Inserting an entry
+ * @see Removing an entry
+ */
+ public final Map sessionList = new HashMap<>(); //Map instead of List to make lookup easier
+
+}
diff --git a/src/main/java/org/cryptomator/jsmb/srvs/SrvsSession.java b/src/main/java/org/cryptomator/jsmb/srvs/SrvsSession.java
new file mode 100644
index 0000000..94826d9
--- /dev/null
+++ b/src/main/java/org/cryptomator/jsmb/srvs/SrvsSession.java
@@ -0,0 +1,12 @@
+package org.cryptomator.jsmb.srvs;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * @see [MS-SRVS] Per Session
+ */
+public record SrvsSession(int globalSessionId) {
+
+ public static final AtomicInteger SRVS_SESSION_ID_GENERATOR = new AtomicInteger(Integer.MIN_VALUE);
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/cryptomator/jsmb/smb2/crypto/MessageSignerTest.java b/src/test/java/org/cryptomator/jsmb/smb2/crypto/MessageSignerTest.java
new file mode 100644
index 0000000..c5e1ddb
--- /dev/null
+++ b/src/test/java/org/cryptomator/jsmb/smb2/crypto/MessageSignerTest.java
@@ -0,0 +1,124 @@
+package org.cryptomator.jsmb.smb2.crypto;
+
+import org.cryptomator.jsmb.common.NTStatus;
+import org.cryptomator.jsmb.smb2.Command;
+import org.cryptomator.jsmb.smb2.PacketHeader;
+import org.cryptomator.jsmb.smb2.PacketHeaderBuilder;
+import org.cryptomator.jsmb.smb2.SMB2Message;
+import org.cryptomator.jsmb.smb2.SessionSetupResponse;
+import org.cryptomator.jsmb.smb2.negotiate.SigningCapabilities;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import java.lang.foreign.MemorySegment;
+import java.lang.foreign.ValueLayout;
+import java.nio.ByteBuffer;
+import java.util.Base64;
+import java.util.HexFormat;
+
+class MessageSignerTest {
+
+ private final static HexFormat HEX_FORMAT = HexFormat.of();
+
+ @Test
+ public void testGmacSignature() {
+ var data = Base64.getDecoder().decode("/lNNQkAAAAAAAAAAAQAAIAEAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkAAABIAAkAoQcwBaADCgEA");
+ var nonce = new byte[12];
+ ByteBuffer.wrap(nonce) //
+ .putLong(0, 2L) //
+ .putInt(8, 1);
+ var signer = new MessageSigner();
+
+ var signature = signer.gmac(data, nonce, Base64.getDecoder().decode("9fORJ+Vx1QUv43YChZvR6A=="));
+
+ Assertions.assertEquals("5W28PWtNfcE+BVqzLOsDdg==", Base64.getEncoder().encodeToString(signature));
+ }
+
+ @Test
+ public void testGmacSignature2() {
+ // test vector from NIST CAVP: https://csrc.nist.gov/projects/cryptographic-algorithm-validation-program/cavp-testing-block-cipher-modes
+ /*
+ Key = a4851117328a93bf528382f22f35ac94688259fd2f517e4fd27ee9cf9b8c8a2c
+ IV = 44395ca4943aca24875a281a
+ CT =
+ AAD = b9a63c85bd7cb93c9d2543572099ac0a0b1ab4dddbea4c75bacfab9755ae763cb1062a594dda9ca860134c74776752ad357cfda32d1c20e896370dac5808c147061ed1545a2a6ff26fe2e0e2e38ec887c1e210cecad4a8c9a86d
+ Tag = 1b13e6132415fd70d9092e32ff2759be
+ */
+ var data = Base64.getDecoder().decode("uaY8hb18uTydJUNXIJmsCgsatN3b6kx1us+rl1WudjyxBipZTdqcqGATTHR3Z1KtNXz9oy0cIOiWNw2sWAjBRwYe0VRaKm/yb+Lg4uOOyIfB4hDOytSoyaht");
+ var nonce = Base64.getDecoder().decode("RDlcpJQ6yiSHWiga");
+ var signer = new MessageSigner();
+
+ var signature = signer.gmac(data, nonce, Base64.getDecoder().decode("pIURFzKKk79Sg4LyLzWslGiCWf0vUX5P0n7pz5uMiiw="));
+
+ Assertions.assertEquals("GxPmEyQV/XDZCS4y/ydZvg==", Base64.getEncoder().encodeToString(signature));
+
+ }
+
+ @Test
+ public void testCMAC() {
+ // test vector from NIST Cryptographic Standards and Guidelines: https://csrc.nist.gov/projects/cryptographic-standards-and-guidelines/example-values (https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/AES_CMAC.pdf)
+ /*
+ Key = 2B7E1516 28AED2A6 ABF71588 09CF4F3C
+ PT = 6BC1BEE2 2E409F96 E93D7E11 7393172A
+ Last Block > Block #1 > outBlock = 070A16B4 6B4D4144 F79BDD9D D04A287C
+ */
+ var data = HEX_FORMAT.parseHex("6bc1bee22e409f96e93d7e117393172a");
+ var signer = new MessageSigner();
+
+ var signature = signer.cmac(data, HEX_FORMAT.parseHex("2b7e151628aed2a6abf7158809cf4f3c"));
+
+ Assertions.assertEquals("070a16b46b4d4144f79bdd9dd04a287c", HEX_FORMAT.formatHex(signature));
+ }
+
+ @ParameterizedTest
+ @CsvSource(textBlock = """
+ 0x00000000ef9270f6,\
+ fe534d42400001000000000001000020110000000000000002000000000000000000000000000000f67092ef0000000000000000000000000000000000000000,\
+ a11b3019a0030a0100a3120410010000003336c415d6ca63b600000000,\
+ fe534d42400001000000000001000020110000000000000002000000000000000000000000000000f67092ef00000000000000000000000000000000000000000900000048001d00a11b3019a0030a0100a3120410010000003336c415d6ca63b600000000,\
+ 23A69FFF57BCB19502BB74E79CE760DB,\
+ AES_CMAC,\
+ fe534d42400001000000000001000020190000000000000002000000000000000000000000000000f67092ef000000000df13f46f7f59874e581dbbe82736c040900000048001d00a11b3019a0030a0100a3120410010000003336c415d6ca63b600000000\
+
+ 0x00000000dfc1a81a,\
+ fe534d424000010000000000010000201100000000000000020000000000000000000000000000001aa8c1df0000000000000000000000000000000000000000,\
+ a11b3019a0030a0100a3120410010000009ddb82364613ea5600000000,\
+ fe534d424000010000000000010000201100000000000000020000000000000000000000000000001aa8c1df00000000000000000000000000000000000000000900000048001d00a11b3019a0030a0100a3120410010000009ddb82364613ea5600000000,\
+ E07AA62CC914810061E07EE1084FCE1B,\
+ AES_GMAC,\
+ fe534d424000010000000000010000201900000000000000020000000000000000000000000000001aa8c1df000000003484d3eef43af37ac480d6a4e2ee71400900000048001d00a11b3019a0030a0100a3120410010000009ddb82364613ea5600000000\
+ """ //
+ )
+ public void testSigning(long sessionId, String expUnsignedHdrHex, String securityBufferHex, String expUnsignedMsgHex, String signingKeyHex, SigningCapabilities.Algorithm signingAlg, String expSignedMsgHex) {
+ var unsignedHdr = new PacketHeaderBuilder() //
+ .creditCharge((char) 1) //
+ .status(NTStatus.STATUS_SUCCESS) //
+ .command(Command.SESSION_SETUP.value()) //
+ .creditResponse((char) 8192) //
+ .flags(SMB2Message.Flags.withPriority(SMB2Message.Flags.SERVER_TO_REDIR, 1)) //
+ .messageId(2) //
+ .sessionId(sessionId) //
+ .build();
+ var expUnsignedHdr = HEX_FORMAT.parseHex(expUnsignedHdrHex);
+ assertBytesEquals(expUnsignedHdr, unsignedHdr.segment().toArray(ValueLayout.OfByte.JAVA_BYTE));
+
+ var unsignedMsg = new SessionSetupResponse(unsignedHdr).withSecurityBuffer(HEX_FORMAT.parseHex(securityBufferHex));
+ var expUnsignedMsg = HEX_FORMAT.parseHex(expUnsignedMsgHex);
+ assertBytesEquals(expUnsignedMsg, unsignedMsg.serialize());
+
+ var signer = new MessageSigner();
+ var signedMsg = new SignedMessage(signer.sign(unsignedMsg, HEX_FORMAT.parseHex(signingKeyHex), signingAlg), unsignedMsg.segment());
+ var expSignedMsg = HEX_FORMAT.parseHex(expSignedMsgHex);
+ assertBytesEquals(signedMsg.serialize(), expSignedMsg);
+ }
+
+ record SignedMessage(PacketHeader header, MemorySegment segment) implements SMB2Message {
+
+ }
+
+ private void assertBytesEquals(byte[] expected, byte[] actual) {
+ Assertions.assertEquals(HEX_FORMAT.formatHex(expected), HEX_FORMAT.formatHex(actual));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/cryptomator/jsmb/smb2/crypto/NistSP800108KDFTest.java b/src/test/java/org/cryptomator/jsmb/smb2/crypto/NistSP800108KDFTest.java
new file mode 100644
index 0000000..379f2b2
--- /dev/null
+++ b/src/test/java/org/cryptomator/jsmb/smb2/crypto/NistSP800108KDFTest.java
@@ -0,0 +1,31 @@
+package org.cryptomator.jsmb.smb2.crypto;
+
+import com.google.common.io.BaseEncoding;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+class NistSP800108KDFTest {
+
+ private static final BaseEncoding HEX = BaseEncoding.base16().lowerCase();
+
+ // test vectors taken from https://csrc.nist.gov/Projects/Cryptographic-Algorithm-Validation-Program/Key-Derivation
+ @DisplayName("CAVS 14.4 Test Vectors [PRF=HMAC_SHA256], [CTRLOCATION=BEFORE_FIXED], [RLEN=32_BITS]")
+ @ParameterizedTest(name = "kdf({0}, {1}, {2}) = {3}")
+ @CsvSource(value = {
+ /* COUNT 0 */ "dd1d91b7d90b2bd3138533ce92b272fbf8a369316aefe242e659cc0ae238afe0, 01322b96b30acd197979444e468e1c5c6859bf1b1cf951b7e725303e237e46b864a145fab25e517b08f8683d0315bb2911d80a0e8aba17f3b413faac, 16, 10621342bfb0fd40046c0e29f2cfdbf0",
+ /* COUNT 10 */ "e204d6d466aad507ffaf6d6dab0a5b26152c9e21e764370464e360c8fbc765c6, 7b03b98d9f94b899e591f3ef264b71b193fba7043c7e953cde23bc5384bc1a6293580115fae3495fd845dadbd02bd6455cf48d0f62b33e62364a3a80, 32, 770dfab6a6a4a4bee0257ff335213f78d8287b4fd537d5c1fffa956910e7c779",
+ /* COUNT 20 */ "dc60338d884eecb72975c603c27b360605011756c697c4fc388f5176ef81efb1, 44d7aa08feba26093c14979c122c2437c3117b63b78841cd10a4bc5ed55c56586ad8986d55307dca1d198edcffbc516a8fbe6152aa428cdd800c062d, 20, 29ac07dccf1f28d506cd623e6e3fc2fa255bd60b",
+ /* COUNT 30 */ "c4bedbddb66493e7c7259a3bbbc25f8c7e0ca7fe284d92d431d9cd99a0d214ac, 1c69c54766791e315c2cc5c47ecd3ffab87d0d273dd920e70955814c220eacace6a5946542da3dfe24ff626b4897898cafb7db83bdff3c14fa46fd4b, 40, 1da47638d6c9c4d04d74d4640bbd42ab814d9e8cc22f4326695239f96b0693f12d0dd1152cf44430"
+ })
+ public void testWithHmacSha256(String key, String fixedInputData, int outLen, String expected) {
+ byte[] keyBytes = HEX.decode(key);
+ byte[] fixedInputDataBytes = HEX.decode(fixedInputData);
+
+ byte[] result = NistSP800108KDF.withHmacSha256(keyBytes, fixedInputDataBytes, outLen);
+
+ Assertions.assertEquals(expected, HEX.encode(result));
+ }
+
+}
\ No newline at end of file