• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2023-2023 Huawei Device Co., Ltd.
3  * Licensed under the Apache License, Version 2.0 (the "License");
4  * you may not use this file except in compliance with the License.
5  * You may obtain a copy of the License at
6  *
7  *     http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  */
15 
16 package com.ohos.hapsigntool.codesigning.sign;
17 
18 import com.ohos.hapsigntool.codesigning.datastructure.CodeSignBlock;
19 import com.ohos.hapsigntool.codesigning.datastructure.ElfSignBlock;
20 import com.ohos.hapsigntool.codesigning.datastructure.Extension;
21 import com.ohos.hapsigntool.codesigning.datastructure.FsVerityInfoSegment;
22 import com.ohos.hapsigntool.codesigning.datastructure.MerkleTreeExtension;
23 import com.ohos.hapsigntool.codesigning.datastructure.SignInfo;
24 import com.ohos.hapsigntool.codesigning.exception.CodeSignException;
25 import com.ohos.hapsigntool.codesigning.exception.FsVerityDigestException;
26 import com.ohos.hapsigntool.codesigning.fsverity.FsVerityDescriptor;
27 import com.ohos.hapsigntool.codesigning.fsverity.FsVerityDescriptorWithSign;
28 import com.ohos.hapsigntool.codesigning.fsverity.FsVerityGenerator;
29 import com.ohos.hapsigntool.codesigning.utils.HapUtils;
30 import com.ohos.hapsigntool.entity.Pair;
31 import com.ohos.hapsigntool.error.HapFormatException;
32 import com.ohos.hapsigntool.error.ProfileException;
33 import com.ohos.hapsigntool.hap.config.SignerConfig;
34 import com.ohos.hapsigntool.signer.LocalSigner;
35 import com.ohos.hapsigntool.utils.FileUtils;
36 import com.ohos.hapsigntool.utils.StringUtils;
37 import com.ohos.hapsigntool.zip.Zip;
38 import com.ohos.hapsigntool.zip.ZipEntry;
39 import com.ohos.hapsigntool.zip.ZipEntryHeader;
40 
41 import org.apache.logging.log4j.LogManager;
42 import org.apache.logging.log4j.Logger;
43 
44 import java.io.File;
45 import java.io.FileInputStream;
46 import java.io.IOException;
47 import java.io.InputStream;
48 import java.util.ArrayList;
49 import java.util.Enumeration;
50 import java.util.HashMap;
51 import java.util.List;
52 import java.util.Locale;
53 import java.util.Map;
54 import java.util.jar.JarEntry;
55 import java.util.jar.JarFile;
56 import java.util.stream.Collectors;
57 import java.util.zip.ZipInputStream;
58 
59 /**
60  * core functions of code signing
61  *
62  * @since 2023/06/05
63  */
64 public class CodeSigning {
65     /**
66      * Only hap and hsp, hqf bundle supports code signing
67      */
68     public static final String[] SUPPORT_FILE_FORM = {"hap", "hsp", "hqf"};
69 
70     /**
71      * Only elf file supports bin code signing
72      */
73     public static final String SUPPORT_BIN_FILE_FORM = "elf";
74 
75     /**
76      * Defined entry name of hap file
77      */
78     public static final String HAP_SIGNATURE_ENTRY_NAME = "Hap";
79 
80     private static final Logger LOGGER = LogManager.getLogger(CodeSigning.class);
81 
82     private static final String NATIVE_LIB_AN_SUFFIX = ".an";
83 
84     private static final String NATIVE_LIB_SO_SUFFIX = ".so";
85 
86     private final SignerConfig signConfig;
87 
88     private CodeSignBlock codeSignBlock;
89 
90     /**
91      * provide code sign functions to sign a hap
92      *
93      * @param signConfig configuration of sign
94      */
CodeSigning(SignerConfig signConfig)95     public CodeSigning(SignerConfig signConfig) {
96         this.signConfig = signConfig;
97     }
98 
99     /**
100      * Sign the given elf file, and pack all signature into output file
101      *
102      * @param input  file to sign
103      * @param offset position of codesign block based on start of the file
104      * @param inForm file's format
105      * @param profileContent profile of the elf
106      * @return byte array of code sign block
107      * @throws CodeSignException        code signing exception
108      * @throws IOException              io error
109      * @throws FsVerityDigestException  computing FsVerity digest error
110      * @throws ProfileException         profile of elf is invalid
111      */
getElfCodeSignBlock(File input, long offset, String inForm, String profileContent)112     public byte[] getElfCodeSignBlock(File input, long offset, String inForm, String profileContent)
113         throws CodeSignException, FsVerityDigestException, IOException, ProfileException {
114         LOGGER.info("Start to sign code.");
115         if (!SUPPORT_BIN_FILE_FORM.equalsIgnoreCase(inForm)) {
116             throw new CodeSignException("file's format is unsupported");
117         }
118         long fileSize = input.length();
119         int paddingSize = ElfSignBlock.computeMerkleTreePaddingLength(offset);
120         long fsvTreeOffset = offset + Integer.BYTES * 2 + paddingSize;
121         try (FileInputStream inputStream = new FileInputStream(input)) {
122             FsVerityGenerator fsVerityGenerator = new FsVerityGenerator();
123             fsVerityGenerator.generateFsVerityDigest(inputStream, fileSize, fsvTreeOffset);
124             byte[] fsVerityDigest = fsVerityGenerator.getFsVerityDigest();
125             // ownerID should be DEBUG_LIB_ID while signing ELF
126             String ownerID = (profileContent == null)
127                 ? HapUtils.HAP_DEBUG_OWNER_ID
128                 : HapUtils.getAppIdentifier(profileContent);
129             byte[] signature = generateSignature(fsVerityDigest, ownerID);
130             // add fs-verify info
131             FsVerityDescriptor.Builder fsdbuilder = new FsVerityDescriptor.Builder().setFileSize(fileSize)
132                 .setHashAlgorithm(FsVerityGenerator.getFsVerityHashAlgorithm())
133                 .setLog2BlockSize(FsVerityGenerator.getLog2BlockSize())
134                 .setSaltSize((byte) fsVerityGenerator.getSaltSize())
135                 .setSignSize(signature.length)
136                 .setFileSize(fileSize)
137                 .setSalt(fsVerityGenerator.getSalt())
138                 .setRawRootHash(fsVerityGenerator.getRootHash())
139                 .setFlags(FsVerityDescriptor.FLAG_STORE_MERKLE_TREE_OFFSET)
140                 .setMerkleTreeOffset(fsvTreeOffset)
141                 .setCsVersion(FsVerityDescriptor.CODE_SIGN_VERSION);
142             FsVerityDescriptorWithSign fsVerityDescriptorWithSign = new FsVerityDescriptorWithSign(fsdbuilder.build(),
143                 signature);
144             byte[] treeBytes = fsVerityGenerator.getTreeBytes();
145             ElfSignBlock signBlock = new ElfSignBlock(paddingSize, treeBytes, fsVerityDescriptorWithSign);
146             LOGGER.info("Sign elf successfully.");
147             return signBlock.toByteArray();
148         }
149     }
150 
151     /**
152      * Sign the given hap file, and pack all signature into output file
153      *
154      * @param input  file to sign
155      * @param offset position of codesign block based on start of the file
156      * @param inForm file's format
157      * @param profileContent profile of the hap
158      * @param zip zip
159      * @return byte array of code sign block
160      * @throws CodeSignException        code signing exception
161      * @throws IOException              io error
162      * @throws HapFormatException       hap format invalid
163      * @throws FsVerityDigestException  computing FsVerity digest error
164      * @throws ProfileException         profile of the hap error
165      */
getCodeSignBlock(File input, long offset, String inForm, String profileContent, Zip zip)166     public byte[] getCodeSignBlock(File input, long offset, String inForm, String profileContent, Zip zip)
167         throws CodeSignException, IOException, HapFormatException, FsVerityDigestException, ProfileException {
168         LOGGER.info("Start to sign code.");
169         if (!StringUtils.containsIgnoreCase(SUPPORT_FILE_FORM, inForm)) {
170             throw new CodeSignException("file's format is unsupported");
171         }
172         long dataSize = computeDataSize(zip);
173         // generate CodeSignBlock
174         this.codeSignBlock = new CodeSignBlock();
175         // compute merkle tree offset, replace with computeMerkleTreeOffset if fs-verity descriptor supports
176         long fsvTreeOffset = this.codeSignBlock.computeMerkleTreeOffset(offset);
177         // update fs-verity segment
178         FsVerityInfoSegment fsVerityInfoSegment = new FsVerityInfoSegment(FsVerityDescriptor.VERSION,
179             FsVerityGenerator.getFsVerityHashAlgorithm(), FsVerityGenerator.getLog2BlockSize());
180         this.codeSignBlock.setFsVerityInfoSegment(fsVerityInfoSegment);
181 
182         LOGGER.debug("Sign hap.");
183         String ownerID = HapUtils.getAppIdentifier(profileContent);
184 
185         try (FileInputStream inputStream = new FileInputStream(input)) {
186             Pair<SignInfo, byte[]> hapSignInfoAndMerkleTreeBytesPair = signFile(inputStream, dataSize, true,
187                 fsvTreeOffset, ownerID);
188             // update hap segment in CodeSignBlock
189             this.codeSignBlock.getHapInfoSegment().setSignInfo(hapSignInfoAndMerkleTreeBytesPair.getFirst());
190             // Insert merkle tree bytes into code sign block
191             this.codeSignBlock.addOneMerkleTree(HAP_SIGNATURE_ENTRY_NAME,
192                 hapSignInfoAndMerkleTreeBytesPair.getSecond());
193         }
194         // update native lib info segment in CodeSignBlock
195         List<Pair<String, SignInfo>> nativeLibInfoList = new ArrayList<>();
196         nativeLibInfoList.addAll(signNativeLibs(input, ownerID));
197         nativeLibInfoList.addAll(signNativeHnps(input, profileContent, ownerID));
198         // update SoInfoSegment in CodeSignBlock
199         this.codeSignBlock.getSoInfoSegment().setSoInfoList(nativeLibInfoList);
200 
201         // last update codeSignBlock before generating its byte array representation
202         updateCodeSignBlock(this.codeSignBlock);
203 
204         // complete code sign block byte array here
205         byte[] generated = this.codeSignBlock.generateCodeSignBlockByte(fsvTreeOffset);
206         LOGGER.info("Sign successfully.");
207         return generated;
208     }
209 
computeDataSize(Zip zip)210     private long computeDataSize(Zip zip) throws HapFormatException {
211         long dataSize = 0L;
212         for (ZipEntry entry : zip.getZipEntries()) {
213             ZipEntryHeader zipEntryHeader = entry.getZipEntryData().getZipEntryHeader();
214             if (FileUtils.isRunnableFile(zipEntryHeader.getFileName())
215                 && zipEntryHeader.getMethod() == Zip.FILE_UNCOMPRESS_METHOD_FLAG) {
216                 continue;
217             }
218             // if the first file is not uncompressed abc or so, set dataSize to zero
219             if (entry.getCentralDirectory().getOffset() == 0) {
220                 break;
221             }
222             // the first entry which is not abc/so/an is found, return its data offset
223             dataSize = entry.getCentralDirectory().getOffset() + ZipEntryHeader.HEADER_LENGTH
224                     + zipEntryHeader.getFileNameLength() + zipEntryHeader.getExtraLength();
225             break;
226         }
227         if ((dataSize % CodeSignBlock.PAGE_SIZE_4K) != 0) {
228             throw new HapFormatException(
229                 String.format(Locale.ROOT, "Invalid dataSize(%d), not a multiple of 4096", dataSize));
230         }
231         return dataSize;
232     }
233 
signNativeLibs(File input, String ownerID)234     private List<Pair<String, SignInfo>> signNativeLibs(File input, String ownerID)
235         throws IOException, FsVerityDigestException, CodeSignException {
236         // sign native files
237         try (JarFile inputJar = new JarFile(input, false)) {
238             List<String> entryNames = getNativeEntriesFromHap(inputJar);
239             if (entryNames.isEmpty()) {
240                 LOGGER.info("No native libs.");
241                 return new ArrayList<>();
242             }
243             return signFilesFromJar(entryNames, inputJar, ownerID);
244         }
245     }
246 
signNativeHnps(File input, String profileContent, String ownerID)247     private List<Pair<String, SignInfo>> signNativeHnps(File input, String profileContent, String ownerID)
248         throws IOException, CodeSignException, ProfileException {
249         List<Pair<String, SignInfo>> nativeLibInfoList = new ArrayList<>();
250         try (JarFile inputJar = new JarFile(input, false)) {
251             Map<String, String> hnpTypeMap = HapUtils.getHnpsFromJson(inputJar);
252             // get hnp entry
253             for (Enumeration<JarEntry> e = inputJar.entries(); e.hasMoreElements(); ) {
254                 JarEntry entry = e.nextElement();
255                 String entryName = entry.getName();
256                 if (entry.isDirectory() || !entryName.startsWith("hnp/") || !entryName.toLowerCase(Locale.ROOT)
257                     .endsWith(".hnp")) {
258                     continue;
259                 }
260                 String hnpFileName = HapUtils.parseHnpPath(entryName);
261                 if (!hnpTypeMap.containsKey(hnpFileName)) {
262                     throw new CodeSignException("hnp should be described in module.json");
263                 }
264                 LOGGER.debug("Sign hnp name = {}", entryName);
265                 String type = hnpTypeMap.get(hnpFileName);
266                 String hnpOwnerId = ownerID;
267                 if ("public".equals(type)) {
268                     hnpOwnerId = HapUtils.getPublicHnpOwnerId(profileContent);
269                 }
270                 nativeLibInfoList.addAll(signHnpLibs(inputJar, entry, hnpOwnerId));
271             }
272         }
273         return nativeLibInfoList;
274     }
275 
signHnpLibs(JarFile inputJar, JarEntry hnpEntry, String ownerID)276     private List<Pair<String, SignInfo>> signHnpLibs(JarFile inputJar, JarEntry hnpEntry, String ownerID)
277         throws IOException, CodeSignException {
278         Map<String, Long> elfEntries = getElfEntriesFromHnp(inputJar, hnpEntry);
279         List<Pair<String, SignInfo>> nativeLibInfoList = elfEntries.entrySet().stream().parallel().map(elf -> {
280             String hnpElfPath = hnpEntry.getName() + "!/" + elf.getKey();
281             try (InputStream inputStream = inputJar.getInputStream(hnpEntry);
282                 ZipInputStream hnpInputStream = new ZipInputStream(inputStream)) {
283                 return signHnpElf(hnpInputStream, hnpElfPath, ownerID, elf);
284             } catch (IOException | FsVerityDigestException | CodeSignException e) {
285                 LOGGER.error("Sign hnp lib error, entry name = {}, msg : {}", hnpElfPath, e.getMessage());
286             }
287             return null;
288         }).collect(Collectors.toList());
289         if (nativeLibInfoList.contains(null)) {
290             throw new CodeSignException("Sign hnp lib error");
291         }
292         return nativeLibInfoList;
293     }
294 
295     /**
296      * sign hnp's elf
297      *
298      * @param hnpInputStream hnp entry input stream
299      * @param hnpElfPath hnp's elf path
300      * @param ownerID ownerId
301      * @param elf elf entry
302      * @return native lib info
303      * @throws IOException             io error
304      * @throws FsVerityDigestException computing FsVerity digest error
305      * @throws CodeSignException       code signing exception
306      */
signHnpElf(ZipInputStream hnpInputStream, String hnpElfPath, String ownerID, Map.Entry<String, Long> elf)307     public Pair<String, SignInfo> signHnpElf(ZipInputStream hnpInputStream, String hnpElfPath, String ownerID,
308         Map.Entry<String, Long> elf) throws IOException, FsVerityDigestException, CodeSignException {
309         java.util.zip.ZipEntry libEntry = null;
310         while ((libEntry = hnpInputStream.getNextEntry()) != null) {
311             if (elf.getKey().equals(libEntry.getName())) {
312                 long fileSize = elf.getValue();
313                 // We don't store merkle tree in code signing of native libs
314                 // Therefore, the second value of pair returned is ignored
315                 Pair<SignInfo, byte[]> pairSignInfoAndMerkleTreeBytes = signFile(hnpInputStream, fileSize, false, 0,
316                     ownerID);
317                 return (Pair.create(hnpElfPath, pairSignInfoAndMerkleTreeBytes.getFirst()));
318             }
319         }
320         return null;
321     }
322 
getElfEntriesFromHnp(JarFile inputJar, JarEntry hnpEntry)323     private Map<String, Long> getElfEntriesFromHnp(JarFile inputJar, JarEntry hnpEntry) throws IOException {
324         Map<String, Long> elfEntries = new HashMap<>();
325         try (InputStream inputStream = inputJar.getInputStream(hnpEntry);
326             ZipInputStream hnpInputStream = new ZipInputStream(inputStream)) {
327             java.util.zip.ZipEntry libEntry = null;
328             while ((libEntry = hnpInputStream.getNextEntry()) != null) {
329                 byte[] bytes = new byte[4];
330                 hnpInputStream.read(bytes, 0, 4);
331                 if (!isElfFile(bytes)) {
332                     hnpInputStream.closeEntry();
333                     continue;
334                 }
335                 // read input stream end to get entry size, can be adjusted based on performance testing
336                 byte[] tmp = new byte[4096];
337                 int readLen;
338                 do {
339                     readLen = hnpInputStream.read(tmp, 0, 4096);
340                 } while (readLen > 0);
341                 elfEntries.put(libEntry.getName(), libEntry.getSize());
342                 hnpInputStream.closeEntry();
343             }
344         }
345         return elfEntries;
346     }
347 
348     /**
349      * Get entry name of all native files in hap
350      *
351      * @param hap the given hap
352      * @return list of entry name
353      */
getNativeEntriesFromHap(JarFile hap)354     private List<String> getNativeEntriesFromHap(JarFile hap) {
355         List<String> result = new ArrayList<>();
356         for (Enumeration<JarEntry> e = hap.entries(); e.hasMoreElements();) {
357             JarEntry entry = e.nextElement();
358             if (!entry.isDirectory()) {
359                 if (!isNativeFile(entry.getName())) {
360                     continue;
361                 }
362                 result.add(entry.getName());
363             }
364         }
365         return result;
366     }
367 
368     /**
369      * Check whether the entry is a native file
370      *
371      * @param entryName the name of entry
372      * @return true if it is a native file, and false otherwise
373      */
isNativeFile(String entryName)374     private boolean isNativeFile(String entryName) {
375         if (StringUtils.isEmpty(entryName)) {
376             return false;
377         }
378         if (entryName.endsWith(NATIVE_LIB_AN_SUFFIX)) {
379             return true;
380         }
381         if (entryName.startsWith(FileUtils.LIBS_PATH_PREFIX)) {
382             return true;
383         }
384         return false;
385     }
386 
isElfFile(byte[] bytes)387     private boolean isElfFile(byte[] bytes) {
388         if (bytes == null || bytes.length != 4) {
389             return false;
390         }
391         return bytes[0] == 0x7F && bytes[1] == 0x45 && bytes[2] == 0x4C && bytes[3] == 0x46;
392     }
393 
394     /**
395      * Sign specific entries in a hap
396      *
397      * @param entryNames list of entries which need to be signed
398      * @param hap        input hap
399      * @param ownerID    app-id in signature to identify
400      * @return sign info and merkle tree of each file
401      * @throws CodeSignException       sign error
402      */
signFilesFromJar(List<String> entryNames, JarFile hap, String ownerID)403     private List<Pair<String, SignInfo>> signFilesFromJar(List<String> entryNames, JarFile hap, String ownerID)
404         throws CodeSignException {
405         List<Pair<String, SignInfo>> nativeLibInfoList = entryNames.stream().parallel().map(name -> {
406             LOGGER.debug("Sign entry name = {}", name);
407             JarEntry inEntry = hap.getJarEntry(name);
408             try (InputStream inputStream = hap.getInputStream(inEntry)) {
409                 long fileSize = inEntry.getSize();
410                 // We don't store merkle tree in code signing of native libs
411                 // Therefore, the second value of pair returned is ignored
412                 Pair<SignInfo, byte[]> pairSignInfoAndMerkleTreeBytes = signFile(inputStream, fileSize, false, 0,
413                     ownerID);
414                 return Pair.create(name, pairSignInfoAndMerkleTreeBytes.getFirst());
415             } catch (FsVerityDigestException | CodeSignException | IOException e) {
416                 LOGGER.error("Sign lib error, entry name = {}, msg : {}", name, e.getMessage());
417             }
418             return null;
419         }).collect(Collectors.toList());
420         if (nativeLibInfoList.contains(null)) {
421             throw new CodeSignException("Sign lib error");
422         }
423         return nativeLibInfoList;
424     }
425 
426     /**
427      * Sign a file from input stream
428      *
429      * @param inputStream   input stream of a file
430      * @param fileSize      size of the file
431      * @param storeTree     whether to store merkle tree in signed info
432      * @param fsvTreeOffset merkle tree raw bytes offset based on the start of file
433      * @param ownerID       app-id in signature to identify
434      * @return pair of signature and tree
435      * @throws FsVerityDigestException computing FsVerity Digest error
436      * @throws CodeSignException       signing error
437      */
signFile(InputStream inputStream, long fileSize, boolean storeTree, long fsvTreeOffset, String ownerID)438     public Pair<SignInfo, byte[]> signFile(InputStream inputStream, long fileSize, boolean storeTree,
439         long fsvTreeOffset, String ownerID) throws FsVerityDigestException, CodeSignException {
440         FsVerityGenerator fsVerityGenerator = new FsVerityGenerator();
441         fsVerityGenerator.generateFsVerityDigest(inputStream, fileSize, fsvTreeOffset);
442         byte[] fsVerityDigest = fsVerityGenerator.getFsVerityDigest();
443         byte[] signature = generateSignature(fsVerityDigest, ownerID);
444         int flags = 0;
445         if (storeTree) {
446             flags = SignInfo.FLAG_MERKLE_TREE_INCLUDED;
447         }
448         SignInfo signInfo = new SignInfo(fsVerityGenerator.getSaltSize(), flags, fileSize, fsVerityGenerator.getSalt(),
449             signature);
450         // if store merkle tree in sign info
451         if (storeTree) {
452             int merkleTreeSize = fsVerityGenerator.getTreeBytes() == null ? 0 : fsVerityGenerator.getTreeBytes().length;
453             Extension merkleTreeExtension = new MerkleTreeExtension(merkleTreeSize, fsvTreeOffset,
454                 fsVerityGenerator.getRootHash());
455             signInfo.addExtension(merkleTreeExtension);
456         }
457         return Pair.create(signInfo, fsVerityGenerator.getTreeBytes());
458     }
459 
generateSignature(byte[] signedData, String ownerID)460     private byte[] generateSignature(byte[] signedData, String ownerID) throws CodeSignException {
461         SignerConfig copiedConfig = signConfig;
462         // signConfig is created by SignerFactory
463         if ((copiedConfig.getSigner() instanceof LocalSigner)) {
464             if (copiedConfig.getCertificates().isEmpty()) {
465                 throw new CodeSignException("No certificates configured for sign");
466             }
467             BcSignedDataGenerator bcSignedDataGenerator = new BcSignedDataGenerator();
468             bcSignedDataGenerator.setOwnerID(ownerID);
469             return bcSignedDataGenerator.generateSignedData(signedData, copiedConfig);
470         } else {
471             copiedConfig = signConfig.copy();
472             BcSignedDataGenerator bcSignedDataGenerator = new BcSignedDataGenerator();
473             bcSignedDataGenerator.setOwnerID(ownerID);
474             return bcSignedDataGenerator.generateSignedData(signedData, copiedConfig);
475         }
476     }
477 
478     /**
479      * At here, segment header, fsverity info/hap/so info segment, merkle tree
480      * segment should all be generated.
481      * code sign block size, segment number, offset is not updated.
482      * Try to update whatever could be updated here.
483      *
484      * @param codeSignBlock CodeSignBlock
485      */
updateCodeSignBlock(CodeSignBlock codeSignBlock)486     private void updateCodeSignBlock(CodeSignBlock codeSignBlock) {
487         // construct segment header list
488         codeSignBlock.setSegmentHeaders();
489         // Compute and set segment number
490         codeSignBlock.setSegmentNum();
491         // update code sign block header flag
492         codeSignBlock.setCodeSignBlockFlag();
493         // compute segment offset
494         codeSignBlock.computeSegmentOffset();
495     }
496 
497 }
498