diff --git a/entry/src/test/List.test.ets b/entry/src/test/List.test.ets index e1c8873..3f08e15 100644 --- a/entry/src/test/List.test.ets +++ b/entry/src/test/List.test.ets @@ -2,10 +2,12 @@ import localUnitTest from './LocalUnit.test'; import bridgeTest from './Bridge.test'; import versionResolverTest from './VersionResolver.test'; import shareTextTest from './ShareText.test'; +import qiniuTokenTest from './QiniuToken.test'; export default function testsuite() { localUnitTest(); bridgeTest(); versionResolverTest(); shareTextTest(); + qiniuTokenTest(); } diff --git a/entry/src/test/QiniuToken.test.ets b/entry/src/test/QiniuToken.test.ets new file mode 100644 index 0000000..220dde8 --- /dev/null +++ b/entry/src/test/QiniuToken.test.ets @@ -0,0 +1,20 @@ +import { describe, it, expect } from '@ohos/hypium'; +import { QiniuToken } from 'platform'; +import { util } from '@kit.ArkTS'; + +export default function QiniuTokenTest() { + describe('QiniuTokenTest', () => { + it('token_has_three_parts_and_valid_policy', 0, async () => { + const token: string = await QiniuToken.uploadToken(); + const parts: string[] = token.split(':'); + expect(parts.length).assertEqual(3); + expect(parts[0]).assertEqual('ngN3rFW1j8dn7ZGpATKl7mreaNmp2Ei_l9AIhkIf'); + expect(parts[1].length).assertEqual(28); + const helper = new util.Base64Helper(); + const std: string = parts[2].replace(/-/g, '+').replace(/_/g, '/'); + const decoded: Uint8Array = helper.decodeSync(std); + const policy: string = String.fromCharCode.apply(null, Array.from(decoded)); + expect(policy.indexOf('"scope":"gameauio"') >= 0).assertTrue(); + }); + }); +} diff --git a/platform/Index.ets b/platform/Index.ets index f91243f..50d473e 100644 --- a/platform/Index.ets +++ b/platform/Index.ets @@ -15,3 +15,4 @@ export { Unzipper } from './src/main/ets/zip/Unzipper'; export { PermissionGuard } from './src/main/ets/perm/PermissionGuard'; export { LocalUploadServer } from './src/main/ets/upload/LocalUploadServer'; export { QiniuUploader, QiniuTokenProvider } from './src/main/ets/upload/QiniuUploader'; +export { QiniuToken } from './src/main/ets/upload/QiniuToken'; diff --git a/platform/src/main/ets/upload/QiniuToken.ets b/platform/src/main/ets/upload/QiniuToken.ets new file mode 100644 index 0000000..e76a2ce --- /dev/null +++ b/platform/src/main/ets/upload/QiniuToken.ets @@ -0,0 +1,42 @@ +/** + * 七牛 uploadToken 端侧生成(设计 §7)。 + * ⚠️ 刻意对齐原 Android 工程:AK/SK 端侧硬编码、不走服务端——违背 CLAUDE.md 附录B 加固红线, + * 已与用户明确确认(对齐原工程)。token 算法见七牛官方:AK:urlsafeB64(HMAC_SHA1(SK,policy)):policy。 + */ +import { cryptoFramework } from '@kit.CryptoArchitectureKit'; +import { buffer, util } from '@kit.ArkTS'; + +export class QiniuToken { + private static readonly ACCESS_KEY: string = 'ngN3rFW1j8dn7ZGpATKl7mreaNmp2Ei_l9AIhkIf'; + private static readonly SECRET_KEY: string = '6VXav9eqUCORJTYvTFOeTVRonAyk4Hrs-PIZ8jmZ'; + private static readonly BUCKET: string = 'gameauio'; + + /** 生成 uploadToken(deadline=now+1h)。 */ + static async uploadToken(): Promise { + const deadline: number = Math.floor(Date.now() / 1000) + 3600; + const policy: string = `{"scope":"${QiniuToken.BUCKET}","deadline":${deadline}}`; + const encodedPolicy: string = + QiniuToken.urlsafeBase64(new Uint8Array(buffer.from(policy, 'utf-8').buffer)); + const sign: Uint8Array = await QiniuToken.hmacSha1(QiniuToken.SECRET_KEY, encodedPolicy); + const encodedSign: string = QiniuToken.urlsafeBase64(sign); + return `${QiniuToken.ACCESS_KEY}:${encodedSign}:${encodedPolicy}`; + } + + private static async hmacSha1(secret: string, message: string): Promise { + const gen = cryptoFramework.createSymKeyGenerator('HMAC'); + const key = await gen.convertKey({ data: new Uint8Array(buffer.from(secret, 'utf-8').buffer) }); + // eslint-disable-next-line @security/no-unsafe-mac -- 七牛 uploadToken 协议强制 HMAC-SHA1,非自选算法 + const mac = cryptoFramework.createMac('SHA1'); + await mac.init(key); + await mac.update({ data: new Uint8Array(buffer.from(message, 'utf-8').buffer) }); + const out = await mac.doFinal(); + return out.data; + } + + /** 标准 Base64 → 七牛 URL-safe(保留 padding,+→- /→_)。 */ + private static urlsafeBase64(bytes: Uint8Array): string { + const helper = new util.Base64Helper(); + const std: string = helper.encodeToStringSync(bytes, util.Type.BASIC); + return std.replace(/\+/g, '-').replace(/\//g, '_'); + } +}