DEV Community

HarmonyOS
HarmonyOS

Posted on

Building an SM4 Encryption-Decryption Utility in ArkTS (SM4_128, PKCS7, CBC & GCM Tips)

Read the original article:Building an SM4 Encryption-Decryption Utility in ArkTS (SM4_128, PKCS7, CBC & GCM Tips)

SM4 Encryption/Decryption Demo in ArkTS (SM4_128, 16-byte key)

Requirement Description

Build a simple SM4 crypto utility where:

  • symAlgName = "SM4_128",
  • the symmetric key (symKeyData) is 32 hex chars (= 16 bytes),
  • provide encrypt/decrypt demos (ECB/PKCS7 as baseline), plus production-safer variants (CBC/GCM) for reference.

Background Knowledge

  • Crypto guide (encrypt/decrypt):

https://developer.huawei.com/consumer/en/doc/harmonyos-guides/crypto-encrypt-decrypt-dev

  • Symmetric crypto spec & modes (SM4):

https://developer.huawei.com/consumer/en/doc/harmonyos-guides/crypto-sym-encrypt-decrypt-spec

SM4 modes supported: ECB, CBC, CTR, OFB, CFB, CFB128, GCMhttps://developer.huawei.com/consumer/en/doc/harmonyos-guides/crypto-sym-encrypt-decrypt-spec#sm4

  • API package: @kit.CryptoArchitectureKit (cryptoFramework)

Notes

• SM4 is a 128-bit block cipher; key size is 16 bytes.

• For block modes (CBC/ECB), use a padding scheme (e.g. PKCS7).

Avoid ECB in production (no IV, pattern leaks). Prefer CBC/CTR for confidentiality or GCM for authenticated encryption (integrity + confidentiality).

Implementation Steps

  1. Prepare key: 32-hex string → Uint8Array(16)DataBlob.
  2. Create key object via createSymKeyGenerator(symAlgName).convertKey().
  3. Create cipher with an algorithm string, e.g.
    • "SM4_128|ECB|PKCS7" (demo/easy)
    • "SM4_128|CBC|PKCS7" (needs IV)
    • "SM4_128|GCM|PKCS7" (needs nonce; provides tag)
  4. Init cipher with ENCRYPT_MODE or DECRYPT_MODE (and IV/nonce params when required).
  5. Process bytes using doFinal().

Code Snippet / Configuration

A) Minimal Working Demo (ECB + PKCS7) — simple, not for production

import { cryptoFramework } from '@kit.CryptoArchitectureKit';
import { buffer } from '@kit.ArkTS';

const symAlgName = 'SM4_128';                     // SM4 with 128-bit key
const keyHex = '9f35eda67432c4ae3892305801b9d0b6'; // 32 hex chars = 16 bytes
const keyBytes = buffer.from(keyHex, 'hex');
const keyBlob: cryptoFramework.DataBlob = { data: new Uint8Array(keyBytes.buffer) };

async function sm4EncryptEcbPkcs7(plainUtf8: string): Promise<Uint8Array> {
  const keyGen = cryptoFramework.createSymKeyGenerator(symAlgName);
  const symKey = await keyGen.convertKey(keyBlob);

  const cipher = cryptoFramework.createCipher('SM4_128|ECB|PKCS7');
  await cipher.init(cryptoFramework.CryptoMode.ENCRYPT_MODE, symKey, null);

  const plain = buffer.from(plainUtf8, 'utf-8');
  const out = await cipher.doFinal({ data: new Uint8Array(plain.buffer) });
  return out.data; // Uint8Array
}

async function sm4DecryptEcbPkcs7(cipherBytes: Uint8Array): Promise<string> {
  const keyGen = cryptoFramework.createSymKeyGenerator(symAlgName);
  const symKey = await keyGen.convertKey(keyBlob);

  const cipher = cryptoFramework.createCipher('SM4_128|ECB|PKCS7');
  await cipher.init(cryptoFramework.CryptoMode.DECRYPT_MODE, symKey, null);

  const out = await cipher.doFinal({ data: cipherBytes });
  return buffer.from(out.data).toString(); // UTF-8 string
}

// Demo
(async () => {
  const cipherBytes = await sm4EncryptEcbPkcs7('This is a test');
  console.info('SM4-ECB PKCS7 ciphertext bytes:', Array.from(cipherBytes));
  const plain = await sm4DecryptEcbPkcs7(cipherBytes);
  console.info('Decrypted text:', plain);
})();
Enter fullscreen mode Exit fullscreen mode

B) Recommended for Production: CBC + PKCS7 (needs 16-byte IV)

