TOTP算法实现微信支付宝付款码离线支付

250 阅读4分钟

TOTP原理:

       TOTP(Time-Based One-Time Password)算法是基于时间的一次性密码算法,根据客户端(被验方)和服务端(校验方)的密钥与当前时间(时间戳)计算一次性密码。使用TOTP算法,要求客户端(被验方)和服务端(校验方)保持时钟一致,且双方预先设置好一个共享密钥的前提下,在同一个时间(时间戳)算出来的TOTP值是一样的。

使用TOTP条件:

       1.要求客户端(被验方)和服务端(校验方)保持时钟一致;

       2.共享密钥一致;

既然要实现类型微信、支付宝付款码支付,首先以微信线下付款码支付为例(微信官付款码支付场景介绍如下):

商家扫码-->订单信息+商家信息+付款码提交给微信服务-->微信服务拿到付款码解密用户信息+订单信息+商家信息进行后续生成订单支付扣费操作;

这个过程中订单信息(消费金额)是由商家(商品)决定,那么消费者是谁呢?消费者就是这个“付款码”;付款码包含了消费者信息(这个应该是由付款码算法生成决定的,具体怎么实现不得而知),这一部分是个人理解,大致如此,今天的重点不是微信或者支付宝,今天主要说下怎么用totp算法实现一个类似微信、支付宝付款码支付的例子。

totp算法:

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.lang.reflect.UndeclaredThrowableException;
import java.nio.ByteBuffer;
import java.security.GeneralSecurityException;
public class TOTP {    
    private static final int[] DIGITS_POWER            
    // 0 1 2 3 4 5 6 7 8            
    = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000};
    private TOTP(){ 
   }    
/**    
 * This method uses the JCE to provide the crypto algorithm. HMAC computes a 
 * Hashed Message Authentication Code with the crypto hash algorithm as a
 * parameter.     
 *     
 * @param crypto   : the crypto algorithm (HmacSHA1, HmacSHA256, HmacSHA512)     
 * @param keyBytes : the bytes to use for the HMAC key     
 * @param text     : the message or text to be authenticated    
 */    
 private static byte[] hmacSha(String crypto, byte[] keyBytes, byte[] text) {       
     try {            
             Mac hmac;           
             hmac = Mac.getInstance(crypto);           
             SecretKeySpec macKey = new SecretKeySpec(keyBytes, "RAW");            
             hmac.init(macKey);            return hmac.doFinal(text);        
        } catch (GeneralSecurityException gse) {  
          throw new UndeclaredThrowableException(gse);       
         }    
  }  
  /**     
* This method generates a TOTP value for the given set of parameters.     
* 
* @param key    : the shared secret     
* @param time   : a value that reflects a time     
* @param digits : number of digits to return     
* @param crypto : the crypto function to use     
* @return: digits     
*/    
public static int generateTOTP(byte[] key, long time, int digits, String crypto) {    
    byte[] msg = ByteBuffer.allocate(8).putLong(time).array();        
    byte[] hash = hmacSha(crypto, key, msg);        
    // put selected bytes into result int        
    int offset = hash[hash.length - 1] & 0xf;        
    int binary = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16) | ((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff);  
    int otp = binary % DIGITS_POWER[digits];
    return otp;
 }
}

TOTPUtil工具类

import org.apache.commons.codec.binary.Base32;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Random;public class TOTPUtils { 
   private static final int SECRET_SIZE = 10; 
   private static final int PASS_CODE_LENGTH = 6;
   private static final int INTERVAL = 30; 
   private static final int WINDOW = 2;
   private static final String CRYPTO = "HmacSHA1"; 
   private static final Random rand = new Random();

   public static String generateSecret() {
        // Allocating the buffer
        byte[] buffer = new byte[SECRET_SIZE];
        // Filling the buffer with random numbers.
        rand.nextBytes(buffer); 
       // Getting the key and converting it to Base32
        Base32 codec = new Base32();
        byte[] secretKey = Arrays.copyOf(buffer, SECRET_SIZE);
        byte[] encodedKey = codec.encode(secretKey);
        return new String(encodedKey);
    }
    public static Long generateTOTP(String secret, long time) {
        Base32 codec = new Base32();
        byte[] decodedKey = codec.decode(secret);
        int totp = TOTP.generateTOTP(decodedKey, time, PASS_CODE_LENGTH, CRYPTO);
        return Long.valueOf(totp);
    }
    public static boolean checkCode(String secret, long code) throws NoSuchAlgorithmException, InvalidKeyException {
        Base32 codec = new Base32();        byte[] decodedKey = codec.decode(secret);
        // Window is used to check codes generated in the near past.
        // You can use this value to tune how far you're willing to go.
        int window = WINDOW;
        long currentInterval = getCurrentInterval();
        for (int i = -window; i <= window; ++i) {
            long hash = TOTP.generateTOTP(decodedKey, currentInterval + i, PASS_CODE_LENGTH, CRYPTO);
            if (hash == code) {                return true;            }        }
        // The validation code is invalid.
        return false;
    }
    private static long getCurrentInterval() {
        long currentTimeSeconds = System.currentTimeMillis() / 1000;
        return currentTimeSeconds / INTERVAL;
    }
    public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException, InterruptedException, ParseException {
        final String secret = "G3XFFLRBUXY7AQXB";
//       final  String  secret = generateSecret();
        System.out.println("secret:" + secret);
        String dateFormat = "yyyy-MM-dd HH:mm:ss";
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat);
//      Date date = simpleDateFormat.parse("2024-12-12 23:59:59"); 
        Date date = new Date();
        System.out.println("生成totp时间:" + simpleDateFormat.format(date));
        System.out.println("当前    时间:" + simpleDateFormat.format(new Date(System.currentTimeMillis())));
        //时间戳
        long timeSeconds = date.getTime() / 1000 / INTERVAL;
        //当前时间时间戳
        //long timeSeconds = getCurrentInterval();
        Long totp = generateTOTP(secret, timeSeconds);
        System.out.println("totp:" + totp);
        //假设系统最大用户量最大1000亿(很极限了)
        //大于用户量最小质数
        Long zhishu = 1000000000007L;
        //用户Id 
       Long userId = 2L;
        //付款码
        Long fukuanma = totp * zhishu + userId;
        System.out.println("用户Id:" + userId);
        System.out.println("付款码:" + fukuanma + ",付款码长度:" + String.valueOf(fukuanma).length());
        System.out.println("反向解密付款码开始》》");
        System.out.println("解密后用户Id:" + fukuanma % zhishu);
        Long jmtotp = fukuanma / zhishu;
        System.out.println("解密后totp:" + jmtotp);
        boolean checkCodeResult = checkCode(secret, jmtotp);
        System.out.println("totp校验结果:" + checkCodeResult);
    }
}

测试结果:

客户端(被验方)生成付款码:付款码 = totp * 质数因子+ 用户ID;

服务端(校验方) totp = 付款码 / 质数因子;

服务端(校验方)用户ID = 付款码 % 质数因子;

服务端拿到用户ID+totp即可检验用户已经totp有效性;

服务端大致这些,等后面时候就写一个小程序前端(客户端(被验方)生成付款码)案例整合一下服务端(校验方)+服务端(校验方);