Data is encrypted and decrypted during transmission for security purposes. Since the encryption and decryption may be performed on different environments, it is critical to ensure interoperability, that is, the compatibility of cross-platform data. To achieve this purpose, you need to observe the following:
This topic analyzes encryption and decryption failures caused by different data encoding formats and different uses of encryption and decryption algorithms, and provides solutions to help you efficiently and accurately complete encryption and decryption tasks on the HarmonyOS platform and ensure cross-platform data compatibility.
The encryption and decryption process is as follows:
Determine the encryption and decryption algorithm and key to use.
Obtain the data to be encrypted and decrypted.
Initialize the encryption and decryption engine.
Initialize the encryption engine based on the selected encryption algorithm. You can set parameters such as the encryption mode (CBC or ECB) and padding mode to meet specific security requirements.
Perform the encryption operation.
Output the result.
Encryption and decryption are usually performed across platforms. For example, the data encrypted on a server may need to be decrypted on a mobile device. The keys and data processed by the encryption and decryption APIs of the HarmonyOS platform must be in Uint8Array format. Therefore, you need to convert data between the Uint8Array format and other encoding formats to ensure data consistency.
The following describes the conversions between three data formats:
Convert a string between common encoding format and Uint8Array format.
UTF-8 encoding is recommended for conversion between a common text string and Uint8Array. However, ciphertext data (Uint8Array) should be transmitted using Base64 or hexadecimal encoding instead of UTF-8 encoding.
// Convert string to uint8Array.
public static stringToUint8Array(str: string): Uint8Array {
let textEncoder = util.TextEncoder.create('utf-8');
return textEncoder.encodeInto(str);
}
// Convert uint8Array to string.
public static uint8ArrayToString(input: Uint8Array): string {
let textDecoder = util.TextDecoder.create('utf-8');
return textDecoder.decodeToString(input);
} Convert a string between Base64 format and Uint8Array format.
For the Base64-encoded ciphertext sent from a server to HarmonyOS, you need to decode the Base64 data on HarmonyOS, convert it to a Uint8Array byte stream, and then decrypt it.
For the ciphertext sent from HarmonyOS to a server, you need to encode the encrypted byte stream in the Base64 format, convert the byte stream into a string, and then transmit the string to the server.
// Convert base64 to uint8Array.
public static base64ToUint8Array(str: string): Uint8Array {
let base64Helper = new util.Base64Helper();
return base64Helper.decodeSync(str);
}
// Convert uint8Array to base64.
public static uint8ArrayToBase64(input: Uint8Array): string {
let base64Helper = new util.Base64Helper();
return base64Helper.encodeToStringSync(input);
} Convert a string between hexadecimal format and Uint8Array format
// Convert hex to uint8Array.
public static hexStrToUint8Array(data: string): Uint8Array {
return new Uint8Array(buffer.from(data, 'hex').buffer);
}
// Convert uint8Array to hex.
public static uint8ArrayToHexStr(input: Uint8Array): string {
let hexString = '';
for (let i = 0; i < input.length; i++) {
let char = ('00' + input[i].toString(16)).slice(-2);
hexString += char;
}
return hexString;
} Different platforms may support the same algorithms, but the usage of encryption and decryption algorithms can vary across platforms. These differences may lead to issues with cross-platform encryption and decryption.
The encryption and decryption processes on HarmonyOS differ from those on other platforms in the following aspects:
SM2 is a public key cryptographic algorithm based on elliptic curves.
The following describes how to prevent the key and ciphertext format issues when SM2 is used for cross-platform encryption and decryption.
SM2 key format conversion
If SM2 is used for encryption and decryption, HarmonyOS supports ASN.1 serialized data (91 bytes for the public key and 51 bytes for the private key), whereas other platforms use raw, unserialized key data (64 bytes for the public key and 32 bytes for the private key). These keys cannot be directly used on HarmonyOS. Instead, you need to construct the public key specifications using key parameters to generate an SM2 public key object that is compatible with HarmonyOS. The following example demonstrates how to convert a hexadecimal key generated by another platform into an SM2 key that can be used on HarmonyOS.
For example, the public key generated by another platforms is in 04+x+y format. The private key is a 128-bit string.
Pass in the hexadecimal string, and generate an SM2 public key using key parameters.
public static async convertStrToPubKey(keyStr: string): Promise<cryptoFramework.PubKey | undefined> {
const puKeyStr = keyStr.startsWith('04') ? keyStr.slice(2) : keyStr;
const pkPart1 = puKeyStr.slice(0, puKeyStr.length / 2);
const pkPart2 = puKeyStr.slice(puKeyStr.length / 2);
const pks: cryptoFramework.Point = {
x: BigInt('0x' + pkPart1),
y: BigInt('0x' + pkPart2)
};
try {
const pubKeySpec: cryptoFramework.ECCPubKeySpec = {
params: cryptoFramework.ECCKeyUtil.genECCCommonParamsSpec('NID_sm2'),
pk: pks,
algName: 'SM2',
specType: cryptoFramework.AsyKeySpecType.PUBLIC_KEY_SPEC
};
const keypairGenerator = cryptoFramework.createAsyKeyGeneratorBySpec(pubKeySpec);
return keypairGenerator.generatePubKeySync();
} catch (error) {
let err = error as BusinessError;
hilog.error(0x0000, TAG, `convertStrToPubKey failed, error code=${err.code}, message=${err.message}`);
return undefined;
}
} Similarly, you can use SM2 to generate an SM2 private key using key parameters.
public static async convertStrToPriKey(keyStr: string): Promise<cryptoFramework.PriKey | undefined> {
try {
const sks = BigInt('0x' + keyStr);
const priKeySpec: cryptoFramework.ECCPriKeySpec = {
params: cryptoFramework.ECCKeyUtil.genECCCommonParamsSpec('NID_sm2'),
sk: sks,
algName: 'SM2',
specType: cryptoFramework.AsyKeySpecType.PRIVATE_KEY_SPEC
};
const priKeypairGenerator = cryptoFramework.createAsyKeyGeneratorBySpec(priKeySpec);
let sksData = priKeypairGenerator.generatePriKeySync();
return sksData;
} catch (error) {
let err = error as BusinessError;
hilog.error(0x0000, TAG, `convertStrToPriKey failed, error code=${err.code}, message=${err.message}`);
return undefined;
}
} SM2 ciphertext format conversion
SM2 is a Chinese asymmetric encryption algorithm. The ciphertext consists of three parts:
C1: elliptic curve point, which is 65 bytes and used for calculation during decryption.
C2: encrypted data. The length is determined by the encrypted data.
C3: a 32-byte hash value, which is used to verify the integrity of the ciphertext.
Currently, the SM2 ciphertext can be arranged as C1C2C3 according to the old standards, or as C1C3C2 according to the new standards.
Currently, the SM2 ciphertext supported by HarmonyOS is in ASN.1 format, in which the parameters are in the C1C3C2 order. For details about the parameters, see the description of converting the SM2 ciphertext format.
For the ciphertext in C1C2C3 order used by other platforms, you need to extract the required parameters, construct an SM2CipherTextSpec object, and call the genCipherTextBySpec() method to generate the SM2 ciphertext in ASN.1 format. For details, see Converting SM2 Ciphertext (ArkTS)
bytesToBigInt(bytes: Uint8Array): bigint {
let result = 0n;
for (const byte of bytes) {
result = (result << 8n) | BigInt(byte);
}
return result;
}
i2dSM2CipherText(primal_data: string): Uint8Array | undefined {
// Converting a Hex String to byte array.
const cipherBytes = DataConversion.hexStrToUint8Array(primal_data);
// Defines that the length of C1 is 65 bytes (uncompressed format: 04 + x(32) + y(32)).
const c1Length = 65;
const totalLength = cipherBytes.length;
if (totalLength < c1Length + 32) {
hilog.error(0, 'id2_SM2_Ciphertext', 'Non-SM2 criterion compliant.');
}
// Extract C1 Part.
const c1Bytes = cipherBytes.slice(0, c1Length);
// Verify that C1 is in uncompressed format starting with 04.
if (c1Bytes[0] !== 0x04) {
hilog.error(0, 'id2_SM2_Ciphertext', 'C1 is not in uncompressed format.');
}
// Extract x and y coordinates.
const xBytes = c1Bytes.slice(1, 33);
const yBytes = c1Bytes.slice(33, 65);
// Convert to BigInt.
const xBigInt = this.bytesToBigInt(xBytes);
const yBigInt = this.bytesToBigInt(yBytes);
// Extract the C3 part (last 32 bytes).
const c3Start = totalLength - 32;
const c3Bytes = cipherBytes.slice(c3Start);
// Extract C2 part (middle part).
const c2Bytes = cipherBytes.slice(c1Length, c3Start);
let spec: cryptoFramework.SM2CipherTextSpec = {
xCoordinate: xBigInt,
yCoordinate: yBigInt,
cipherTextData: c2Bytes,
hashData: c3Bytes
}
try {
let data = cryptoFramework.SM2CryptoUtil.genCipherTextBySpec(spec, 'C1C3C2');
return data.data;
} catch (error) {
let err = error as BusinessError;
hilog.error(0x0000, 'xxx', ` failed, error code=${err.code}, message=${err.message}`);
return undefined;
}
} Similarly, before the ciphertext in C1C3C2 order in ASN.1 format generated by HarmonyOS is used on other platforms, you need to decode it into raw ciphertext in C1C3C2 order.
d2i_SM2_Ciphertext(standard_data: string): string {
let message: string = standard_data;
if (!message.startsWith('30')) {
hilog.error(0, 'd2i_SM2_Ciphertext', 'Incorrect ciphertext format');
}
message = message.slice('30'.length, message.length);
// The length of the SM2 sequence content.
let sequence_lexHex: string = this.getLenHex(message);
message = message.slice(sequence_lexHex.length, message.length);
let sequence_len: number = this.lenHex2number(sequence_lexHex);
if (sequence_len !== message.length / 2) {
hilog.error(0x0000, 'd2i_SM2_Ciphertext', 'Incorrect ciphertext format');
}
let sm2_sequence = new SM2Sequence();
message = this.readC1(sm2_sequence, message);
message = this.readC3(sm2_sequence, message);
message = this.readC2(sm2_sequence, message);
hilog.info(0x0000, 'sm2_sequence', sm2_sequence.toString());
let primal_data: string = sm2_sequence.C1x + sm2_sequence.C1y + sm2_sequence.C3 + sm2_sequence.C2;
return primal_data;
} AES is the most common symmetric encryption algorithm.
The following describes how to prevent the key length problems and set the offset for the specified block cipher mode when AES is used for cross-platform encryption and decryption.
AES key length
The AES algorithm uses keys of the fixed length.
When initializing the encryption/decryption engine on HarmonyOS, you need to specify the algorithm specifications and the key length. The key length can be 128 bits (corresponding to the string parameter AES128), 192 bits (corresponding to AES192), or 256 bits (corresponding to AES256). In addition, there is a mapping between the length of an AES key and the number of bits. 16 bytes correspond to 128 bits, 24 bytes to 192 bits, and 32 bytes to 256 bits. Therefore, the length of a key can be inferred based on the number of bits.
For example, in the guide Encryption and Decryption with an AES Symmetric Key (GCM Mode), use 'AES128|GCM|PKCS7' to initialize the encryption/decryption algorithm instance. The key used for encryption and decryption must be 128 bits long, that is, 16 characters. Therefore, when generating a key, the input character string is newUint8Array([83,217,231,76,28,113,23,219,250,71,209,210,205,97,32,159]).
Obtaining the Number of Key Bits Based on a String
When calling the encryption and decryption APIs of HarmonyOS, you need to specify the algorithm specifications and the key length. The key length must match the key passed in. If the key is expressed in the form of a byte sequence or a hexadecimal string, the number of bits in the key string cannot be directly obtained.
To determine the number of bits in an AES key from the given key string, do as follows:
Setting of the offset parameter for AES
AES is a block cipher, with a fixed block size of 128 bits. If the last block is less than 128 bits (16 bytes), you can specify the padding mode to pad data.
For example, in GCM mode and CBC mode, the specified string parameters are 'AES128|GCM|PKCS7' and 'AES128|CBC|PKCS7', respectively, with offsets of 16 bytes and 12 bytes, respectively.
| Block Cipher Mode | Parameters | Remarks |
|---|---|---|
| ECB | There are no parameters such as offset. | - |
| CBC, CTR, OFB, CFB | iv, which is of 16 bytes. | - |
| GCM | iv, which is of 1 to 16 bytes. A 12-byte IV is commonly used. add, which is of 0 to INT_MAX bytes. A 16-byte add is commonly used. authTag, which is of 16 bytes. | In GCM mode, extract the last 16 bytes from the encrypted data as the authentication information for initializing the Cipher instance in decryption. |