前言

阅读本文,你将解锁以下成就😎😎😎

  • 鸿蒙应用开发
  • 发布鸿蒙ohpm
  • 了解ArkTSArkUI语法

如果文章对你有帮助的话,记得一键三连哟。有问题和疑惑的话也可以在评论区留言。我会第一时间回复大家,如果觉得我的文章哪里有知识点错误的话,也恳请能够告知,把错的东西理解成对的,无论在什么行业,都是致命的。

image.png

依赖

项目基于OpenHarmonyapi10DevEco Studio 4.0 Beta2

项目仓库

https://github.com/taosiqi/oh-mfa

编辑器

安装

https://developer.huawei.com/consumer/cn/forum/topic/02071242...

汉化

进入插件中心,在已安装里面搜索 chinese,勾选并确认,重启后汉化完成
image.png

SDK下载

image.png下载OpenHarmonyArkUI 相关SDKimage.png
image.png

安装ohpm

ohpm相当于鸿蒙版本的npm,能管理依赖, 同时需要安装node工具,后面需要用到它提交包和安装依赖。
image.png
image.png

配置环境变量

mac为例,其他系统自行搜索

OHPM_HOME=/Users/taosiqi/Library/Huawei/ohpm  # 替换为下图的路径
export PATH=${PATH}:${OHPM_HOME}/bin

项目管理

导入工程

新建项目暂时不能使用api10,需要导入实例工程(不确定新建项目修改配置文件为api10能不能使用)
image.png
image.png
查看配置文件,是用的10版本
image.png

修改项目信息

根据自己的要求,修改包名、Icon、应用名称等
image.png
image.png

编译项目

首次编译项目时,根据控制台提示,生成证书,选择真机运行,模拟器暂时不支持api10项目
image.png
image.png
真机运行
image.png

新增模块(可发布为ohpm包)

在项目根目录,新增模块,新增 Static Library,我这里包名叫totp,后面以这个为例
image.png
image.png
image.png

项目开发

删除无用目录

删除包下面的/totp/src/main/ets/components目录,这个项目是抽离通用方法

编写包代码

/totp/src/main/ets/下新建 totp.ets,导入自己写的公共方法

import { CryptoJS } from '@ohos/crypto-js'

type HashAlgorithm = 'SHA1' | 'SHA224' | 'SHA256' | 'SHA384' | 'SHA512' | 'SHA3';


interface TokenOptions {
  period?: number;
  digits?: number;
  timestamp?: number;
  algorithm?: HashAlgorithm | undefined;
}

/**
 * 生成验证码
 * @param key
 * @param options
 * @returns
 */
export function generateTotp(key: string, options?: TokenOptions): string {
  try {
    let epoch: number, time: string, mac: string, offset: number, otp: string;
    options = options || {};
    options.period = options.period || 30;
    options.digits = options.digits || 6;
    options.timestamp = options.timestamp || Date.now();
    options.algorithm = options.algorithm || "SHA1"

    key = base32hex(key);
    epoch = Math.floor(options.timestamp / 1000.0);
    time = leftPad(dec2hex(Math.floor(epoch / options.period)), 16, "0");

    // 使用CryptoJS计算HMAC,动态选择哈希算法
    const keyHex = CryptoJS.enc.Hex.parse(key);
    const timeHex = CryptoJS.enc.Hex.parse(time);
    mac = hmacDigest(options.algorithm, timeHex, keyHex);

    offset = hex2dec(mac.substring(mac.length - 1));
    otp = ((hex2dec(mac.substring(offset * 2, offset * 2 + 8)) & hex2dec("7fffffff")) + "").substring(0);
    otp = otp.substring(otp.length - options.digits);
    return otp;
  } catch (e) {
    return ''
  }
}

/**
 * 动态选择算法
 * @param algorithm
 * @param message
 * @param key
 * @returns
 */
function hmacDigest(algorithm: HashAlgorithm, message: string, key: string): string {
  let mac
  switch (algorithm) {
    case 'SHA224':
      mac = CryptoJS.HmacSHA224(message, key);
    case 'SHA256':
      mac = CryptoJS.HmacSHA256(message, key);
    case 'SHA384':
      mac = CryptoJS.HmacSHA384(message, key);
    case 'SHA512':
      mac = CryptoJS.HmacSHA512(message, key);
    case 'SHA3':
      mac = CryptoJS.HmacSHA3(message, key);
    case 'SHA1':
    default:
      mac = CryptoJS.HmacSHA1(message, key)
  }
  return mac.toString(CryptoJS.enc.Hex);
}

/**
 * 将十六进制字符串转换为对应的十进制数值
 * @param s
 * @returns
 */
function hex2dec(s: string): number {
  return parseInt(s, 16);
}

/**
 * 将一个十进制数转换为对应的十六进制
 * @param s
 * @returns
 */
function dec2hex(s: number): string {
  return (s < 15.5 ? "0" : "") + Math.round(s).toString(16);
}

/**
 * base32转hex
 * @param base32
 * @returns
 */
function base32hex(base32: string): string {
  const base32chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
  let bits = "";
  let hex = "";
  base32 = base32.replace(/=+$/, "");

  for (let i = 0; i < base32.length; i++) {
    const val = base32chars.indexOf(base32.charAt(i).toUpperCase());
    if (val === -1) throw new Error("Invalid base32 character in key");
    bits += val.toString(2).padStart(5, '0');
  }

  for (let i = 0; i + 8 <= bits.length; i += 8) {
    const chunk = bits.slice(i, i + 8);
    hex += parseInt(chunk, 2).toString(16).padStart(2, '0');
  }

  return hex;
}

