diff --git a/chsm-common/pom.xml b/chsm-common/pom.xml index e768b9f..bed3d4e 100644 --- a/chsm-common/pom.xml +++ b/chsm-common/pom.xml @@ -49,6 +49,11 @@ net.java.dev.jna jna + + com.google.guava + guava + 33.2.1-jre + diff --git a/chsm-common/src/main/java/com/sunyard/chsm/sdf/adapter/JnaSdfAdaptor.java b/chsm-common/src/main/java/com/sunyard/chsm/sdf/adapter/JnaSdfAdaptor.java index 5b379df..65cfcca 100644 --- a/chsm-common/src/main/java/com/sunyard/chsm/sdf/adapter/JnaSdfAdaptor.java +++ b/chsm-common/src/main/java/com/sunyard/chsm/sdf/adapter/JnaSdfAdaptor.java @@ -6,9 +6,12 @@ import com.sunyard.chsm.sdf.lib.SdfLibrary; import com.sunyard.chsm.sdf.model.DeviceInfo; import com.sunyard.chsm.sdf.model.EccKey; import com.sunyard.chsm.sdf.model.EccPubKey; +import com.sunyard.chsm.sdf.model.SDF_DeviceInfo; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.util.Assert; +import java.util.Arrays; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -17,6 +20,7 @@ import java.util.concurrent.ConcurrentHashMap; * @author liulu * @since 2024/11/4 */ +@Slf4j @RequiredArgsConstructor public abstract class JnaSdfAdaptor implements SdfApiAdapter { @@ -87,7 +91,21 @@ public abstract class JnaSdfAdaptor implements SdfApiAdapter { @Override public DeviceInfo getDeviceInfo(String sessionHandle) { - return null; + + SDF_DeviceInfo sdfInfo = new SDF_DeviceInfo(); + sdfLibrary.SDF_GetDeviceInfo(getSessionHandle(sessionHandle), sdfInfo); + + DeviceInfo deviceInfo = new DeviceInfo(); + deviceInfo.setIssuerName(new String(sdfInfo.IssuerName)); + deviceInfo.setDeviceName(new String(sdfInfo.DeviceName)); + deviceInfo.setDeviceSerial(new String(sdfInfo.DeviceSerial)); + deviceInfo.setDeviceVersion(sdfInfo.DeviceVersion); + deviceInfo.setStandardVersion(sdfInfo.StandardVersion); + deviceInfo.setAsymAlgAbility(sdfInfo.AsymAlgAbility); + deviceInfo.setSymAlgAbility(sdfInfo.SymAlgAbility); + deviceInfo.setHashAlgAbility(sdfInfo.HashAlgAbility); + deviceInfo.setBufferSize(sdfInfo.BufferSize); + return deviceInfo; } @Override @@ -100,7 +118,10 @@ public abstract class JnaSdfAdaptor implements SdfApiAdapter { @Override public EccPubKey exportEncPublicKeyECC(String sessionHandle, int uiKeyIndex) { - return null; + byte[] pubKey = new byte[132]; + Pointer hSessionHandle = getSessionHandle(sessionHandle); + sdfLibrary.SDF_ExportEncPublicKey_ECC(hSessionHandle, uiKeyIndex, pubKey); + return new EccPubKey(256, Arrays.copyOfRange(pubKey, 36, 68), Arrays.copyOfRange(pubKey, 100, 132)); } @Override diff --git a/chsm-common/src/main/java/com/sunyard/chsm/sdf/adapter/SdfApiAdapterFactory.java b/chsm-common/src/main/java/com/sunyard/chsm/sdf/adapter/SdfApiAdapterFactory.java index ceb2a8d..e7d787d 100644 --- a/chsm-common/src/main/java/com/sunyard/chsm/sdf/adapter/SdfApiAdapterFactory.java +++ b/chsm-common/src/main/java/com/sunyard/chsm/sdf/adapter/SdfApiAdapterFactory.java @@ -27,7 +27,7 @@ public class SdfApiAdapterFactory { case enc001: return new SunyardJnaSdfAdaptor(device.getServiceIp(), device.getServicePort()); default: - return null; + throw new UnsupportedOperationException("不支持的设备型号: " + device.getManufacturerModel()); } } diff --git a/chsm-common/src/main/java/com/sunyard/chsm/sdf/adapter/SunyardJnaSdfAdaptor.java b/chsm-common/src/main/java/com/sunyard/chsm/sdf/adapter/SunyardJnaSdfAdaptor.java index ce93bf4..1c47d55 100644 --- a/chsm-common/src/main/java/com/sunyard/chsm/sdf/adapter/SunyardJnaSdfAdaptor.java +++ b/chsm-common/src/main/java/com/sunyard/chsm/sdf/adapter/SunyardJnaSdfAdaptor.java @@ -24,7 +24,7 @@ public class SunyardJnaSdfAdaptor extends JnaSdfAdaptor { private final Integer dealTimeout; public SunyardJnaSdfAdaptor(String ip, int port) { - this(ip, port, "libsdf"); + this(ip, port, "sdf"); } public SunyardJnaSdfAdaptor(String ip, int port, String libName) { diff --git a/chsm-common/src/main/java/com/sunyard/chsm/sdf/lib/SdfLibrary.java b/chsm-common/src/main/java/com/sunyard/chsm/sdf/lib/SdfLibrary.java index f8bf741..51ce618 100644 --- a/chsm-common/src/main/java/com/sunyard/chsm/sdf/lib/SdfLibrary.java +++ b/chsm-common/src/main/java/com/sunyard/chsm/sdf/lib/SdfLibrary.java @@ -2,7 +2,9 @@ package com.sunyard.chsm.sdf.lib; import com.sun.jna.Library; import com.sun.jna.Pointer; +import com.sun.jna.ptr.IntByReference; import com.sun.jna.ptr.PointerByReference; +import com.sunyard.chsm.sdf.model.SDF_DeviceInfo; /** * @author liulu @@ -50,7 +52,7 @@ public interface SdfLibrary extends Library { * @param pstDeviceInfo 设备能力描述信息,内容及格式见设备信息定义 * @return 0 成功; 非0 失败,返回错误代码 */ - int SDF_GetDeviceInfo(Pointer hSessionHandle, byte[] pstDeviceInfo); + int SDF_GetDeviceInfo(Pointer hSessionHandle, SDF_DeviceInfo pstDeviceInfo); /** * 产生随机数 @@ -63,4 +65,237 @@ public interface SdfLibrary extends Library { int SDF_GenerateRandom(Pointer hSessionHandle, int uiLength, byte[] pucRandom); + /** + * 生成ECC密钥对 + * + * @param phSessionHandle 会话句柄 + * @param uiAlgID 算法标识 + * @param uiKeyBits 密钥长度 + * @param pucPublicKey 输出的ECC公钥结构 + * @param pucPrivateKey 输出的ECC私钥结构 + * @return int 响应码 + */ + int SDF_GenerateKeyPair_ECC( + Pointer phSessionHandle, + int uiAlgID, + int uiKeyBits, + byte[] pucPublicKey, + byte[] pucPrivateKey + ); + + /** + * 导出签名公钥 + * + * @param phSessionHandle + * @param uiKeyIndex + * @param pucPublicKey + * @return + */ + int SDF_ExportSignPublicKey_ECC(Pointer phSessionHandle, + int uiKeyIndex, + byte[] pucPublicKey); + + /** + * 导出加密公钥 + * + * @param phSessionHandle + * @param uiKeyIndex + * @param pucPublicKey + * @return + */ + int SDF_ExportEncPublicKey_ECC(Pointer phSessionHandle, + int uiKeyIndex, + byte[] pucPublicKey); + + /** + * 密钥加密 + * + * @param phSessionHandle 会话句柄 + * @param uiAlgID 算法标识 + * @param pucPublicKey ECC公钥结构 + * @param pucData 输入的数据明文 + * @param pucDatalength 输入的数据明文长度 + * @param pucEncData 输出的密文数据 + * @return int 响应码 + */ + int SDF_ExternalEncrypt_ECC(Pointer phSessionHandle, int uiAlgID, byte[] pucPublicKey, byte[] pucData, int pucDatalength, byte[] pucEncData); + + /** + * 密钥解密 + * + * @param phSessionHandle 会话句柄 + * @param uiAlgID 算法标识 + * @param pucPrivateKey ECC私钥结构 + * @param pucEncData 输入的密文数据 + * @param pucDataOut 输出的数据明文 + * @param pucDatalength 输出的数据明文长度 + * @return int 响应码 + */ + int SDF_ExternalDecrypt_ECC(Pointer phSessionHandle, int uiAlgID, byte[] pucPrivateKey, byte[] pucEncData, byte[] pucDataOut, IntByReference pucDatalength); + + /** + * 外部ECC签名 + * + * @param phSessionHandle 会话句柄 + * @param uiAlgID 算法标识 + * @param pucPublicKey ECC公钥结构 + * @param pucData 输入的数据明文 + * @param pucDataLength 输入的数据明文长度 + * @param pucECCSignature 输出的签名值数据 + * @return int 响应码 + */ + int SDF_ExternalSign_ECC( + Pointer phSessionHandle, + int uiAlgID, + byte[] pucPublicKey, + byte[] pucData, + int pucDataLength, + byte[] pucECCSignature + ); + + /** + * 外部ECC验签 + * + * @param phSessionHandle 会话句柄 + * @param uiAlgID 算法标识 + * @param pucPublicKey ECC公钥结构 + * @param pucData 输入的数据明文 + * @param pucDataLength 输入的数据明文长度 + * @param pucECCSignature 输入的签名值数据 + * @return int 响应码 + */ + int SDF_ExternalVerify_ECC(Pointer phSessionHandle, int uiAlgID, byte[] pucPublicKey, byte[] pucData, int pucDataLength, byte[] pucECCSignature); + + /** + * 基于ECC算法的数字信封转换 + * + * @param hSessionHandle 与设备建立的会话句柄 + * @param uiKeyIndex 密码设备内部存储ECC密钥对索引值 + * @param uiAlgID 外部ECC公钥的算法标识 SGD_SM2_1 + * @param pucPublicKey 外部ECC公钥结构 + * @param pucEncDateIn 缓冲区指针,用于存放输入的会话密钥密文 + * @param pucEncDateOut 缓冲区指针,用于存放输出的会话密钥密文 + * @return 0 成功; 非0 失败,返回错误代码 + */ + int SDF_ExchangeDigitEnvelopeBaseOnECC(Pointer hSessionHandle, int uiKeyIndex, int uiAlgID, byte[] pucPublicKey, byte[] pucEncDateIn, byte[] pucEncDateOut); + + /** + * 导入会话密钥并用内部ECC私钥解密 + * + * @param phSessionHandle + * @param uiISKIndex + * @param puckey + * @param phKeyhandle + * @return + */ + int SDF_ImportKeyWithISK_ECC(Pointer phSessionHandle, + int uiISKIndex, + byte[] puckey, + PointerByReference phKeyhandle); + + /** + * 销毁会话密钥 + * + * @param phSessionHandle + * @param phKeyhandle + * @return + */ + int SDF_DestroyKey(Pointer phSessionHandle, PointerByReference phKeyhandle); + + + /** + * 对称加密 + * + * @param phSessionHandle 会话句柄 + * @param hKeyHandle 密钥句柄 + * @param uiAlgID 算法标识 + * @param pucIV IV数据 + * @param pucData 待加密数据 + * @param pucDataLength 待加密数据长度 + * @param pucEncData 存放密文容器 + * @param pucEncDataLength 密文长度 + * @return int 响应码 + */ + int SDF_Encrypt( + Pointer phSessionHandle, + Pointer hKeyHandle, + int uiAlgID, + byte[] pucIV, + byte[] pucData, + int pucDataLength, + byte[] pucEncData, + IntByReference pucEncDataLength + ); + + /** + * 对称解密 + * + * @param phSessionHandle 会话句柄 + * @param hKeyHandle 密钥句柄 + * @param uiAlgID 算法标识 + * @param pucIV IV数据 + * @param purEncData 指向密文数据的指针 + * @param encDataLength 密文数据长度 + * @param pucData 输出明文数据 + * @param pucDataLength 输出明文数据长度 + * @return int 响应码 + */ + int SDF_Decrypt( + Pointer phSessionHandle, + Pointer hKeyHandle, + int uiAlgID, + byte[] pucIV, + byte[] purEncData, + int encDataLength, + byte[] pucData, + IntByReference pucDataLength + ); + + + /** + * 杂凑运算初始化 + * + * @param phSessionHandle 会话句柄 + * @param uiAlgID 算法标识 + * @param pucPublicKey ECC公钥结构 + * @param pucID 签名者的ID值 + * @param pucIDlength 签名者ID的长度 + * @return int 响应码 + */ + int SDF_HashInit(Pointer phSessionHandle, int uiAlgID, byte[] pucPublicKey, String pucID, int pucIDlength); + + /** + * 多包杂凑运算 + * + * @param phSessionHandle 会话句柄 + * @param pucData 输入的数据明文 + * @param uiDataLength 输入的数据明文长度 + * @return int 响应码 + */ + int SDF_HashUpdate( + Pointer phSessionHandle, // 使用IntByReference,因为SessionHandle可能是引用类型的 + byte[] pucData, // 输入的明文数据作为字节数组 + int uiDataLength // 明文数据长度 + ); + + /** + * 杂凑运算结束 + * + * @param phSessionHandle 会话句柄 + * @param pucHash 杂凑结果 + * @param pucHashLength 杂凑结果长度 + * @return int 响应码 + */ + int SDF_HashFinal(Pointer phSessionHandle, byte[] pucHash, IntByReference pucHashLength); + + + int SDF_CalculateMAC(Pointer phSessionHandle, + Pointer hKeyHadnle, + int uiAlgID, + byte[] pucIV, + byte[] purData, + int pucDatalength, + byte[] pucMAC, + IntByReference pucMACLength); + } diff --git a/chsm-common/src/main/java/com/sunyard/chsm/sdf/model/SDF_DeviceInfo.java b/chsm-common/src/main/java/com/sunyard/chsm/sdf/model/SDF_DeviceInfo.java new file mode 100755 index 0000000..d8c30e6 --- /dev/null +++ b/chsm-common/src/main/java/com/sunyard/chsm/sdf/model/SDF_DeviceInfo.java @@ -0,0 +1,40 @@ +package com.sunyard.chsm.sdf.model; + +import com.sun.jna.Structure; + +/** + * @author liulu + * @version V1.0 + * @since 2022/10/11 + */ +@Structure.FieldOrder({"IssuerName", "DeviceName", "DeviceSerial", "DeviceVersion", "StandardVersion", "AsymAlgAbility", "SymAlgAbility", "HashAlgAbility", "BufferSize"}) +public class SDF_DeviceInfo extends Structure { + + // 设备 生产 厂商名称 + public byte[] IssuerName = new byte[40]; + + // 设备 型号 + public byte[] DeviceName = new byte[16]; + + // 设 备 编 号 ,包 含 :日 期 (8 字 符 )、批 次 号 (3 字 符 )、流 水 号 (5 字 符 ) + public byte[] DeviceSerial = new byte[16]; + + // 密码 设备 内部软件的版本号 + public int DeviceVersion; + + // 密码 设备 支持的接口规范版本号 + public int StandardVersion; + + // 前4字节表示支持的算法,表示方法为非对称算法标识 按位或的结果;后4字节表示算法的最大模长,表示方法 为支持的模长按位或的结果 + public int[] AsymAlgAbility = new int[2]; + + // 所有支持的对称算法,表示方法为对称算法标识按位或 运算结果 + public int SymAlgAbility; + + // 所有支持的杂凑算法,表示方法为杂凑算法标识按位或 运算结果 + public int HashAlgAbility; + + // 支持 的最 大文件存储空间(单位字节) + public int BufferSize; + +} \ No newline at end of file diff --git a/chsm-common/src/main/java/com/sunyard/chsm/sdf/util/LangUtils.java b/chsm-common/src/main/java/com/sunyard/chsm/sdf/util/LangUtils.java new file mode 100644 index 0000000..bfcf9ff --- /dev/null +++ b/chsm-common/src/main/java/com/sunyard/chsm/sdf/util/LangUtils.java @@ -0,0 +1,59 @@ +package com.sunyard.chsm.sdf.util; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +/** + * @author liulu + * @version V1.0 + * @since 2024/11/06 + */ +public abstract class LangUtils { + + public static byte[] merge(byte[]... bytes) { + int newLen = Arrays.stream(bytes).mapToInt(it -> it.length).sum(); + byte[] res = new byte[newLen]; + + int mergedLen = 0; + for (byte[] item : bytes) { + System.arraycopy(item, 0, res, mergedLen, item.length); + mergedLen += item.length; + } + return res; + } + + public static byte[] toByteArray(int i) { + byte[] result = new byte[4]; + result[0] = (byte) ((i >> 24) & 0xFF); + result[1] = (byte) ((i >> 16) & 0xFF); + result[2] = (byte) ((i >> 8) & 0xFF); + result[3] = (byte) (i & 0xFF); + return result; + } + + public static byte[] toByteArray(long i) { + ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES); + buffer.putLong(i); + return buffer.array(); + } + + public static int toInt(byte[] bytes) { + if (bytes == null || bytes.length != Integer.BYTES) { + throw new IllegalArgumentException("转int需要4个字节"); + } + int result = 0; + for (int i = 0; i < 4; i++) { + int shift = (3 - i) * 8; + result += (bytes[i] & 0xFF) << shift; + } + return result; + } + + public static long toLong(byte[] bytes) { + if (bytes == null || bytes.length != Long.BYTES) { + throw new IllegalArgumentException("转long需要8个字节"); + } + ByteBuffer buffer = ByteBuffer.wrap(bytes); + return buffer.getLong(); + } +} diff --git a/chsm-common/src/main/java/com/sunyard/chsm/sdf/util/PaddingUtil.java b/chsm-common/src/main/java/com/sunyard/chsm/sdf/util/PaddingUtil.java new file mode 100644 index 0000000..5aaa532 --- /dev/null +++ b/chsm-common/src/main/java/com/sunyard/chsm/sdf/util/PaddingUtil.java @@ -0,0 +1,65 @@ +package com.sunyard.chsm.sdf.util; + +/** + * @author liulu + */ +public abstract class PaddingUtil { + + public static byte[] PKCS7Padding(byte[] content, int blockSize) { + if (content == null || blockSize < 8) + throw new IllegalStateException("parameter error"); + + int len = content.length; + int remain = len % blockSize; + int NUM = blockSize - remain; + + int Len = blockSize * (len / blockSize + 1); + byte[] padded = new byte[Len]; + System.arraycopy(content, 0, padded, 0, len); + for (int i = 0; i < NUM; i++) + padded[len + i] = (byte) NUM; + + return padded; + } + + public static byte[] PKCS7Padding(byte[] content) { + try { + return PKCS7Padding(content, 16); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static byte[] PKCS5Padding(byte[] content) throws Exception { + return PKCS7Padding(content, 8); + } + + public static byte[] PKCS7Unpadding(byte[] content, int blockSize) + throws Exception { + if (blockSize < 8 || content == null || content.length % blockSize != 0) + throw new Exception("parameter error"); + return PKCS7Unpadding(content); + } + + public static byte[] PKCS7Unpadding(byte[] content) { + if (content == null || content.length < 8) + throw new IllegalStateException("parameter error"); + + int len = content.length; + int NUM = content[len - 1]; + if (NUM > len) + throw new IllegalStateException("invalid padding"); + for (int i = 0; i < NUM; i++) + if (content[len - i - 1] != NUM) + throw new IllegalStateException("invalid padding"); + byte[] unpadded = new byte[len - NUM]; + System.arraycopy(content, 0, unpadded, 0, len - NUM); + return unpadded; + } + + public static byte[] PKCS5Unpadding(byte[] content) throws Exception { + return PKCS7Unpadding(content, 8); + } + + +} diff --git a/chsm-common/src/main/resources/linux-aarch64/libsdf.so b/chsm-common/src/main/resources/linux-aarch64/libsdf.so new file mode 100644 index 0000000..33a7e5a Binary files /dev/null and b/chsm-common/src/main/resources/linux-aarch64/libsdf.so differ diff --git a/chsm-common/src/main/resources/linux-x86-64/libsdf.so b/chsm-common/src/main/resources/linux-x86-64/libsdf.so new file mode 100644 index 0000000..ca2068a Binary files /dev/null and b/chsm-common/src/main/resources/linux-x86-64/libsdf.so differ diff --git a/chsm-common/src/main/resources/win32-x86-64/libsdf.dll b/chsm-common/src/main/resources/win32-x86-64/libsdf.dll new file mode 100644 index 0000000..31f51ad Binary files /dev/null and b/chsm-common/src/main/resources/win32-x86-64/libsdf.dll differ diff --git a/chsm-web-manage/pom.xml b/chsm-web-manage/pom.xml index 426e23d..4ac60cc 100644 --- a/chsm-web-manage/pom.xml +++ b/chsm-web-manage/pom.xml @@ -117,10 +117,19 @@ - + + + src/main/resources + + **/** + + false + + + org.springframework.boot diff --git a/chsm-web-manage/src/main/java/com/sunyard/chsm/task/DeviceTask.java b/chsm-web-manage/src/main/java/com/sunyard/chsm/task/DeviceTask.java index 1a76244..84b77f0 100644 --- a/chsm-web-manage/src/main/java/com/sunyard/chsm/task/DeviceTask.java +++ b/chsm-web-manage/src/main/java/com/sunyard/chsm/task/DeviceTask.java @@ -4,6 +4,13 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.sunyard.chsm.mapper.SpDeviceMapper; import com.sunyard.chsm.model.entity.Device; +import com.sunyard.chsm.sdf.adapter.SdfApiAdapter; +import com.sunyard.chsm.sdf.adapter.SdfApiAdapterFactory; +import com.sunyard.chsm.sdf.context.DeviceContext; +import com.sunyard.chsm.sdf.model.DeviceInfo; +import com.sunyard.chsm.sdf.model.EccPubKey; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.util.encoders.Hex; import org.springframework.beans.factory.InitializingBean; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; @@ -12,12 +19,14 @@ import org.springframework.util.CollectionUtils; import javax.annotation.Resource; import java.time.Duration; +import java.time.LocalDateTime; import java.util.List; /** * @author liulu * @since 2024/11/4 */ +@Slf4j @Component public class DeviceTask implements InitializingBean { @@ -34,7 +43,7 @@ public class DeviceTask implements InitializingBean { for (int i = 1; i < 500; i++) { Page devicePage = spDeviceMapper.selectPage( - new Page<>(i, 100L), + new Page<>(i, 20L), new LambdaQueryWrapper().orderByAsc(Device::getId) ); List records = devicePage.getRecords(); @@ -43,15 +52,38 @@ public class DeviceTask implements InitializingBean { } for (Device record : records) { + DeviceContext context = new DeviceContext(); + context.setServiceIp(record.getServiceIp()); + context.setServicePort(record.getServicePort()); + context.setManufacturer(record.getManufacturer()); + context.setManufacturerModel(record.getManufacturerModel()); + boolean connected = false; + try { + SdfApiAdapter sdfApiAdapter = SdfApiAdapterFactory.newInstance(context); + String dh = sdfApiAdapter.openDevice(); + String sh = sdfApiAdapter.openSession(dh); + DeviceInfo info = sdfApiAdapter.getDeviceInfo(sh); + log.info("get DeviceInfo: {}", info); + EccPubKey eccPubKey = sdfApiAdapter.exportEncPublicKeyECC(sh, 2); + log.info("exportEncPublicKeyECC: {}", Hex.toHexString(eccPubKey.getPubKeyBytes())); + sdfApiAdapter.closeSession(sh); + sdfApiAdapter.closeDevice(dh); + connected = true; - - - - + } catch (Exception ex) { + log.warn("设备 {}:{} 连接异常", record.getServiceIp(), record.getServicePort(), ex); + } + Device up = new Device(); + up.setId(record.getId()); + up.setConnected(connected); + LocalDateTime now = LocalDateTime.now(); + if (connected) { + up.setLastConnectedTime(now); + } + up.setLastCheckTime(now); + spDeviceMapper.updateById(up); } } - - } @Override