背景

手上持有多张电话卡,包括大学注册的电话卡、工作之后办的工作卡、宽带送的亲情号、还有专门对付快递的小号,满打满算已经 4 张卡了。虽然双持手机,每个手机插两张卡,但是实际使用的时候,不同账号登录使用不同的手机号,依然需要在两个手机上来回查看验证码,非常的不优雅,于是考虑通过短信转发的方式将多个手机号的短信都统一转发至一个手机号上,在实现这个方案的路上遇到了两个问题:

  • 有两张卡是保号卡,顾名思义,除了 8 元套餐送的流量以外没有免费的短信额度,转发成本太高
  • 可爱的运营商有短信拦截,涉及敏感信息的短信无法转发

一开始采用的方法是卡号互相搭配,一张有免费短信的卡+一张保号套餐,通过短信 base64 编码之后进行转发,可以完美解决上述两个问题,同时带来了另一个问题:接受到的 base64 编码的短信如果需要再次查看,需要手动解码,而且短信内容一多之后完全不知道每条短信是谁发送的,如果遇到多个号码同时转发的情况,需要逐条排查。


考虑到未来的使用成本和可靠性,最终决定采用 XOR 加密 + 云端服务器的方式实现短信加密和短信转发,具体工作流程如下:
 

短信转发流程

准备工作

  • 短信发送侧:安卓手机+tasker
  • 短信接受侧:苹果手机+scriptable
  • 云服务器:阿里云(也可以使用 cloudflare 的无状态服务直接白嫖)
    点击对应软件可以跳转至官方网站

执行步骤

  1. 设置配置,当接受到短信时执行任务
     
设置配置

2. 创建任务
 

创建任务

3. 配置全局环境变量
 


其中 token 用于存储生成的密钥,xortext 用于存储 xor 加密后的短信内容

4. 编写 tasker 加密代码

// 步骤1:从Tasker全局变量获取加密token
var token = global('TOKEN');
if (!token) {
    flash("错误:未找到token全局变量(变量名:token,当前值:" + token + ")");
    exit();
}
// 步骤2:获取短信内容(Tasker 中使用 %SMSRB 获取短信内容)
var smsContent = global('SMSRB');
if (!smsContent) {
    flash("错误:未获取短信内容");
    exit();
}

// XOR加密函数(兼容Unicode)
function xorEncrypt(text, key) {
    var result = [];
    var keyIndex = 0;
    
    // 将字符串转为UTF-8字节数组
    var encoder = new TextEncoder();
    var data = encoder.encode(text);
    var keyData = encoder.encode(key);
    
    // 执行XOR加密
    for (var i = 0; i < data.length; i++) {
        var encryptedByte = data[i] ^ keyData[keyIndex];
        result.push(encryptedByte);
        keyIndex = (keyIndex + 1) % keyData.length;
    }
     // 将字节数组转为16进制字符串
     var hexString = '';
     for (var j = 0; j < result.length; j++) {
         var hex = result[j].toString(16);
         // 确保每个字节是两位的十六进制表示
         hexString += (hex.length === 1 ? '0' + hex : hex);
     }
     return hexString;
}

try {
    // 执行加密和编码
    var encryptedData = xorEncrypt(smsContent, token);
    // 将加密后的数据保存至 XORTEXT 全局变量中
    setGlobal("XORTEXT", encryptedData)
    // 测试加密结果
    flash("加密结果" + encryptedData)
} catch (e) {
    flash("发生异常:" + e.message);
    exit();
}

完成安卓侧的短信发送以后,现在开始编写 IOS 侧的小组件用于获取短信内容

// 小组件配置
const widget = new ListWidget();
widget.spacing = 8;

// 主函数
async function main() {
  try {
    // 1. 发起GET请求获取数据
    const apiUrl = "https://api.example.com";
    const request = new Request(apiUrl);
    request.method = "GET";
    request.headers = { "Content-Type": "application/json" };
    const response = await request.loadJSON();
    
    // 2. 提取 results 中的 content 数据
    const contents = response.results.map(item => item.content);
    
    // 3. 对每个 content 应用算法A(示例:XOR解码算法)
    const parsedContents = contents.map(content => xorDecrypt(content));
    
    // 3. 展示数据到小组件
    displayData(parsedContents);
    
    // 4. 添加刷新按钮
    addRefreshButton();
    
  } catch (error) {
    widget.addText("⚠️ 数据加载失败");
    widget.addText(error.message);
  }
  
  // 最终渲染小组件
  Script.setWidget(widget);
}

