diff --git a/common/utils/src/main/resources/error/error-conditions.json b/common/utils/src/main/resources/error/error-conditions.json index 964eee1da16c7..4452838562264 100644 --- a/common/utils/src/main/resources/error/error-conditions.json +++ b/common/utils/src/main/resources/error/error-conditions.json @@ -4577,6 +4577,16 @@ "Invalid extension: . Extension is limited to exactly 3 letters (e.g. csv, tsv, etc...)" ] }, + "HMAC_ALGORITHM" : { + "message" : [ + "expects one of the algorithms 'SHA-224', 'SHA-256', 'SHA-384', 'SHA-512', 'SHA-1', 'MD5', but got ." + ] + }, + "HMAC_CRYPTO_ERROR" : { + "message" : [ + "detail message: " + ] + }, "INTEGER" : { "message" : [ "expects an integer literal, but got ." diff --git a/python/docs/source/reference/pyspark.sql/functions.rst b/python/docs/source/reference/pyspark.sql/functions.rst index 2ccfde351d874..ba09531bcee87 100644 --- a/python/docs/source/reference/pyspark.sql/functions.rst +++ b/python/docs/source/reference/pyspark.sql/functions.rst @@ -659,6 +659,7 @@ Misc Functions current_path current_schema current_user + hmac input_file_block_length input_file_block_start input_file_name diff --git a/python/pyspark/sql/connect/functions/builtin.py b/python/pyspark/sql/connect/functions/builtin.py index 1caa3a9bf7f2a..d401038b6928b 100644 --- a/python/pyspark/sql/connect/functions/builtin.py +++ b/python/pyspark/sql/connect/functions/builtin.py @@ -5373,6 +5373,20 @@ def zeroifnull(col: "ColumnOrName") -> Column: zeroifnull.__doc__ = pysparkfuncs.zeroifnull.__doc__ +def hmac( + key: "ColumnOrName", + message: "ColumnOrName", + algorithm: Optional["ColumnOrName"] = None, +) -> Column: + if algorithm is None: + return _invoke_function_over_columns("hmac", key, message) + else: + return _invoke_function_over_columns("hmac", key, message, algorithm) + + +hmac.__doc__ = pysparkfuncs.hmac.__doc__ + + def aes_encrypt( input: "ColumnOrName", key: "ColumnOrName", diff --git a/python/pyspark/sql/functions/__init__.py b/python/pyspark/sql/functions/__init__.py index b3da0adee9905..78dffaf5a1310 100644 --- a/python/pyspark/sql/functions/__init__.py +++ b/python/pyspark/sql/functions/__init__.py @@ -519,6 +519,7 @@ "current_path", "current_schema", "current_user", + "hmac", "input_file_block_length", "input_file_block_start", "input_file_name", diff --git a/python/pyspark/sql/functions/builtin.py b/python/pyspark/sql/functions/builtin.py index 2c2345c1a58a4..a48248b8f9a45 100644 --- a/python/pyspark/sql/functions/builtin.py +++ b/python/pyspark/sql/functions/builtin.py @@ -30189,6 +30189,65 @@ def zeroifnull(col: "ColumnOrName") -> Column: return _invoke_function_over_columns("zeroifnull", col) +@_try_remote_functions +def hmac( + key: "ColumnOrName", + message: "ColumnOrName", + algorithm: Optional["ColumnOrName"] = None, +) -> Column: + """ + Returns the keyed-hash message authentication code (HMAC) of `message` using `key` and the + given hash `algorithm`. The result is returned as raw MAC bytes; wrap it with :func:`hex` or + :func:`base64` for a textual value. The default algorithm is 'SHA-256'. + + .. versionadded:: 4.3.0 + + Parameters + ---------- + key : :class:`~pyspark.sql.Column` or column name + The secret key, as a binary value. + message : :class:`~pyspark.sql.Column` or column name + The message to authenticate, as a binary value. + algorithm : :class:`~pyspark.sql.Column` or column name, optional + The hash algorithm. Valid values: SHA-224, SHA-256, SHA-384, SHA-512, SHA-1, MD5. + The default is SHA-256. + + Returns + ------- + :class:`~pyspark.sql.Column` + A new column that contains the raw HMAC bytes. + + Examples + -------- + + Example 1: Compute the HMAC with the default SHA-256 algorithm. + + >>> import pyspark.sql.functions as sf + >>> df = spark.createDataFrame([("key", "message")], ["key", "message"]) + >>> df.select(sf.hex(sf.hmac(df.key, df.message))).show(truncate=False) + +----------------------------------------------------------------+ + |hex(hmac(key, message, SHA-256)) | + +----------------------------------------------------------------+ + |6E9EF29B75FFFC5B7ABAE527D58FDADB2FE42E7219011976917343065F58ED4A| + +----------------------------------------------------------------+ + + Example 2: Compute the HMAC with an explicit algorithm. + + >>> import pyspark.sql.functions as sf + >>> df = spark.createDataFrame([("key", "message")], ["key", "message"]) + >>> df.select(sf.hex(sf.hmac(df.key, df.message, sf.lit("SHA-1")))).show(truncate=False) + +----------------------------------------+ + |hex(hmac(key, message, SHA-1)) | + +----------------------------------------+ + |2088DF74D5F2146B48146CAF4965377E9D0BE3A4| + +----------------------------------------+ + """ + if algorithm is None: + return _invoke_function_over_columns("hmac", key, message) + else: + return _invoke_function_over_columns("hmac", key, message, algorithm) + + @_try_remote_functions def aes_encrypt( input: "ColumnOrName", diff --git a/sql/api/src/main/scala/org/apache/spark/sql/functions.scala b/sql/api/src/main/scala/org/apache/spark/sql/functions.scala index f602c64a62484..a140a5ea0e160 100644 --- a/sql/api/src/main/scala/org/apache/spark/sql/functions.scala +++ b/sql/api/src/main/scala/org/apache/spark/sql/functions.scala @@ -4608,6 +4608,40 @@ object functions { */ def uuid(seed: Column): Column = Column.fn("uuid", seed) + /** + * Returns the keyed-hash message authentication code (HMAC) of `message` using `key` and the + * given hash `algorithm`. The result is returned as raw MAC bytes; wrap it with `hex` or + * `base64` for a textual value. + * + * @param key + * The secret key, as a binary value. + * @param message + * The message to authenticate, as a binary value. + * @param algorithm + * The hash algorithm. Valid values: SHA-224, SHA-256, SHA-384, SHA-512, SHA-1, MD5. + * + * @group misc_funcs + * @since 4.3.0 + */ + def hmac(key: Column, message: Column, algorithm: Column): Column = + Column.fn("hmac", key, message, algorithm) + + /** + * Returns the keyed-hash message authentication code (HMAC) of `message` using `key` and + * SHA-256. The result is returned as raw MAC bytes; wrap it with `hex` or `base64` for a + * textual value. To use a different algorithm, call the three-argument overload. + * + * @param key + * The secret key, as a binary value. + * @param message + * The message to authenticate, as a binary value. + * + * @group misc_funcs + * @since 4.3.0 + */ + def hmac(key: Column, message: Column): Column = + Column.fn("hmac", key, message) + /** * Returns an encrypted value of `input` using AES in given `mode` with the specified `padding`. * Key lengths of 16, 24 and 32 bits are supported. Supported combinations of (`mode`, diff --git a/sql/catalyst/src/main/java/org/apache/spark/sql/catalyst/expressions/ExpressionImplUtils.java b/sql/catalyst/src/main/java/org/apache/spark/sql/catalyst/expressions/ExpressionImplUtils.java index e744ae3e2fc2a..3f2c7e1fc5d41 100644 --- a/sql/catalyst/src/main/java/org/apache/spark/sql/catalyst/expressions/ExpressionImplUtils.java +++ b/sql/catalyst/src/main/java/org/apache/spark/sql/catalyst/expressions/ExpressionImplUtils.java @@ -30,6 +30,7 @@ import java.util.regex.PatternSyntaxException; import java.util.zip.CRC32; import javax.crypto.Cipher; +import javax.crypto.Mac; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; @@ -181,6 +182,42 @@ public static byte[] aesDecrypt(byte[] input, ); } + /** + * Computes a keyed-hash message authentication code (HMAC) of the given message using the + * given key and hash algorithm. + * @param key The secret key. + * @param message The message to authenticate. + * @param algorithm The hash algorithm. Supported values (case-insensitive): + * SHA-224, SHA-256, SHA-384, SHA-512, SHA-1, MD5. + * @return The raw HMAC bytes. + */ + public static byte[] hmac(byte[] key, byte[] message, UTF8String algorithm) { + String macName = hmacName(algorithm.toString()); + try { + Mac mac = Mac.getInstance(macName); + mac.init(new SecretKeySpec(key, macName)); + return mac.doFinal(message); + } catch (GeneralSecurityException | IllegalArgumentException e) { + throw QueryExecutionErrors.hmacCryptoError(e.getMessage()); + } + } + + /** + * Maps a user-facing hash algorithm name to its JCA {@link Mac} algorithm name. Supported + * algorithms are SHA-224, SHA-256, SHA-384, SHA-512, SHA-1 and MD5 (case-insensitive). + */ + private static String hmacName(String algorithm) { + return switch (algorithm.toUpperCase(Locale.ROOT)) { + case "SHA-224", "SHA224" -> "HmacSHA224"; + case "SHA-256", "SHA256" -> "HmacSHA256"; + case "SHA-384", "SHA384" -> "HmacSHA384"; + case "SHA-512", "SHA512" -> "HmacSHA512"; + case "SHA-1", "SHA1" -> "HmacSHA1"; + case "MD5" -> "HmacMD5"; + default -> throw QueryExecutionErrors.hmacUnsupportedAlgorithmError(algorithm); + }; + } + /** * Function to return the Spark version. * @return diff --git a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala index a9be95d7b9513..2a4772e60704e 100644 --- a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala +++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala @@ -851,6 +851,7 @@ object FunctionRegistry { expression[Sha2]("sha2"), expression[AesEncrypt]("aes_encrypt"), expression[AesDecrypt]("aes_decrypt"), + expression[Hmac]("hmac"), expression[SparkPartitionID]("spark_partition_id"), expression[InputFileName]("input_file_name"), expression[InputFileBlockStart]("input_file_block_start"), diff --git a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/misc.scala b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/misc.scala index 23e8b343e431e..1328533aa7290 100644 --- a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/misc.scala +++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/misc.scala @@ -600,3 +600,56 @@ case class TryAesDecrypt( this.copy(replacement = newChild) } // scalastyle:on line.size.limit + +/** + * A function that computes a keyed-hash message authentication code (HMAC) of the `message` + * using the given `key` and hash `algorithm`. The result is returned as raw MAC bytes, which + * can be fed back in as the `key` of a subsequent HMAC to chain derivations, or wrapped with + * `hex`/`base64` for a textual representation. + */ +// scalastyle:off line.size.limit +@ExpressionDescription( + usage = """ + _FUNC_(key, message[, algorithm]) - Returns the keyed-hash message authentication code (HMAC) of `message` using `key` and the hash `algorithm`. The result is returned as raw MAC bytes; wrap it with `hex` or `base64` for a textual value. The default algorithm is 'SHA-256'. + """, + arguments = """ + Arguments: + * key - The secret key, as a binary value. + * message - The message to authenticate, as a binary value. + * algorithm - Optional hash algorithm. Valid values: SHA-224, SHA-256, SHA-384, SHA-512, SHA-1, MD5. The default is SHA-256. + """, + examples = """ + Examples: + > SELECT hex(_FUNC_('key', 'message')); + 6E9EF29B75FFFC5B7ABAE527D58FDADB2FE42E7219011976917343065F58ED4A + > SELECT hex(_FUNC_('key', 'message', 'SHA-512')); + E477384D7CA229DD1426E64B63EBF2D36EBD6D7E669A6735424E72EA6C01D3F8B56EB39C36D8232F5427999B8D1A3F9CD1128FC69F4D75B434216810FA367E98 + """, + since = "4.3.0", + group = "misc_funcs") +// scalastyle:on line.size.limit +case class Hmac(key: Expression, message: Expression, algorithm: Expression) + extends RuntimeReplaceable with ImplicitCastInputTypes { + + override lazy val replacement: Expression = StaticInvoke( + classOf[ExpressionImplUtils], + BinaryType, + "hmac", + Seq(key, message, algorithm), + inputTypes) + + def this(key: Expression, message: Expression) = + this(key, message, Literal("SHA-256")) + + override def prettyName: String = "hmac" + + override def inputTypes: Seq[AbstractDataType] = + Seq(BinaryType, BinaryType, StringTypeWithCollation(supportsTrimCollation = true)) + + override def children: Seq[Expression] = Seq(key, message, algorithm) + + override protected def withNewChildrenInternal( + newChildren: IndexedSeq[Expression]): Expression = { + copy(newChildren(0), newChildren(1), newChildren(2)) + } +} diff --git a/sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryExecutionErrors.scala b/sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryExecutionErrors.scala index ef7d3a1317a85..a5d64ddd314ed 100644 --- a/sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryExecutionErrors.scala +++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryExecutionErrors.scala @@ -2526,6 +2526,24 @@ private[sql] object QueryExecutionErrors extends QueryErrorsBase with ExecutionE "functionName" -> toSQLId("aes_encrypt"))) } + def hmacUnsupportedAlgorithmError(algorithm: String): RuntimeException = { + new SparkRuntimeException( + errorClass = "INVALID_PARAMETER_VALUE.HMAC_ALGORITHM", + messageParameters = Map( + "parameter" -> toSQLId("algorithm"), + "functionName" -> toSQLId("hmac"), + "algorithm" -> toSQLValue(algorithm, StringType))) + } + + def hmacCryptoError(detailMessage: String): RuntimeException = { + new SparkRuntimeException( + errorClass = "INVALID_PARAMETER_VALUE.HMAC_CRYPTO_ERROR", + messageParameters = Map( + "parameter" -> toSQLId("key"), + "functionName" -> toSQLId("hmac"), + "detailMessage" -> detailMessage)) + } + def hiveTableWithAnsiIntervalsError( table: TableIdentifier): SparkUnsupportedOperationException = { new SparkUnsupportedOperationException( diff --git a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/ExpressionImplUtilsSuite.scala b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/ExpressionImplUtilsSuite.scala index e197c71026eba..e3a9177bfdbcb 100644 --- a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/ExpressionImplUtilsSuite.scala +++ b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/ExpressionImplUtilsSuite.scala @@ -403,6 +403,65 @@ class ExpressionImplUtilsSuite extends SparkFunSuite { // scalastyle:on nonascii } + // RFC 4231 test case 1 (HMAC-SHA-224/256/384/512) and RFC 2202 test case 1 + // (HMAC-SHA-1 / HMAC-MD5): key is 0x0b repeated, data is "Hi There". + private val hmacKey20 = Array.fill[Byte](20)(0x0b) + private val hmacKey16 = Array.fill[Byte](16)(0x0b) + private val hmacData = "Hi There".getBytes("UTF-8") + + private def hmacHex(key: Array[Byte], message: Array[Byte], algorithm: String): String = { + ExpressionImplUtils.hmac(key, message, UTF8String.fromString(algorithm)) + .map(b => f"$b%02x").mkString + } + + test("Hmac with supported algorithms") { + assert(hmacHex(hmacKey20, hmacData, "SHA-224") == + "896fb1128abbdf196832107cd49df33f47b4b1169912ba4f53684b22") + assert(hmacHex(hmacKey20, hmacData, "SHA-256") == + "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7") + assert(hmacHex(hmacKey20, hmacData, "SHA-384") == + "afd03944d84895626b0825f4ab46907f15f9dadbe4101ec682aa034c7cebc59c" + + "faea9ea9076ede7f4af152e8b2fa9cb6") + assert(hmacHex(hmacKey20, hmacData, "SHA-512") == + "87aa7cdea5ef619d4ff0b4241a1d6cb02379f4e2ce4ec2787ad0b30545e17cde" + + "daa833b7d6b8a702038b274eaea3f4e4be9d914eeb61f1702e696c203a126854") + assert(hmacHex(hmacKey20, hmacData, "SHA-1") == + "b617318655057264e28bc0b6fb378c8ef146be00") + assert(hmacHex(hmacKey16, hmacData, "MD5") == + "9294727a3638bb1c13f48ef8158bfc9d") + } + + test("Hmac algorithm name is case-insensitive and accepts the compact form") { + val expected = "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7" + assert(hmacHex(hmacKey20, hmacData, "sha-256") == expected) + assert(hmacHex(hmacKey20, hmacData, "SHA256") == expected) + } + + test("Hmac unsupported algorithm error") { + checkError( + exception = intercept[SparkRuntimeException] { + ExpressionImplUtils.hmac(hmacKey20, hmacData, UTF8String.fromString("SHA-3")) + }, + condition = "INVALID_PARAMETER_VALUE.HMAC_ALGORITHM", + parameters = Map( + "parameter" -> "`algorithm`", + "functionName" -> "`hmac`", + "algorithm" -> "'SHA-3'")) + } + + test("Hmac empty key error") { + checkError( + exception = intercept[SparkRuntimeException] { + ExpressionImplUtils.hmac(Array.empty[Byte], hmacData, UTF8String.fromString("SHA-256")) + }, + condition = "INVALID_PARAMETER_VALUE.HMAC_CRYPTO_ERROR", + parameters = Map( + "parameter" -> "`key`", + "functionName" -> "`hmac`", + "detailMessage" -> ".*"), + matchPVals = true) + } + test("TryValidate UTF8 string") { def tryValidateUTF8(str: UTF8String, expected: UTF8String): Unit = { assert(ExpressionImplUtils.tryValidateUTF8String(str) == expected) diff --git a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/MiscExpressionsSuite.scala b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/MiscExpressionsSuite.scala index ff0183eb265b8..0327624709001 100644 --- a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/MiscExpressionsSuite.scala +++ b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/MiscExpressionsSuite.scala @@ -21,7 +21,7 @@ import java.io.PrintStream import scala.util.Random -import org.apache.spark.SparkFunSuite +import org.apache.spark.{SparkFunSuite, SparkRuntimeException} import org.apache.spark.sql.catalyst.analysis.TypeCheckResult.DataTypeMismatch import org.apache.spark.sql.catalyst.util.TypeUtils.ordinalNumber import org.apache.spark.sql.types._ @@ -112,4 +112,38 @@ class MiscExpressionsSuite extends SparkFunSuite with ExpressionEvalHelper { assert(outputCodegen.contains(s"Result of $inputExpr is 1")) assert(outputEval.contains(s"Result of $inputExpr is 1")) } + + test("Hmac") { + def bytes(hex: String): Array[Byte] = + hex.grouped(2).map(Integer.parseInt(_, 16).toByte).toArray + + val key = Literal("key".getBytes("UTF-8")) + val message = Literal("message".getBytes("UTF-8")) + + // Default algorithm is SHA-256. + checkEvaluation( + new Hmac(key, message), + bytes("6e9ef29b75fffc5b7abae527d58fdadb2fe42e7219011976917343065f58ed4a")) + + // Explicit algorithm. + checkEvaluation( + Hmac(key, message, Literal("SHA-1")), + bytes("2088df74d5f2146b48146caf4965377e9d0be3a4")) + + // Null propagation. + checkEvaluation(Hmac(Literal.create(null, BinaryType), message, Literal("SHA-256")), null) + checkEvaluation(Hmac(key, Literal.create(null, BinaryType), Literal("SHA-256")), null) + checkEvaluation(Hmac(key, message, Literal.create(null, StringType)), null) + } + + test("Hmac unsupported algorithm") { + checkErrorInExpression[SparkRuntimeException]( + Hmac(Literal("key".getBytes("UTF-8")), Literal("message".getBytes("UTF-8")), + Literal("SHA-3")), + "INVALID_PARAMETER_VALUE.HMAC_ALGORITHM", + Map( + "parameter" -> "`algorithm`", + "functionName" -> "`hmac`", + "algorithm" -> "'SHA-3'")) + } } diff --git a/sql/core/src/test/resources/sql-functions/sql-expression-schema.md b/sql/core/src/test/resources/sql-functions/sql-expression-schema.md index 481c17e12486e..0757f823b5876 100644 --- a/sql/core/src/test/resources/sql-functions/sql-expression-schema.md +++ b/sql/core/src/test/resources/sql-functions/sql-expression-schema.md @@ -166,6 +166,7 @@ | org.apache.spark.sql.catalyst.expressions.Hex | hex | SELECT hex(17) | struct | | org.apache.spark.sql.catalyst.expressions.HllSketchEstimate | hll_sketch_estimate | SELECT hll_sketch_estimate(hll_sketch_agg(col)) FROM VALUES (1), (1), (2), (2), (3) tab(col) | struct | | org.apache.spark.sql.catalyst.expressions.HllUnion | hll_union | SELECT hll_sketch_estimate(hll_union(hll_sketch_agg(col1), hll_sketch_agg(col2))) FROM VALUES (1, 4), (1, 4), (2, 5), (2, 5), (3, 6) tab(col1, col2) | struct | +| org.apache.spark.sql.catalyst.expressions.Hmac | hmac | SELECT hex(hmac('key', 'message')) | struct | | org.apache.spark.sql.catalyst.expressions.HourExpressionBuilder | hour | SELECT hour('2018-02-14 12:58:59') | struct | | org.apache.spark.sql.catalyst.expressions.Hypot | hypot | SELECT hypot(3, 4) | struct | | org.apache.spark.sql.catalyst.expressions.ILike | ilike | SELECT ilike('Spark', '_Park') | struct | diff --git a/sql/core/src/test/resources/sql-tests/analyzer-results/misc-functions.sql.out b/sql/core/src/test/resources/sql-tests/analyzer-results/misc-functions.sql.out index 708ba0ad7b58d..a470d33d98931 100644 --- a/sql/core/src/test/resources/sql-tests/analyzer-results/misc-functions.sql.out +++ b/sql/core/src/test/resources/sql-tests/analyzer-results/misc-functions.sql.out @@ -233,3 +233,66 @@ SELECT assert_true(col1 <= col2) FROM VALUES ('2025-03-01', '2025-03-10') -- !query analysis Project [assert_true((col1#x <= col2#x), '(col1 <= col2)' is not true!) AS assert_true((col1 <= col2), '(col1 <= col2)' is not true!)#x] +- LocalRelation [col1#x, col2#x] + + +-- !query +SELECT hex(hmac('key', 'message')) +-- !query analysis +Project [hex(hmac(cast(key as binary), cast(message as binary), SHA-256)) AS hex(hmac(key, message, SHA-256))#x] ++- OneRowRelation + + +-- !query +SELECT hex(hmac('key', 'message', 'SHA-256')) +-- !query analysis +Project [hex(hmac(cast(key as binary), cast(message as binary), SHA-256)) AS hex(hmac(key, message, SHA-256))#x] ++- OneRowRelation + + +-- !query +SELECT hex(hmac('key', 'message', 'SHA-512')) +-- !query analysis +Project [hex(hmac(cast(key as binary), cast(message as binary), SHA-512)) AS hex(hmac(key, message, SHA-512))#x] ++- OneRowRelation + + +-- !query +SELECT lower(hex(hmac('key', 'message', 'sha256'))) +-- !query analysis +Project [lower(hex(hmac(cast(key as binary), cast(message as binary), sha256))) AS lower(hex(hmac(key, message, sha256)))#x] ++- OneRowRelation + + +-- !query +SELECT hex(hmac(hmac('key', 'message'), 'message2')) +-- !query analysis +Project [hex(hmac(hmac(cast(key as binary), cast(message as binary), SHA-256), cast(message2 as binary), SHA-256)) AS hex(hmac(hmac(key, message, SHA-256), message2, SHA-256))#x] ++- OneRowRelation + + +-- !query +SELECT hmac(CAST(NULL AS BINARY), 'message') +-- !query analysis +Project [hmac(cast(null as binary), cast(message as binary), SHA-256) AS hmac(CAST(NULL AS BINARY), message, SHA-256)#x] ++- OneRowRelation + + +-- !query +SELECT hmac('key', CAST(NULL AS BINARY)) +-- !query analysis +Project [hmac(cast(key as binary), cast(null as binary), SHA-256) AS hmac(key, CAST(NULL AS BINARY), SHA-256)#x] ++- OneRowRelation + + +-- !query +SELECT hmac('key', 'message', CAST(NULL AS STRING)) +-- !query analysis +Project [hmac(cast(key as binary), cast(message as binary), cast(null as string)) AS hmac(key, message, CAST(NULL AS STRING))#x] ++- OneRowRelation + + +-- !query +SELECT hmac('key', 'message', 'SHA-3') +-- !query analysis +Project [hmac(cast(key as binary), cast(message as binary), SHA-3) AS hmac(key, message, SHA-3)#x] ++- OneRowRelation diff --git a/sql/core/src/test/resources/sql-tests/inputs/misc-functions.sql b/sql/core/src/test/resources/sql-tests/inputs/misc-functions.sql index 417af4220641f..e0b7b6ac3f88f 100644 --- a/sql/core/src/test/resources/sql-tests/inputs/misc-functions.sql +++ b/sql/core/src/test/resources/sql-tests/inputs/misc-functions.sql @@ -44,3 +44,17 @@ SET spark.sql.legacy.raiseErrorWithoutErrorClass=false; -- Implicit alias of assert expression SELECT assert_true(col1 <= col2) FROM VALUES ('2025-03-01', '2025-03-10'); + +-- hmac +SELECT hex(hmac('key', 'message')); +SELECT hex(hmac('key', 'message', 'SHA-256')); +SELECT hex(hmac('key', 'message', 'SHA-512')); +SELECT lower(hex(hmac('key', 'message', 'sha256'))); +-- Chaining: the binary output of one hmac feeds in as the key of the next. +SELECT hex(hmac(hmac('key', 'message'), 'message2')); +-- Null propagation. +SELECT hmac(CAST(NULL AS BINARY), 'message'); +SELECT hmac('key', CAST(NULL AS BINARY)); +SELECT hmac('key', 'message', CAST(NULL AS STRING)); +-- Unsupported algorithm. +SELECT hmac('key', 'message', 'SHA-3'); diff --git a/sql/core/src/test/resources/sql-tests/results/misc-functions.sql.out b/sql/core/src/test/resources/sql-tests/results/misc-functions.sql.out index efcb0409bf935..9373dc63289aa 100644 --- a/sql/core/src/test/resources/sql-tests/results/misc-functions.sql.out +++ b/sql/core/src/test/resources/sql-tests/results/misc-functions.sql.out @@ -321,3 +321,84 @@ SELECT assert_true(col1 <= col2) FROM VALUES ('2025-03-01', '2025-03-10') struct -- !query output NULL + + +-- !query +SELECT hex(hmac('key', 'message')) +-- !query schema +struct +-- !query output +6E9EF29B75FFFC5B7ABAE527D58FDADB2FE42E7219011976917343065F58ED4A + + +-- !query +SELECT hex(hmac('key', 'message', 'SHA-256')) +-- !query schema +struct +-- !query output +6E9EF29B75FFFC5B7ABAE527D58FDADB2FE42E7219011976917343065F58ED4A + + +-- !query +SELECT hex(hmac('key', 'message', 'SHA-512')) +-- !query schema +struct +-- !query output +E477384D7CA229DD1426E64B63EBF2D36EBD6D7E669A6735424E72EA6C01D3F8B56EB39C36D8232F5427999B8D1A3F9CD1128FC69F4D75B434216810FA367E98 + + +-- !query +SELECT lower(hex(hmac('key', 'message', 'sha256'))) +-- !query schema +struct +-- !query output +6e9ef29b75fffc5b7abae527d58fdadb2fe42e7219011976917343065f58ed4a + + +-- !query +SELECT hex(hmac(hmac('key', 'message'), 'message2')) +-- !query schema +struct +-- !query output +28042756E0362D954B61661BB4DEC9FF9F6C970D346CAEB6DB36ED7B735AB38C + + +-- !query +SELECT hmac(CAST(NULL AS BINARY), 'message') +-- !query schema +struct +-- !query output +NULL + + +-- !query +SELECT hmac('key', CAST(NULL AS BINARY)) +-- !query schema +struct +-- !query output +NULL + + +-- !query +SELECT hmac('key', 'message', CAST(NULL AS STRING)) +-- !query schema +struct +-- !query output +NULL + + +-- !query +SELECT hmac('key', 'message', 'SHA-3') +-- !query schema +struct<> +-- !query output +org.apache.spark.SparkRuntimeException +{ + "errorClass" : "INVALID_PARAMETER_VALUE.HMAC_ALGORITHM", + "sqlState" : "22023", + "messageParameters" : { + "algorithm" : "'SHA-3'", + "functionName" : "`hmac`", + "parameter" : "`algorithm`" + } +}