using System; using System.Collections.Generic; using System.Text; using System.IO; using Org.BouncyCastle.X509; using PaySharp.Alipay.Util.Asymmetric; namespace PaySharp.Alipay.Util { /// /// 支付宝签名工具类 /// public static class AlipaySignature { /** 默认编码字符集 */ private static readonly string DEFAULT_CHARSET = "GBK"; /// /// 从支付宝公钥证书中提取支付宝公钥 /// /// 证书路径 /// 公钥字符串 public static string ExtractPemPublicKeyFromCert(string certPath) { ArgumentValidator.CheckArgument(!String.IsNullOrEmpty(certPath), "证书文件路径不可为空。"); X509Certificate alipayPublicKeyCert = AntCertificationUtil.ParseCert(File.ReadAllText(certPath)); return AntCertificationUtil.ExtractPemPublicKeyFromCert(alipayPublicKeyCert); } /// /// 异步通知参数验签,支持RSA、RSA2、SM2三种算法 /// V1版本方法将删除sign_type参数再进行验签,V2版本方法则不会 /// /// 待验签的参数集合 /// 支付宝公钥证书路径 /// 参数编码字符集 /// 签名类型,RSA2或RSA、SM2 /// true:验证成功;false:验证失败 public static bool CertVerifyV1(IDictionary parameters, string alipayPublicCertPath, string charset, string signType) { return RSACertCheckV1(parameters, alipayPublicCertPath, charset, signType); } /// /// 异步通知参数验签,支持RSA、RSA2、SM2三种算法,推荐替换为相同功能的方法CertVerifyV1 /// V1版本方法将删除sign_type参数再进行验签,V2版本方法则不会 /// /// 待验签的参数集合 /// 支付宝公钥证书路径 /// 参数编码字符集 /// 签名类型,RSA2或RSA、SM2 /// true:验证成功;false:验证失败 public static bool RSACertCheckV1(IDictionary parameters, string alipayPublicCertPath, string charset, string signType) { ArgumentValidator.CheckNotNull(parameters, "parameters参数不可为Null"); ArgumentValidator.CheckArgument(!String.IsNullOrEmpty(alipayPublicCertPath), "证书文件路径不可为空。"); string alipayPublicKey = ExtractPemPublicKeyFromCert(alipayPublicCertPath); return RSACheckV1(parameters, alipayPublicKey, charset, signType, false); } /// /// 异步通知参数验签,支持RSA、RSA2、SM2三种算法 /// V1版本方法将删除sign_type参数再进行验签,V2版本方法则不会 /// /// 待验签字符串 /// 支付宝公钥 /// 参数编码字符集 /// 签名类型,RSA2或RSA、SM2 /// 是否从文件加载支付宝公钥内容。 /// 如果该参数为true,则publicKey为公钥文件路径; /// 如果该参数为false,则publicKey为公钥内容 /// /// true:验证成功;false:验证失败 public static bool VerifyV1(IDictionary parameters, string publicKey, string charset, string signType, bool keyFromFile) { return RSACheckV1(parameters, publicKey, charset, signType, keyFromFile); } /// /// 异步通知参数验签,支持RSA、RSA2、SM2三种算法,推荐替换为相同功能的方法VerifyV1 /// V1版本方法将删除sign_type参数再进行验签,V2版本方法则不会 /// /// 待验签字符串 /// 支付宝公钥 /// 参数编码字符集 /// 签名类型,RSA2或RSA、SM2 /// 是否从文件加载支付宝公钥内容。 /// 如果该参数为true,则publicKey为公钥文件路径; /// 如果该参数为false,则publicKey为公钥内容 /// /// true:验证成功;false:验证失败 public static bool RSACheckV1(IDictionary parameters, string publicKey, string charset, string signType, bool keyFromFile) { ArgumentValidator.CheckNotNull(parameters, "parameters参数不可为Null"); string sign = parameters["sign"]; parameters.Remove("sign"); parameters.Remove("sign_type"); string signContent = GetSignContent(parameters); return RSACheckContent(signContent, sign, publicKey, charset, signType, keyFromFile); } /// /// 异步通知参数验签,支持RSA、RSA2、SM2三种算法 /// V1版本方法将删除sign_type参数再进行验签,V2版本方法则不会 /// /// 待验签的参数集合 /// 支付宝公钥证书路径 /// 参数编码字符集 /// 签名类型,RSA2或RSA、SM2 /// true:验证成功;false:验证失败 public static bool CertVerifyV2(IDictionary parameters, string alipayPublicCertPath, string charset, string signType) { return RSACertCheckV2(parameters, alipayPublicCertPath, charset, signType); } /// /// 异步通知参数验签,支持RSA、RSA2、SM2三种算法,推荐替换为相同功能的方法CertVerifyV2 /// V1版本方法将删除sign_type参数再进行验签,V2版本方法则不会 /// /// 待验签的参数集合 /// 支付宝公钥证书路径 /// 参数编码字符集 /// 签名类型,RSA2或RSA、SM2 /// true:验证成功;false:验证失败 public static bool RSACertCheckV2(IDictionary parameters, string alipayPublicCertPath, string charset, string signType) { ArgumentValidator.CheckNotNull(parameters, "parameters参数不可为Null"); string alipayPublicKey = ExtractPemPublicKeyFromCert(alipayPublicCertPath); return RSACheckV2(parameters, alipayPublicKey, charset, signType, false); } /// /// 异步通知参数验签,支持RSA、RSA2、SM2三种算法 /// V1版本方法将删除sign_type参数再进行验签,V2版本方法则不会 /// /// 待验签字符串 /// 支付宝公钥 /// 参数编码字符集 /// 签名类型,RSA2或RSA、SM2 /// 是否从文件加载支付宝公钥内容。 /// 如果该参数为true,则publicKey为公钥文件路径; /// 如果该参数为false,则publicKey为公钥内容 /// /// true:验证成功;false:验证失败 public static bool VerifyV2(IDictionary parameters, string publicKey, string charset, string signType, bool keyFromFile) { return RSACheckV2(parameters, publicKey, charset, signType, keyFromFile); } /// /// 异步通知参数验签,支持RSA、RSA2、SM2三种算法,推荐替换为相同功能的方法VerifyV2 /// V1版本方法将删除sign_type参数再进行验签,V2版本方法则不会 /// /// 待验签字符串 /// 支付宝公钥 /// 参数编码字符集 /// 签名类型,RSA2或RSA、SM2 /// 是否从文件加载支付宝公钥内容。 /// 如果该参数为true,则publicKey为公钥文件路径; /// 如果该参数为false,则publicKey为公钥内容 /// /// true:验证成功;false:验证失败 public static bool RSACheckV2(IDictionary parameters, string publicKey, string charset, string signType, bool keyFromFile) { ArgumentValidator.CheckNotNull(parameters, "parameters参数不可为Null"); string sign = parameters["sign"]; parameters.Remove("sign"); string signContent = GetSignContent(parameters); return RSACheckContent(signContent, sign, publicKey, charset, signType, keyFromFile); } /// /// 验证指定内容的签名,支持RSA、RSA2、SM2三种算法 /// /// 待验签的内容 /// 签名字符串 /// 支付宝公钥 /// 字符集编码 /// 签名算法类型,RSA2或RSA、SM2 /// 是否从文件加载支付宝公钥内容。 /// 如果该参数为true,则publicKey为公钥文件路径; /// 如果该参数为false,则publicKey为公钥内容 /// /// true:验证成功;false:验证失败 public static bool VerifyContent(string content, string sign, string publicKey, string charset, string signType, bool keyFromFile) { return RSACheckContent(content, sign, publicKey, charset, signType, keyFromFile); } /// /// 验证指定内容的签名,支持RSA、RSA2、SM2三种算法,推荐替换为相同功能的方法VerifyContent /// /// 待验签的内容 /// 签名字符串 /// 支付宝公钥 /// 字符集编码 /// 签名算法类型,RSA2或RSA、SM2 /// 是否从文件加载支付宝公钥内容。 /// 如果该参数为true,则publicKey为公钥文件路径; /// 如果该参数为false,则publicKey为公钥内容 /// /// true:验证成功;false:验证失败 public static bool RSACheckContent(string content, string sign, string publicKey, string charset, string signType, bool keyFromFile) { if (keyFromFile) { ArgumentValidator.CheckArgument(!String.IsNullOrEmpty(publicKey), "公钥文件路径不可为空。"); publicKey = File.ReadAllText(publicKey); } return AsymmetricManager.GetByName(signType).Verify(content, charset, publicKey, sign); } /// /// 对指定参数进行签名,支持RSA、RSA2、SM2三种算法 /// /// 参数集合 /// 商户私钥 /// 字符集编码 /// 签名算法类型,RSA2或RSA、SM2 /// 是否从私钥证书文件中加载私钥 /// 如果该参数为true,privateKey为私钥证书文件路径; /// 如果该参数为false,privateKey为私钥内容字符串 /// /// 签名字符串 public static string Sign(IDictionary parameters, string privateKey, string charset, string signType, bool keyFromFile) { return RSASign(parameters, privateKey, charset, signType, keyFromFile); } /// /// 对指定参数进行签名,支持RSA、RSA2、SM2三种算法,推荐替换为相同功能的方法Sign /// /// 参数集合 /// 商户私钥 /// 字符集编码 /// 签名算法类型,RSA2或RSA、SM2 /// 是否从私钥证书文件中加载私钥 /// 如果该参数为true,privateKey为私钥证书文件路径; /// 如果该参数为false,privateKey为私钥内容字符串 /// /// 签名字符串 public static string RSASign(IDictionary parameters, string privateKey, string charset, string signType, bool keyFromFile) { ArgumentValidator.CheckNotNull(parameters, "parameters参数不可为Null"); string signContent = GetSignContent(parameters); return RSASignCharSet(signContent, privateKey, charset, signType, keyFromFile); } /// /// 对指定内容进行签名,支持RSA、RSA2、SM2三种算法 /// /// 内容字符串 /// 商户私钥 /// 字符集编码 /// 签名算法类型,RSA2或RSA、SM2 /// 是否从私钥证书文件中加载私钥 /// 如果该参数为true,privateKey为私钥证书文件路径; /// 如果该参数为false,privateKey为私钥内容字符串 /// /// 签名字符串 public static string Sign(string data, string privateKey, string charset, string signType, bool keyFromFile) { return RSASign(data, privateKey, charset, signType, keyFromFile); } /// /// 对指定内容进行签名,支持RSA、RSA2、SM2三种算法,推荐替换为相同功能的方法Sign /// /// 内容字符串 /// 商户私钥 /// 字符集编码 /// 签名算法类型,RSA2或RSA、SM2 /// 是否从私钥证书文件中加载私钥 /// 如果该参数为true,privateKey为私钥证书文件路径; /// 如果该参数为false,privateKey为私钥内容字符串 /// /// 签名字符串 public static string RSASign(string data, string privateKey, string charset, string signType, bool keyFromFile) { return RSASignCharSet(data, privateKey, charset, signType, keyFromFile); } /// /// 验签并解密,目前仅适用于生活号 /// /// 待验签并解密的参数 /// 支付宝公钥字符串,用于验签 /// 商户私钥字符串,用于解密 /// 是否检查签名 /// 是否解密 /// 非对称加密算法类型,RSA2或RSA、SM2 /// 是否从文件加载支付宝公钥和商户私钥 /// 如果该参数为true,alipayPublicKey为公钥文件路径,cusPrivateKey为私钥证书文件路径; /// 如果该参数为false,则publicKeyPem为公钥内容字符串,cusPrivateKey为私钥内容字符串 /// /// 验签解密后的内容 public static string CheckSignAndDecrypt(IDictionary parameters, string alipayPublicKey, string cusPrivateKey, bool isCheckSign, bool isDecrypt, string signType, bool keyFromFile) { ArgumentValidator.CheckNotNull(parameters, "parameters参数不可为Null"); string charset = parameters["charset"]; string bizContent = parameters["biz_content"]; if (isCheckSign) { if (!RSACheckV2(parameters, alipayPublicKey, charset, signType, keyFromFile)) { throw new Exception("rsaCheck failure:rsaParams=" + parameters); } } if (isDecrypt) { return RSADecrypt(bizContent, cusPrivateKey, charset, signType, keyFromFile); } return bizContent; } /// /// 加密并加签,目前仅适用于生活号 /// /// 待加密和加签的原文 /// 支付宝公钥字符串,用于加密 /// 商户私钥字符串,用于加签 /// 字符集编码 /// 是否需要加密 /// 是否需要加签 /// 非对称加密算法类型,RSA或RSA2、SM2 /// 是否从文件加载支付宝公钥和商户私钥 /// 如果该参数为true,alipayPublicKey为公钥文件路径,cusPrivateKey为私钥证书文件路径; /// 如果该参数为false,则publicKeyPem为公钥内容字符串,cusPrivateKey为私钥内容字符串 /// /// 加密加签后的内容 public static string EncryptAndSign(string bizContent, string alipayPublicKey, string cusPrivateKey, string charset, bool isEncrypt, bool isSign, string signType, bool keyFromFile) { StringBuilder sb = new StringBuilder(); if (string.IsNullOrEmpty(charset)) { charset = DEFAULT_CHARSET; } sb.Append(""); if (isEncrypt) { // 加密 sb.Append(""); String encrypted = RSAEncrypt(bizContent, alipayPublicKey, charset, keyFromFile); sb.Append("" + encrypted + ""); sb.Append("" + signType + ""); if (isSign) { String sign = RSASign(encrypted, cusPrivateKey, charset, signType, keyFromFile); sb.Append("" + sign + ""); sb.Append("" + signType + ""); } sb.Append(""); } else if (isSign) { // 不加密,但需要签名 sb.Append(""); sb.Append("" + bizContent + ""); String sign = RSASign(bizContent, cusPrivateKey, charset, signType, keyFromFile); sb.Append("" + sign + ""); sb.Append("" + signType + ""); sb.Append(""); } else { // 不加密,不加签 sb.Append(bizContent); } return sb.ToString(); } public static string GetSignContent(IDictionary parameters) { // 第一步:把字典按Key的字母顺序排序 IDictionary sortedParams = new SortedDictionary(parameters, StringComparer.Ordinal); IEnumerator> dem = sortedParams.GetEnumerator(); // 第二步:把所有参数名和参数值串在一起 StringBuilder query = new StringBuilder(""); while (dem.MoveNext()) { string key = dem.Current.Key; string value = dem.Current.Value; if (!string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(value)) { query.Append(key).Append("=").Append(value).Append("&"); } } string content = query.ToString().Substring(0, query.Length - 1); return content; } public static string RSASignCharSet(string data, string privateKeyPem, string charset, string signType, bool keyFromFile) { if (keyFromFile) { ArgumentValidator.CheckArgument(!String.IsNullOrEmpty(privateKeyPem), "私钥文件路径不可为空。"); privateKeyPem = LoadPrivateKeyFromRSACertFile(privateKeyPem); } return AsymmetricManager.GetByName(signType).Sign(data, charset, privateKeyPem); } public static string RSAEncrypt(string content, string publicKeyPem, string charset, bool keyFromFile) { if (keyFromFile) { ArgumentValidator.CheckArgument(!String.IsNullOrEmpty(publicKeyPem), "公钥文件路径不可为空。"); publicKeyPem = File.ReadAllText(publicKeyPem); } return AsymmetricManager.GetByName("RSA").Encrypt(content, charset, publicKeyPem); } public static string RSADecrypt(string content, string privateKeyPem, string charset, string signType, bool keyFromFile) { if (keyFromFile) { ArgumentValidator.CheckArgument(!String.IsNullOrEmpty(privateKeyPem), "私钥文件路径不可为空。"); privateKeyPem = LoadPrivateKeyFromRSACertFile(privateKeyPem); } return AsymmetricManager.GetByName(signType).Decrypt(content, charset, privateKeyPem); } private static string LoadPrivateKeyFromRSACertFile(string filename) { using (FileStream fs = File.OpenRead(filename)) { byte[] data = new byte[fs.Length]; fs.Read(data, 0, data.Length); if (data[0] != 0x30) { return GetPem("RSA PRIVATE KEY", data); } throw new Exception("证书文件格式不符合预期,无法提取私钥。"); } } private static string GetPem(string type, byte[] data) { string pem = Encoding.UTF8.GetString(data); string header = String.Format("-----BEGIN {0}-----\\n", type); string footer = String.Format("-----END {0}-----", type); int start = pem.IndexOf(header, StringComparison.Ordinal) + header.Length; int end = pem.IndexOf(footer, start, StringComparison.Ordinal); return pem.Substring(start, (end - start)); } public static SignSourceData ExtractSignContent(String str, int begin) { if (str == null) { return null; } int beginIndex = ExtractBeginPosition(str, begin); if (beginIndex >= str.Length) { return null; } int endIndex = ExtractEndPosition(str, beginIndex); return new SignSourceData() { SourceData = str.Substring(beginIndex, endIndex - beginIndex), BeginIndex = beginIndex, EndIndex = endIndex }; } private static int ExtractBeginPosition(String responseString, int begin) { int beginPosition = begin; //找到第一个左大括号(对应响应的是JSON对象的情况:普通调用OpenAPI响应明文) //或者双引号(对应响应的是JSON字符串的情况:加密调用OpenAPI响应Base64串),作为待验签内容的起点 while (beginPosition < responseString.Length && responseString[beginPosition] != '{' && responseString[beginPosition] != '"') { ++beginPosition; } return beginPosition; } private static int ExtractEndPosition(String responseString, int beginPosition) { //提取明文验签内容终点 if (responseString[beginPosition] == '{') { return ExtractJsonObjectEndPosition(responseString, beginPosition); } //提取密文验签内容终点 else { return ExtractJsonBase64ValueEndPosition(responseString, beginPosition); } } private static int ExtractJsonBase64ValueEndPosition(String responseString, int beginPosition) { for (int index = beginPosition; index < responseString.Length; ++index) { //找到第2个双引号作为终点,由于中间全部是Base64编码的密文,所以不会有干扰的特殊字符 if (responseString[index] == '"' && index != beginPosition) { return index + 1; } } //如果没有找到第2个双引号,说明验签内容片段提取失败,直接尝试选取剩余整个响应字符串进行验签 return responseString.Length; } private static int ExtractJsonObjectEndPosition(String responseString, int beginPosition) { //记录当前尚未发现配对闭合的大括号 LinkedList braces = new LinkedList(); //记录当前字符是否在双引号中 bool inQuotes = false; //记录当前字符前面连续的转义字符个数 int consecutiveEscapeCount = 0; //从待验签字符的起点开始遍历后续字符串,找出待验签字符串的终止点,终点即是与起点{配对的} for (int index = beginPosition; index < responseString.Length; ++index) { //提取当前字符 char currentChar = responseString[index]; //如果当前字符是"且前面有偶数个转义标记(0也是偶数) if (currentChar == '"' && consecutiveEscapeCount % 2 == 0) { //是否在引号中的状态取反 inQuotes = !inQuotes; } //如果当前字符是{且不在引号中 else if (currentChar == '{' && !inQuotes) { //将该{加入未闭合括号中 braces.AddLast("{"); } //如果当前字符是}且不在引号中 else if (currentChar == '}' && !inQuotes) { //弹出一个未闭合括号 braces.RemoveLast(); //如果弹出后,未闭合括号为空,说明已经找到终点 if (braces.Count == 0) { return index + 1; } } //如果当前字符是转义字符 if (currentChar == '\\') { //连续转义字符个数+1 ++consecutiveEscapeCount; } else { //连续转义字符个数置0 consecutiveEscapeCount = 0; } } //如果没有找到配对的闭合括号,说明验签内容片段提取失败,直接尝试选取剩余整个响应字符串进行验签 return responseString.Length; } /// /// 获取公钥证书序列号 /// /// 公钥证书路径 /// 公钥证书序列号 public static String GetCertSN(String certPath) { X509Certificate cert = AntCertificationUtil.ParseCert(File.ReadAllText(certPath)); return AntCertificationUtil.GetCertSN(cert); } [Obsolete("因之前方法命名不符合C#规范而废弃的方法,请替换为EncryptAndSign方法")] public static string encryptAndSign(string bizContent, string alipayPublicKey, string cusPrivateKey, string charset, bool isEncrypt, bool isSign, string signType, bool keyFromFile) { return EncryptAndSign(bizContent, alipayPublicKey, cusPrivateKey, charset, isEncrypt, isSign, signType, keyFromFile); } [Obsolete("请替换为EncryptAndSign方法,显式设置参数:signTyp=RSA,keyFromFile=true")] public static string encryptAndSign(string bizContent, string alipayPublicKey, string cusPrivateKey, string charset, bool isEncrypt, bool isSign) { return EncryptAndSign(bizContent, alipayPublicKey, cusPrivateKey, charset, isEncrypt, isSign, "RSA", true); } [Obsolete("请替换为未废弃的有完整参数列表的重载版本,明确指定各参数的值")] public static string CheckSignAndDecrypt(IDictionary parameters, string alipayPublicKey, string cusPrivateKey, bool isCheckSign, bool isDecrypt) { return CheckSignAndDecrypt(parameters, alipayPublicKey, cusPrivateKey, isCheckSign, isDecrypt, "RSA", true); } [Obsolete("请替换为未废弃的有完整参数列表的重载版本,明确指定各参数的值")] public static string RSASign(IDictionary parameters, string privateKeyPem, string charset, string signType) { string signContent = GetSignContent(parameters); return RSASignCharSet(signContent, privateKeyPem, charset, signType); } [Obsolete("请替换为未废弃的有完整参数列表的重载版本,明确指定各参数的值")] public static string RSASign(string data, string privateKeyPem, string charset, string signType) { return RSASignCharSet(data, privateKeyPem, charset, signType); } [Obsolete("请替换为未废弃的有完整参数列表的重载版本,按照惯例将keyFromFile参数列在最后")] public static string RSASign(IDictionary parameters, string privateKey, string charset, bool keyFromFile, string signType) { string signContent = GetSignContent(parameters); return RSASignCharSet(signContent, privateKey, charset, signType, keyFromFile); } [Obsolete("请替换为未废弃的有完整参数列表的重载版本,明确指定各参数的值")] public static string RSASignCharSet(string data, string privateKeyPem, string charset, string signType) { return RSASignCharSet(data, privateKeyPem, charset, signType, true); } [Obsolete("请替换为未废弃的有完整参数列表的重载版本,明确指定各参数的值")] public static bool RSACheckV1(IDictionary parameters, string publicKeyPem, string charset) { string sign = parameters["sign"]; parameters.Remove("sign"); parameters.Remove("sign_type"); string signContent = GetSignContent(parameters); return RSACheckContent(signContent, sign, publicKeyPem, charset, "RSA"); } [Obsolete("请替换为未废弃的有完整参数列表的重载版本,明确指定各参数的值")] public static bool RSACheckV1(IDictionary parameters, string publicKeyPem) { string sign = parameters["sign"]; parameters.Remove("sign"); parameters.Remove("sign_type"); string signContent = GetSignContent(parameters); return RSACheckContent(signContent, sign, publicKeyPem, DEFAULT_CHARSET, "RSA"); } [Obsolete("请替换为未废弃的有完整参数列表的重载版本,明确指定各参数的值")] public static bool RSACheckV2(IDictionary parameters, string publicKeyPem) { string sign = parameters["sign"]; parameters.Remove("sign"); string signContent = GetSignContent(parameters); return RSACheckContent(signContent, sign, publicKeyPem, DEFAULT_CHARSET, "RSA"); } [Obsolete("请替换为未废弃的有完整参数列表的重载版本,明确指定各参数的值")] public static bool RSACheckV2(IDictionary parameters, string publicKeyPem, string charset) { string sign = parameters["sign"]; parameters.Remove("sign"); string signContent = GetSignContent(parameters); return RSACheckContent(signContent, sign, publicKeyPem, charset, "RSA"); } [Obsolete("请替换为未废弃的有完整参数列表的重载版本,明确指定各参数的值")] public static bool RSACheckContent(string signContent, string sign, string publicKeyPem, string charset, string signType) { return RSACheckContent(signContent, sign, publicKeyPem, charset, signType, true); } [Obsolete("请替换为未废弃的有完整参数列表的重载版本,明确指定各参数的值")] public static bool RSACheckContent(string signContent, string sign, string publicKeyPem, string charset, bool keyFromFile) { return RSACheckContent(signContent, sign, publicKeyPem, charset, "RSA", keyFromFile); } [Obsolete("请替换为未废弃的有完整参数列表的重载版本,明确指定各参数的值")] public static string RSAEncrypt(string content, string publicKeyPem, string charset) { return RSAEncrypt(content, publicKeyPem, charset, true); } [Obsolete("请替换为未废弃的有完整参数列表的重载版本,明确指定各参数的值")] public static string RSADecrypt(string content, string privateKeyPem, string charset, string signType) { return RSADecrypt(content, privateKeyPem, charset, signType, true); } } }