import struct,zlib import argparse,os from datetime import datetime from dataclasses import dataclass from enum import IntEnum class NpkPartID(IntEnum): NAME_INFO =0x01 # Package information: name, ver, etc. DESCRIPTION =0x02 # Package description DEPENDENCIES =0x03 # Package Dependencies FILE_CONTAINER =0x04 # Files container zlib 1.2.3 INSTALL_SCRIPT =0x07 # Install script UNINSTALL_SCRIPT =0x08 # Uninstall script SIGNATURE =0x09 # Package signature ARCHITECTURE =0x10 # Package architecture (e.g. i386) PKG_CONFLICTS =0x11 # Package conflicts PKG_INFO =0x12 FEATURES =0x13 PKG_FEATURES =0x14 SQUASHFS =0x15 # SquashFS NULL_BLOCK =0X16 GIT_COMMIT =0x17 # Git commit CHANNEL =0x18 # Release type (e.g. stable, testing, etc.) HEADER =0x19 @dataclass class NpkPartItem: id: NpkPartID data: bytes|object class NpkInfo: _format = '<16s4sI8s' def __init__(self,name:str,version:str,build_time=datetime.now(),unknow=b'\x00'*8): self._name = name[:16].encode().ljust(16,b'\x00') self._version = self.encode_version(version) self._build_time = int(build_time.timestamp()) self._unknow = unknow def serialize(self)->bytes: return struct.pack(self._format, self._name,self._version,self._build_time,self._unknow) @staticmethod def unserialize_from(data:bytes)->'NpkInfo': assert len(data) == struct.calcsize(NpkInfo._format),'Invalid data length' _name, _version,_build_time,unknow= struct.unpack_from(NpkInfo._format,data) return NpkInfo(_name.decode(),NpkInfo.decode_version(_version),datetime.fromtimestamp(_build_time),unknow) def __len__ (self)->int: return struct.calcsize(self._format) @property def name(self)->str: return self._name.decode().strip('\x00') @name.setter def name(self,value:str): self._name = value[:16].encode().ljust(16,b'\x00') @staticmethod def decode_version(value:bytes): revision,build,minor,major = struct.unpack_from('4B',value) if build == 97: build = 'alpha' elif build == 98: build = 'beta' elif build == 99: build = 'rc' elif build == 102: if revision & 0x80: build = 'test' revision &= 0x7f else: build = 'final' else: build = 'unknown' return f'{major}.{minor}.{revision}.{build}' @staticmethod def encode_version(value:str): s = value.split('.') if 4 != len(s) and s[3] in [ 'alpha', 'beta', 'rc','final', 'test']: raise ValueError('Invalid version string') major = int(s[0]) minor = int(s[1]) revision = int(s[2]) if s[3] == 'alpha': build = 97 elif s[3] == 'beta': build = 98 elif s[3] == 'rc': build = 99 elif s[3] == 'final': build = 102 revision &= 0x7f else: #'test' build = 102 revision |= 0x80 return struct.pack('4B',revision,build,minor,major) @property def version(self)->str: return self.decode_version(self._version) @version.setter def version(self,value:str = '7.15.1.final'): self._version = self.encode_version(value) @property def build_time(self): return datetime.fromtimestamp(self._build_time) @build_time.setter def build_time(self,value:datetime): self._build_time = int(value.timestamp()) class NpkNameInfo(NpkInfo): _format = '<16s4sI12s' def __init__(self,name:str,version:str,build_time=datetime.now(),_unknow=b'\x00'*12): self._name = name[:16].encode().ljust(16,b'\x00') self._version = self.encode_version(version) self._build_time = int(build_time.timestamp()) self._unknow = _unknow def serialize(self)->bytes: return struct.pack(self._format, self._name,self._version,self._build_time,self._unknow) @staticmethod def unserialize_from(data:bytes)->'NpkNameInfo': assert len(data) == struct.calcsize(NpkNameInfo._format),'Invalid data length' _name, _version,_build_time,_unknow = struct.unpack_from(NpkNameInfo._format,data) return NpkNameInfo(_name.decode(),NpkNameInfo.decode_version(_version),datetime.fromtimestamp(_build_time),_unknow) class NpkFileContainer: _format = 'bytes: compressed_data = b'' compressor = zlib.compressobj() for item in self._items: data = struct.pack(self._format, item.perm,item.type,item.usr_or_grp, item.modify_time,item.revision,item.rc,item.minor,item.major,item.create_time,item.unknow,len(item.data),len(item.name)) data += item.name + item.data compressed_data += compressor.compress(data) return compressed_data + compressor.flush() @staticmethod def unserialize_from(data:bytes): items:list['NpkFileContainer.NpkFileItem'] = [] decompressed_data = zlib.decompress(data) while len(decompressed_data): offset = struct.calcsize(NpkFileContainer._format) perm,type,usr_or_grp, modify_time,revision,rc,minor,major,create_time,unknow,data_size,name_size= struct.unpack_from(NpkFileContainer._format, decompressed_data) name = decompressed_data[offset:offset+name_size] data = decompressed_data[offset+name_size:offset+name_size+data_size] items.append(NpkFileContainer.NpkFileItem(perm,type,usr_or_grp, modify_time,revision,rc,minor,major,create_time,unknow,name,data)) decompressed_data = decompressed_data[offset+name_size+data_size:] return NpkFileContainer(items) def __len__ (self)->int: return len(self.serialize()) def __getitem__(self,index:int)->'NpkFileContainer.NpkFileItem': return self._items[index] def __iter__(self): for item in self._items: yield item class Package: def __init__(self) -> None: self._parts:list[NpkPartItem] = [] def __iter__(self): for part in self._parts: yield part def __getitem__(self, id:NpkPartID): for part in self._parts: if part.id == id: return part part = NpkPartItem(id,b'') self._parts.append(part) return part class NovaPackage(Package): NPK_MAGIC = 0xbad0f11e def __init__(self,data:bytes=b''): super().__init__() self._packages:list[Package] = [] offset = 0 self._has_pkg = False while offset < len(data): part_id,part_size = struct.unpack_from('bytes: parts = package._parts if package else self._parts for part in parts: data_header = struct.pack(' 0: if build_time: self[NpkPartID.PKG_INFO].data._build_time = int(build_time) for package in self._packages: if len(package[NpkPartID.SIGNATURE].data) != 20+48+64: package[NpkPartID.SIGNATURE].data = b'\0'*(20+48+64) if build_time: package[NpkPartID.NAME_INFO].data._build_time = int(build_time) sha1_digest = self.get_digest(hashlib.new('SHA1'),package) sha256_digest = self.get_digest(hashlib.new('SHA256'),package) kcdsa_signature = mikro_kcdsa_sign(sha256_digest[:20],kcdsa_private_key) eddsa_signature = mikro_eddsa_sign(sha256_digest,eddsa_private_key) package[NpkPartID.SIGNATURE].data = sha1_digest + kcdsa_signature + eddsa_signature else: if len(self[NpkPartID.SIGNATURE].data) != 20+48+64: self[NpkPartID.SIGNATURE].data = b'\0'*(20+48+64) if build_time: self[NpkPartID.NAME_INFO].data._build_time = int(build_time) sha1_digest = self.get_digest(hashlib.new('SHA1')) sha256_digest = self.get_digest(hashlib.new('SHA256')) kcdsa_signature = mikro_kcdsa_sign(sha256_digest[:20],kcdsa_private_key) eddsa_signature = mikro_eddsa_sign(sha256_digest,eddsa_private_key) self[NpkPartID.SIGNATURE].data = sha1_digest + kcdsa_signature + eddsa_signature def verify(self,kcdsa_public_key:bytes,eddsa_public_key:bytes): import hashlib from mikro import mikro_kcdsa_verify,mikro_eddsa_verify if len(self._packages) > 0: for package in self._packages: sha1_digest = self.get_digest(hashlib.new('SHA1'),package) sha256_digest = self.get_digest(hashlib.new('SHA256'),package) signature = package[NpkPartID.SIGNATURE].data if sha1_digest != signature[:20]: return False if not mikro_kcdsa_verify(sha256_digest[:20],signature[20:68],kcdsa_public_key): return False if not mikro_eddsa_verify(sha256_digest,signature[68:132],eddsa_public_key): return False else: sha1_digest = self.get_digest(hashlib.new('SHA1')) sha256_digest = self.get_digest(hashlib.new('SHA256')) signature = self[NpkPartID.SIGNATURE].data if sha1_digest != signature[:20]: return False if not mikro_kcdsa_verify(sha256_digest[:20],signature[20:68],kcdsa_public_key): return False if not mikro_eddsa_verify(sha256_digest,signature[68:132],eddsa_public_key): return False return True def save(self,file): size = 0 for part in self._parts: size += 6 + len(part.data) for package in self._packages: for part in package: size += 6 + len(part.data) with open(file,'wb') as f: f.write(struct.pack('