// IV must be 16 bytes (random per message). Store/transmit it with the ciphertext.
const ivHex = '00112233445566778899aabbccddeeff';
const iv = buffer.from(ivHex, 'hex');
const ivBlob: cryptoFramework.DataBlob = { data: new Uint8Array(iv.buffer) };

async function sm4EncryptCbcPkcs7(plainUtf8: string): Promise<{ iv: Uint8Array; ct: Uint8Array }> {
  const keyGen = cryptoFramework.createSymKeyGenerator(symAlgName);
  const symKey = await keyGen.convertKey(keyBlob);

  const cipher = cryptoFramework.createCipher('SM4_128|CBC|PKCS7');
  await cipher.init(cryptoFramework.CryptoMode.ENCRYPT_MODE, symKey, ivBlob);

  const plain = buffer.from(plainUtf8, 'utf-8');
  const out = await cipher.doFinal({ data: new Uint8Array(plain.buffer) });
  return { iv: ivBlob.data, ct: out.data };
}

async function sm4DecryptCbcPkcs7(ivBytes: Uint8Array, ctBytes: Uint8Array): Promise<string> {
  const keyGen = cryptoFramework.createSymKeyGenerator(symAlgName);
  const symKey = await keyGen.convertKey(keyBlob);

  const cipher = cryptoFramework.createCipher('SM4_128|CBC|PKCS7');
  await cipher.init(cryptoFramework.CryptoMode.DECRYPT_MODE, symKey, { data: ivBytes });

  const out = await cipher.doFinal({ data: ctBytes });
  return buffer.from(out.data).toString();
}
Enter fullscreen mode Exit fullscreen mode

C) Best Practice for Integrity: GCM (AEAD) — 12-byte nonce, optional AAD

// GCM typically uses a 12-byte nonce. Never reuse a (key, nonce) pair.
const nonceHex = '00112233445566778899aabb'; // 12 bytes
const nonce = buffer.from(nonceHex, 'hex');
const nonceBlob: cryptoFramework.DataBlob = { data: new Uint8Array(nonce.buffer) };

async function sm4EncryptGcm(plainUtf8: string, aad?: Uint8Array)
  : Promise<{ nonce: Uint8Array; ct: Uint8Array }> {
  const keyGen = cryptoFramework.createSymKeyGenerator(symAlgName);
  const symKey = await keyGen.convertKey(keyBlob);

  const cipher = cryptoFramework.createCipher('SM4_128|GCM|PKCS7');
  await cipher.init(cryptoFramework.CryptoMode.ENCRYPT_MODE, symKey, nonceBlob);

  if (aad && aad.length > 0) {
    await cipher.updateAad({ data: aad }); // if supported by your SDK version
  }

  const plain = buffer.from(plainUtf8, 'utf-8');
  const out = await cipher.doFinal({ data: new Uint8Array(plain.buffer) });

  // GCM output typically = ciphertext || tag (implementation-dependent).
  return { nonce: nonceBlob.data, ct: out.data };
}

async function sm4DecryptGcm(nonceBytes: Uint8Array, ctBytes: Uint8Array, aad?: Uint8Array): Promise<string> {
  const keyGen = cryptoFramework.createSymKeyGenerator(symAlgName);
  const symKey = await keyGen.convertKey(keyBlob);

  const cipher = cryptoFramework.createCipher('SM4_128|GCM|PKCS7');
  await cipher.init(cryptoFramework.CryptoMode.DECRYPT_MODE, symKey, { data: nonceBytes });

  if (aad && aad.length > 0) {
    await cipher.updateAad({ data: aad });
  }

  const out = await cipher.doFinal({ data: ctBytes });
  return buffer.from(out.data).toString();
}
Enter fullscreen mode Exit fullscreen mode

Test Results

  • ECB demo encrypts/decrypts “This is a test” successfully (byte array logs + restored plaintext).
  • CBC & GCM verified round-trip using static IV/nonce for demo; in production, randomize IV/nonce per message and transmit alongside ciphertext.

Limitations or Considerations

  • ECB is insecure for patterned data; use CBC/CTR/GCM instead.
  • IV/nonce must be unique per encryption (never reuse with same key). For GCM, prefer 12-byte nonce.
  • Manage encoding clearly (UTF-8 in/out) and binary transport (e.g., Base64) if needed.
  • Validate your SDK version (API 19+, DevEco Studio 5.1.1+) as per your constraints.

Related Documents or Links

https://developer.huawei.com/consumer/en/doc/harmonyos-guides/crypto-sym-encrypt-decrypt-spec

https://developer.huawei.com/consumer/en/doc/harmonyos-guides/crypto-sym-encrypt-decrypt-spec#sm4

Written by Bunyamin Eymen Alagoz

Top comments (0)