/**
 * 左侧填充字符串,确保填充后的字符串长度达到指定的长度
 * @param str
 * @param len
 * @param pad
 * @returns
 */
function leftPad(str: string, len: number, pad: string): string {
  if (len + 1 >= str.length) {
    str = Array(len + 1 - str.length).join(pad) + str;
  }
  return str;
}

安装包级别依赖

cd totp,安装依赖 ohpm install @ohos/crypto-js

导出方法

totp包根目录Index.ets导出方法

import {generateTotp} from "./src/main/ets/totp"

export default generateTotp;

export { generateTotp };

修改包信息

修改包信息为如下格式,有些字段是必填,如果字段存在问题,提交到ohpm的时候提示错误信息

{
  "name": "totp",
  "types": "",
  "keywords": [
    "HarmonyOS",
    "totp",
    "generator",
    "auth",
    "authentication",
    "google authenticator",
    "oath",
    "2-factor",
    "two-factor",
    "mfa"
  ],
  "author": "taosiqi",
  "description": "从密钥生成TOTP验证码 Generate TOTP tokens from key ",
  "main": "Index.ets",
  "repository": "https://github.com/taosiqi/oh-mfa.git",
  "type": "module",
  "version": "0.0.1",
  "tags": [
    "Tools",
    "Security"
  ],
  "license": "MIT",
  "devDependencies": {},
  "dependencies": {
    "@ohos/crypto-js": "^2.0.3"
  }
}

构建包

构建totp
image.png
输出的包目录
image.png

引入本地包

/entry/oh-package.json5使用file:xxx路径,引入本地包,点击右上角Sync Now或打开终端执行 ohpm install命令以同步包

{
  "license": "",
  "devDependencies": {},
  "author": "",
  "name": "entry",
  "description": "Please describe the basic information.",
  "main": "",
  "version": "1.0.0",
  "dependencies": {
    "totp": "file:/Users/taosiqi/DevEcoStudioProjects/testProject/totp/build/default/outputs/default/totp.har"
  }
}

image.png

调试代码

修改entry/src/main/ets/pages/Index.ets,引入包并使用

import { generateTotp } from 'totp'

@Entry
  @Component
  struct Index {
    @State totpKey: string = '5F7XTR3O5ANHF4UI'
    @State genTotpKey: string = ''
    @State second: number = 30
    @State focusableAccount: boolean = false;

    intervalID: number

    countdown() {
      this.intervalID = setInterval(() => {
        this.second= 30 - (new Date().getSeconds() % 30)
        if(this.second===30){
          this.genTotpKey=generateTotp(this.totpKey)
        }
      }, 1000);
    };


    async onPageShow(){
      this.genTotpKey=generateTotp(this.totpKey)
      this.countdown()
    }

    build() {
      Column({ space: 35 }) {
        Text(`倒计时${this.second}s`).fontSize(28)
        Text(this.genTotpKey).fontSize(40)
        TextInput({ text: this.totpKey, placeholder: 'input your word...' }).onChange((value)=>{
          this.totpKey=value
        }).width(300).defaultFocus(false).focusable(this.focusableAccount).onClick(()=>{
          // 默认会聚焦,defaultFocus无效,不知道是不是版本问题
          if(!this.focusableAccount){
            this.focusableAccount = true;
          }
        });
        Button('刷新验证码').onClick(()=>{
          this.genTotpKey=generateTotp(this.totpKey)
        })
        Text('推荐使用《MFA二次验证码》微信小程序版本').fontSize(16)
        Image('https://static-1253419794.cos.ap-nanjing.myqcloud.com/img/code.jpg').width(200).height(200)
      }.justifyContent(FlexAlign.Center).width('100%').height('100%')
    }
  }

运行效果

运行查看实际效果
image.png

打包发布

打包example应用

entry目录下构建hap,打包完的代码就能提供给人安装
image.png

发布准备工作

  • 注册鸿蒙三方库的账号,注册地址 ohpm.openharmony.cn/#/cn/home
  • 生成公私钥:执行ssh-keygen -m PEM -t RSA -b 4096 -f your-keypathmac一般放在~/.ssh目录)
  • ohpm包管理器只支持加密密钥认证,请在生成公私钥时输入密码
  • 配置私钥路径:执行ohpm config set key_path your-keypath(配置直接修改 ~/.ohpm/.ohpmrc也可以)
  • 配置登录用户发布码,在命令行执行:ohpm config set publish_id your-publishId
  • 在个人中心的ohpm公钥管理模块中,添加公钥,并将公钥文件的内容(your-keypath.pub)粘贴到公钥输入框中

发布totp包到ohpm

需要在totp模块根目录添加CHANGELOG.mdREADME.md LICENSE 等必要文件,且不能为空。
image.png

ohpm publish /testProject/totp/build/default/outputs/default/totp.har

遇到的问题

  1. publish时,提示artifactType相关错误,修改build-profile.json5,增加以下字段
"buildOption": {
  "artifactType": "original"
},

审核状态image.png

致谢

鸿蒙应用开发-我的第一个三方库 - 掘金的文章中学习到了不少内容

引用

首发于语雀文档@is_tao


陶某
103 声望5 粉丝

欢迎点赞star加评论😊😊😊