diff --git a/.idea/runConfigurations/NtlmSessionTest.xml b/.idea/runConfigurations/NtlmSessionTest.xml deleted file mode 100644 index 8fff274..0000000 --- a/.idea/runConfigurations/NtlmSessionTest.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/pom.xml b/pom.xml index ed8836a..f388491 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ 5.10.2 - 5.11.0 + 5.17.0 9.1.0 @@ -67,6 +67,12 @@ + + com.google.guava + guava + 33.4.0-jre + test + org.junit.jupiter junit-jupiter @@ -91,6 +97,12 @@ 0.14.0 test + + org.bouncycastle + bcprov-jdk18on + 1.80 + compile + diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index c157fa3..2486cb4 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,6 +1,7 @@ module org.cryptomator.jsmb { requires org.slf4j; requires static org.jetbrains.annotations; + requires org.bouncycastle.provider; // provides java.security.Provider with org.cryptomator.jsmb.ntlmv2.LegacyCryptoProvider; // only required, if we want to find the provider by name diff --git a/src/main/java/org/cryptomator/jsmb/TcpConnection.java b/src/main/java/org/cryptomator/jsmb/TcpConnection.java index f6d9655..1e21b65 100644 --- a/src/main/java/org/cryptomator/jsmb/TcpConnection.java +++ b/src/main/java/org/cryptomator/jsmb/TcpConnection.java @@ -1,12 +1,23 @@ package org.cryptomator.jsmb; import org.cryptomator.jsmb.common.MalformedMessageException; +import org.cryptomator.jsmb.common.NTStatus; import org.cryptomator.jsmb.common.SMBMessage; import org.cryptomator.jsmb.smb1.SMB1MessageParser; import org.cryptomator.jsmb.smb1.SMB1Negotiator; import org.cryptomator.jsmb.smb1.SmbComNegotiateRequest; -import org.cryptomator.jsmb.smb2.*; +import org.cryptomator.jsmb.smb2.Connection; +import org.cryptomator.jsmb.smb2.LogoffRequest; +import org.cryptomator.jsmb.smb2.NegotiateRequest; +import org.cryptomator.jsmb.smb2.Negotiator; +import org.cryptomator.jsmb.smb2.Runtime; +import org.cryptomator.jsmb.smb2.SMB2Message; +import org.cryptomator.jsmb.smb2.SMB2MessageParser; +import org.cryptomator.jsmb.smb2.Session; +import org.cryptomator.jsmb.smb2.SessionSetupRequest; +import org.cryptomator.jsmb.smb2.SessionSetupResponse; import org.cryptomator.jsmb.util.Layouts; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,6 +25,10 @@ import java.io.IOException; import java.lang.foreign.MemorySegment; import java.net.Socket; +import java.util.Arrays; +import java.util.Objects; + +import static org.cryptomator.jsmb.smb2.negotiate.GlobalCapabilities.SMB2_GLOBAL_CAP_ENCRYPTION; class TcpConnection implements Runnable { @@ -23,12 +38,14 @@ class TcpConnection implements Runnable { private final Socket socket; private final Connection connection; private final Negotiator negotiator; + private final Runtime runtime; public TcpConnection(TcpServer server, Socket socket) { this.server = server; this.socket = socket; this.connection = new Connection(server.global); this.negotiator = new Negotiator(server, connection); + this.runtime = new Runtime(connection); } @Override @@ -85,9 +102,10 @@ private void handleSmb2Packet(MemorySegment segment) throws MalformedMessageExce var response = switch (msg) { case NegotiateRequest request -> negotiator.negotiate(request); case SessionSetupRequest request -> negotiator.sessionSetup(request); + case LogoffRequest request -> runtime.logoff(request); default -> throw new MalformedMessageException("Command not implemented: " + msg.header().command()); }; - writeResponse(response); + writeResponse(sign(msg, response)); nextCommand = msg.header().nextCommand(); } while (nextCommand != 0); } @@ -106,4 +124,79 @@ private void writeResponse(SMBMessage response) { LOG.error("Exception while writing response", e); } } + + private SMBMessage sign(SMB2Message request, SMB2Message response) { + var sessionId = response.header().sessionId(); + var session = connection.sessionTable.get(sessionId); + assert (sessionId == 0) == (session == null); + if (shouldSign(request, response, session)) { + assert Objects.equals(connection.dialect, "3.1.1"); + return response.sign(selectKey(response, session), connection); + } + return response; + } + + /** + * @see Signing the Message + */ + private boolean shouldSign(SMB2Message request, SMB2Message response, @Nullable Session session) { + var signed = !Arrays.equals(request.header().signature(), new byte[16]); + var sessionId = response.header().sessionId(); + var treeId = response.header().treeId(); + + assert signed == request.header().hasFlag(SMB2Message.Flags.SIGNED); + assert (sessionId == 0) == (session == null); + if (signed && sessionId != 0 && treeId == 0 && session.signingRequired) { + return true; + } + if (signed && sessionId != 0 && treeId != 0 && session.signingRequired && (!connection.global.encryptData || ((connection.clientCapabilities & SMB2_GLOBAL_CAP_ENCRYPTION) == 0))) { + return true; + } + if (signed && !response.header().hasFlag(SMB2Message.Flags.ASYNC_COMMAND)) { + return true; + } + return false; + } + + /** + * @see Signing the Message + */ + private byte[] selectKey(SMB2Message response, Session session) { + if (connection.dialect.startsWith("3.")) { + if (response instanceof SessionSetupResponse && response.header().status() != NTStatus.STATUS_SUCCESS) { + return session.signingKey; + } + return channelSigningKey(session); + } + return session.sessionKey; + } + + /** + * Provides the {@code Channel.SigningKey} for signing a response. + * + * @apiNote This method implements the following specification from + * Signing the Message: + * + *

[...] For all other responses being signed the server + * MUST provide Channel.SigningKey by looking up the Channel in Session.ChannelList, + * where the connection matches the Channel.Connection.

+ *
+ * @implNote The current implementation of this method depends on two simplifications: + * + * As a result this method will always return {@link Session#signingKey}. + */ + private byte[] channelSigningKey(Session session) { + return session.signingKey; + } } diff --git a/src/main/java/org/cryptomator/jsmb/common/NTStatus.java b/src/main/java/org/cryptomator/jsmb/common/NTStatus.java index 8ad1a1d..d54a33e 100644 --- a/src/main/java/org/cryptomator/jsmb/common/NTStatus.java +++ b/src/main/java/org/cryptomator/jsmb/common/NTStatus.java @@ -10,6 +10,7 @@ public interface NTStatus { // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55 int STATUS_SUCCESS = 0x00000000; int STATUS_SMB_NO_PREAUTH_INTEGRITY_HASH_OVERLAP = 0xC05D0000; + int STATUS_REQUEST_NOT_ACCEPTED = 0xC00000D0; // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-smb/6ab6ca20-b404-41fd-b91a-2ed39e3762ea int STATUS_MORE_PROCESSING_REQUIRED = 0xC0000016; diff --git a/src/main/java/org/cryptomator/jsmb/ntlmv2/Authenticator.java b/src/main/java/org/cryptomator/jsmb/ntlmv2/Authenticator.java index 7ce2fb1..3411f22 100644 --- a/src/main/java/org/cryptomator/jsmb/ntlmv2/Authenticator.java +++ b/src/main/java/org/cryptomator/jsmb/ntlmv2/Authenticator.java @@ -7,25 +7,35 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; +import static org.cryptomator.jsmb.ntlmv2.NegotiateFlags.isSet; + /** * Performs the NTLM v2 Authentication + * @see Server Receives an AUTHENTICATE_MESSAGE from the Client * @see NTLM v2 Authentication */ class Authenticator { public static AuthResponse ntlmV2Auth(NtlmChallengeMessage challengeMessage, NtlmAuthenticateMessage authenticateMessage, String user, String passwd, String userDom) throws AuthenticationFailedException { - byte[] responseKeyNT = NTOWFv2(passwd, user, userDom); - byte[] responseKeyLM = LMOWFv2(passwd, user, userDom); - var serverChallenge = challengeMessage.serverChallenge(); - if (authenticateMessage.userNameLen() == 0 && authenticateMessage.ntChallengeResponseLen() == 0 && (authenticateMessage.lmChallengeResponseLen() == 0 || Arrays.equals(new byte[]{0x00}, authenticateMessage.lmChallengeResponse()))) { throw new AuthenticationFailedException(NTStatus.STATUS_LOGON_FAILURE, "Anonymouse authentication disabled"); } + byte[] responseKeyNT = NTOWFv2(passwd, user, userDom); + byte[] responseKeyLM = LMOWFv2(passwd, user, userDom); + var ntlmV2Response = authenticateMessage.ntlmV2Response(); - byte[] challengeFromClient = ntlmV2Response.challengeFromClient(); + byte[] challengeFromClient; + if (authenticateMessage.ntChallengeResponseLen() > 0x0018) { + challengeFromClient = ntlmV2Response.challengeFromClient(); + } else if (isSet(challengeMessage.negotiateFlags(), NegotiateFlags.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY)) { + throw new UnsupportedOperationException("Not yet implemented"); + } else { + throw new UnsupportedOperationException("Not yet implemented"); + } + var serverChallenge = challengeMessage.serverChallenge(); var time = ntlmV2Response.timestamp(); var expectedResponse = computeResponse(responseKeyNT, responseKeyLM, serverChallenge, challengeFromClient, time, ntlmV2Response.avPairsSegment().toArray(Layouts.BYTE)); diff --git a/src/main/java/org/cryptomator/jsmb/ntlmv2/Crypto.java b/src/main/java/org/cryptomator/jsmb/ntlmv2/Crypto.java index 31516d0..e109677 100644 --- a/src/main/java/org/cryptomator/jsmb/ntlmv2/Crypto.java +++ b/src/main/java/org/cryptomator/jsmb/ntlmv2/Crypto.java @@ -24,6 +24,15 @@ public static byte[] md4(byte[] input) { } } + public static byte[] md5(byte[] input) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + return md.digest(input); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("MD5 not found", e); + } + } + public static byte[] hmacMd5(byte[] key, byte[] data) { try { Mac mac = Mac.getInstance(HMAC_MD5_ALGORITHM); diff --git a/src/main/java/org/cryptomator/jsmb/ntlmv2/NtlmSession.java b/src/main/java/org/cryptomator/jsmb/ntlmv2/NtlmSession.java index 9760207..63532db 100644 --- a/src/main/java/org/cryptomator/jsmb/ntlmv2/NtlmSession.java +++ b/src/main/java/org/cryptomator/jsmb/ntlmv2/NtlmSession.java @@ -4,6 +4,7 @@ import org.cryptomator.jsmb.util.Bytes; import java.lang.foreign.MemorySegment; +import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.Arrays; import java.util.List; @@ -118,16 +119,29 @@ public Authenticated authenticate(byte[] gssToken, String user, String password, } } + + var clientSigningKey = signKey(negFlg, exportedSessionKey, "Client"); + var serverSigningKey = signKey(negFlg, exportedSessionKey, "Server"); // TODO: derive session keys and return ntlm session object - // Set ClientSigningKey to SIGNKEY(NegFlg, ExportedSessionKey , "Client") - // Set ServerSigningKey to SIGNKEY(NegFlg, ExportedSessionKey , "Server") - // Set ClientSealingKey to SEALKEY(NegFlg, ExportedSessionKey , "Client") - // Set ServerSealingKey to SEALKEY(NegFlg, ExportedSessionKey , "Server") - return new Authenticated(); + // Set ClientSealingKey to SEALKEY(NegFlg, ExportedSessionKey , "Client") + // Set ServerSealingKey to SEALKEY(NegFlg, ExportedSessionKey , "Server") + return new Authenticated(exportedSessionKey, clientSigningKey, serverSigningKey); + } + + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/524cdccb-563e-4793-92b0-7bc321fce096 + private byte[] signKey(int flags, byte[] exportedSessionKey, String mode) { + if ((flags & NegotiateFlags.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY) != 0) { + byte[] magicConstant = "Client".equals(mode) + ? "session key to client-to-server signing key magic constant\00".getBytes(StandardCharsets.US_ASCII) + : "session key to server-to-client signing key magic constant\00".getBytes(StandardCharsets.US_ASCII); + return Crypto.md5(Bytes.concat(exportedSessionKey, magicConstant)); + } else { + return new byte[0]; + } } } - final class Authenticated implements NtlmSession { + record Authenticated(byte[] exportedSessionKey, byte[] clientSigningKey, byte[] serverSigningKey) implements NtlmSession { } } diff --git a/src/main/java/org/cryptomator/jsmb/smb1/SMB1Negotiator.java b/src/main/java/org/cryptomator/jsmb/smb1/SMB1Negotiator.java index a0f32ac..508cd15 100644 --- a/src/main/java/org/cryptomator/jsmb/smb1/SMB1Negotiator.java +++ b/src/main/java/org/cryptomator/jsmb/smb1/SMB1Negotiator.java @@ -1,9 +1,14 @@ package org.cryptomator.jsmb.smb1; import org.cryptomator.jsmb.TcpServer; -import org.cryptomator.jsmb.common.SMBMessage; import org.cryptomator.jsmb.common.NTStatus; -import org.cryptomator.jsmb.smb2.*; +import org.cryptomator.jsmb.common.SMBMessage; +import org.cryptomator.jsmb.smb2.Command; +import org.cryptomator.jsmb.smb2.Connection; +import org.cryptomator.jsmb.smb2.Dialects; +import org.cryptomator.jsmb.smb2.NegotiateResponse; +import org.cryptomator.jsmb.smb2.PacketHeader; +import org.cryptomator.jsmb.smb2.SMB2Message; import org.cryptomator.jsmb.smb2.negotiate.GlobalCapabilities; import org.cryptomator.jsmb.smb2.negotiate.SecurityMode; import org.cryptomator.jsmb.util.WinFileTime; @@ -15,7 +20,8 @@ * * @param server The server on behalf of which this negotiator acts * @see Negotiation protocol example - * @see Receiving an SMB_COM_NEGOTIATE + * @see Receiving an SMB_COM_NEGOTIATE + * @see SMB 2.1 or SMB 3.x Support */ public record SMB1Negotiator(TcpServer server, Connection connection) { @@ -39,7 +45,7 @@ public SMBMessage negotiate(SmbComNegotiateRequest request) { header.treeId(0); header.sessionId(0L); var response = new NegotiateResponse(header.build()); - response.securityMode(SecurityMode.SIGNING_ENABLED); + response.securityMode((char) (SecurityMode.SIGNING_ENABLED | (connection.global.requireMessageSigning ? SecurityMode.SIGNING_REQUIRED : 0))); response.dialectRevision(Dialects.SMB2_WILDCARD); response.serverGuid(server.guid); response.capabilities(GlobalCapabilities.SMB2_GLOBAL_CAP_LARGE_MTU); diff --git a/src/main/java/org/cryptomator/jsmb/smb2/Connection.java b/src/main/java/org/cryptomator/jsmb/smb2/Connection.java index 9bdc8dd..b5ce324 100644 --- a/src/main/java/org/cryptomator/jsmb/smb2/Connection.java +++ b/src/main/java/org/cryptomator/jsmb/smb2/Connection.java @@ -1,6 +1,7 @@ package org.cryptomator.jsmb.smb2; import org.cryptomator.jsmb.smb2.negotiate.PreauthIntegrityCapabilities; +import org.cryptomator.jsmb.smb2.negotiate.SigningCapabilities; import java.util.HashMap; import java.util.Map; @@ -32,7 +33,7 @@ public Connection(Global global) { public char preauthIntegrityHashId = PreauthIntegrityCapabilities.HASH_ALGORITHM_SHA512; public byte[] preauthIntegrityHashValue = new byte[64]; public char cipherId; - public char signingAlgorithmId; + public SigningCapabilities.Algorithm signingAlgorithmId; public char[] compressionIds; public boolean supportsChainedCompression; public char[] RDMATransformIds; diff --git a/src/main/java/org/cryptomator/jsmb/smb2/Global.java b/src/main/java/org/cryptomator/jsmb/smb2/Global.java index 374bf98..61e306b 100644 --- a/src/main/java/org/cryptomator/jsmb/smb2/Global.java +++ b/src/main/java/org/cryptomator/jsmb/smb2/Global.java @@ -11,4 +11,9 @@ public class Global { Map sessionTable = new HashMap<>(); Map 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