// XOR 解密算法
function xorDecrypt(message) {
    // 修改你的 token
    token = "this is a example token"
    // 1. 将 Hex 字符串转为字节数组
    const bytes = [];
    for (let i = 0; i < message.length; i += 2) {
        const byte = parseInt(message.substr(i, 2), 16);
        bytes.push(byte);
    }

    // 2. 将密钥转为字节数组(手动 UTF-8 编码)
    const keyBytes = [];
    for (let i = 0; i < token.length; i++) {
        const charCode = token.charCodeAt(i);
        if (charCode < 128) {
            keyBytes.push(charCode); // ASCII 字符(1字节)
        } else if (charCode < 2048) {
            keyBytes.push(0xC0 | (charCode >> 6)); // 2字节 UTF-8
            keyBytes.push(0x80 | (charCode & 0x3F));
        } else {
            keyBytes.push(0xE0 | (charCode >> 12)); // 3字节 UTF-8
            keyBytes.push(0x80 | ((charCode >> 6) & 0x3F));
            keyBytes.push(0x80 | (charCode & 0x3F));
        }
    }
    // 3. 执行 XOR 解密(逐字节异或,循环使用密钥)
    const result = [];
    let keyIndex = 0;
    for (const byte of bytes) {
        const decryptedByte = byte ^ keyBytes[keyIndex];
        result.push(decryptedByte);
        keyIndex = (keyIndex + 1) % keyBytes.length;
    }

     // 4. 将解密后的字节数组转为字符串(手动 UTF-8 解码)
     let str = "";
     let i = 0;
     while (i < result.length) {
         const byte1 = result[i++];
         if (byte1 < 128) {
             str += String.fromCharCode(byte1); // ASCII
         } else if (byte1 >= 192 && byte1 < 224) {
             const byte2 = result[i++];
             str += String.fromCharCode(((byte1 & 0x1F) << 6) | (byte2 & 0x3F));
         } else {
             const byte2 = result[i++];
             const byte3 = result[i++];
             str += String.fromCharCode(((byte1 & 0x0F) << 12) | ((byte2 & 0x3F) << 6) | (byte3 & 0x3F));
         }
     }
     return str;  // 返回解码后的字符串而不是字节数组
}

// 展示数据到小组件
function displayData(contents) {
  const title = widget.addText("📊 解析结果");
  title.font = Font.boldSystemFont(16);
  
  contents.forEach(content => {
    const item = widget.addText(content);
    item.font = Font.systemFont(12);
    item.textColor = Color.gray();
  });
}

// 添加刷新按钮
function addRefreshButton() {
    widget.addSpacer();
    const refreshButton = widget.addText("🔄 刷新数据");
    refreshButton.font = Font.mediumSystemFont(14);
    refreshButton.textColor = Color.blue();
    
    // 修改后的刷新按钮URL,无需二次确认
    refreshButton.url = "scriptable:///run?scriptName=" + encodeURIComponent(Script.name());
  }

// 执行主函数
await main();
Script.complete();

实现效果

短信发送内容


 

展示效果


 

服务器加密


可以看到,安卓手机自行通过 token 加密了对应的短信,并将加密后的数据上传至服务器,服务器没有 token 无法解密对应的数据,IOS 通过组件定时或手动拉取对应的数据进行展示,实现了短信的聚合和转发。

后记

这里集中解释一下几个问题:

  1. 为什么使用 16 进制输出而不是 base64 编解码?
    IOS 自带的捷径是有 bug 的,不能解码非标准的 base64 编码。XOR 加密很容易出现无法打印的字符,即使通过 base64 编码,在捷径上依然无法正常解码,不得已只能采用 16 进制输出的方式,代价是加密后的字符长度变为原来的 4 倍。
  2. 不具备可复制性,没有服务器怎么办?
    有两种办法:第一,采用比较简单的 base64 加密直接进行短信转发,可以绕过运营商的监控,缺点就是费短信钱。第二,采用 cloudflare 的免费接口实现无服务转发,这需要一定的开发能力。未来可能会出一版使用 cloudflare 实现的短信转发接口。
  3. 想要改功能但不会写代码怎么办?
    本文中涉及的所有代码均由 deepseek 模型生成,本人仅进行复制及测试工作,包括文章的头图都是 AI 生成的。AI 元年请学会与 AI 一同工作,如果很想实现某个功能又不想用 AI 的,本人也提供有偿服务。