diff --git a/pubnub/lib/src/crypto/crypto.dart b/pubnub/lib/src/crypto/crypto.dart index d451f62f..6efe8d07 100644 --- a/pubnub/lib/src/crypto/crypto.dart +++ b/pubnub/lib/src/crypto/crypto.dart @@ -6,6 +6,90 @@ import 'dart:typed_data' show Uint8List; import 'package:pubnub/core.dart'; import 'encryption_mode.dart'; +class CryptorHeader { + static const SENTINEL = 'PNED'; + static const LEGACY_IDENTIFIER = ''; + static const IDENTIFIER_LENGTH = 4; + static const MAX_VERSION = 1; + + static CryptorHeaderV1? from(String id, List metadata) { + if (id == LEGACY_IDENTIFIER) return null; + return CryptorHeaderV1(id, metadata.length); + } + + static CryptorHeaderV1? tryParse(List encryptedData) { + List sentinel; + var version; + if (encryptedData.length >= 4) { + sentinel = encryptedData.sublist(0,4).toList(); + if (utf8.decode(sentinel) != SENTINEL) return null; + } + + if (encryptedData.length >= 5) { + version = encryptedData[4]; + } else { + throw PubNubException('decryption error'); + } + if (version > MAX_VERSION) throw PubNubException('unknown cryptor'); + + var identifier; + var pos = 5 + IDENTIFIER_LENGTH; + if (encryptedData.length >= pos) { + identifier = encryptedData.sublist(5, pos).toList(); + } else { + throw PubNubException('decryption error'); + } + var metadataLength; + if (encryptedData.length > pos + 1) { + metadataLength = encryptedData[pos]; + } + pos += 1; + if (metadataLength == 255 && encryptedData.length >= pos + 2) { + metadataLength = encryptedData + .sublist(pos, pos + 2) + .fold(0, (acc, el) => (acc << 8) + el); + } + return CryptorHeaderV1(utf8.decode(identifier), metadataLength); + } +} + +class CryptorHeaderV1 { + static const VERSION = 1; + final String _identifier; + final int _metadataLength; + + CryptorHeaderV1(this._identifier, this._metadataLength); + + String get identifier => _identifier; + int get metadataLength => _metadataLength; + + int get length { + return (CryptorHeader.SENTINEL.length + + 1 + + CryptorHeader.IDENTIFIER_LENGTH + + (_metadataLength < 225 ? 1 : 3) + + _metadataLength); + } + + List get data { + var pos = 0; + var header = List.filled(length, 0); + header.setAll(pos, CryptorHeader.SENTINEL.codeUnits); + pos += CryptorHeader.SENTINEL.length; + header[pos] = VERSION; + pos++; + header.setAll(pos, _identifier.codeUnits); + pos+=CryptorHeader.IDENTIFIER_LENGTH; + var metadataLength = this.metadataLength; + if (metadataLength < 255){ + header[pos] = metadataLength; + } else { + header.setAll(pos, [255, metadataLength >> 8, metadataLength & 0xff]); + } + return header; + } +} + /// Configuration used in cryptography. class CryptoConfiguration { /// Encryption mode used. diff --git a/pubnub/test/unit/crypto/crypto_test.dart b/pubnub/test/unit/crypto/crypto_test.dart index a3285026..79ba2c6c 100644 --- a/pubnub/test/unit/crypto/crypto_test.dart +++ b/pubnub/test/unit/crypto/crypto_test.dart @@ -22,5 +22,14 @@ void main() { expect(result, equals(plaintext)); }); + + test('cryptoHeader tryParse/data from encrypted text', () async { + var encryptedDataWithHeader = 'PNEDACRH�_�ƿ'; + var headerData = [80, 78, 69, 68, 1, 65, 67, 82, 72, 16]; + var expectedBytes = [...headerData, ...List.filled(16, 0)]; + + CryptorHeaderV1? header = CryptorHeader.tryParse(encryptedDataWithHeader.codeUnits); + expect(header!.data, equals(expectedBytes)); + }); }); }