1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.server.locksettings;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertFalse;
21 import static org.junit.Assert.assertNotNull;
22 import static org.junit.Assert.assertSame;
23 import static org.junit.Assert.assertTrue;
24 import static org.junit.Assert.fail;
25 import static org.mockito.ArgumentMatchers.eq;
26 import static org.mockito.Mockito.mock;
27 import static org.mockito.Mockito.when;
28 
29 import android.app.KeyguardManager;
30 import android.app.NotificationManager;
31 import android.app.admin.DevicePolicyManager;
32 import android.app.trust.TrustManager;
33 import android.content.pm.PackageManager;
34 import android.content.pm.UserInfo;
35 import android.database.sqlite.SQLiteDatabase;
36 import android.hardware.face.FaceManager;
37 import android.hardware.fingerprint.FingerprintManager;
38 import android.os.FileUtils;
39 import android.os.SystemClock;
40 import android.os.UserManager;
41 import android.os.storage.StorageManager;
42 import android.platform.test.annotations.Presubmit;
43 import android.util.Log;
44 import android.util.Log.TerribleFailure;
45 import android.util.Log.TerribleFailureHandler;
46 
47 import androidx.test.InstrumentationRegistry;
48 import androidx.test.filters.SmallTest;
49 import androidx.test.runner.AndroidJUnit4;
50 
51 import com.android.internal.widget.LockPatternUtils;
52 import com.android.server.PersistentDataBlockManagerInternal;
53 import com.android.server.locksettings.LockSettingsStorage.CredentialHash;
54 import com.android.server.locksettings.LockSettingsStorage.PersistentData;
55 
56 import org.junit.After;
57 import org.junit.Before;
58 import org.junit.Test;
59 import org.junit.runner.RunWith;
60 
61 import java.io.File;
62 import java.util.ArrayList;
63 import java.util.Arrays;
64 import java.util.List;
65 import java.util.concurrent.CountDownLatch;
66 
67 /**
68  * atest FrameworksServicesTests:LockSettingsStorageTests
69  */
70 @SmallTest
71 @Presubmit
72 @RunWith(AndroidJUnit4.class)
73 public class LockSettingsStorageTests {
74     private static final int SOME_USER_ID = 1034;
75     private final byte[] PASSWORD_0 = "thepassword0".getBytes();
76     private final byte[] PASSWORD_1 = "password1".getBytes();
77     private final byte[] PATTERN_0 = "123654".getBytes();
78     private final byte[] PATTERN_1 = "147852369".getBytes();
79 
80     public static final byte[] PAYLOAD = new byte[] {1, 2, -1, -2, 33};
81 
82     LockSettingsStorageTestable mStorage;
83     File mStorageDir;
84 
85     private File mDb;
86 
87     @Before
setUp()88     public void setUp() throws Exception {
89         mStorageDir = new File(InstrumentationRegistry.getContext().getFilesDir(), "locksettings");
90         mDb = InstrumentationRegistry.getContext().getDatabasePath("locksettings.db");
91 
92         assertTrue(mStorageDir.exists() || mStorageDir.mkdirs());
93         assertTrue(FileUtils.deleteContents(mStorageDir));
94         assertTrue(!mDb.exists() || mDb.delete());
95 
96         final UserManager mockUserManager = mock(UserManager.class);
97         // User 2 is a profile of user 1.
98         when(mockUserManager.getProfileParent(eq(2))).thenReturn(new UserInfo(1, "name", 0));
99         // User 3 is a profile of user 0.
100         when(mockUserManager.getProfileParent(eq(3))).thenReturn(new UserInfo(0, "name", 0));
101 
102         MockLockSettingsContext context = new MockLockSettingsContext(
103                 InstrumentationRegistry.getContext(), mockUserManager,
104                 mock(NotificationManager.class), mock(DevicePolicyManager.class),
105                 mock(StorageManager.class), mock(TrustManager.class), mock(KeyguardManager.class),
106                 mock(FingerprintManager.class), mock(FaceManager.class),
107                 mock(PackageManager.class));
108         mStorage = new LockSettingsStorageTestable(context,
109                 new File(InstrumentationRegistry.getContext().getFilesDir(), "locksettings"));
110         mStorage.setDatabaseOnCreateCallback(new LockSettingsStorage.Callback() {
111                     @Override
112                     public void initialize(SQLiteDatabase db) {
113                         mStorage.writeKeyValue(db, "initializedKey", "initialValue", 0);
114                     }
115                 });
116     }
117 
118     @After
tearDown()119     public void tearDown() throws Exception {
120         mStorage.closeDatabase();
121     }
122 
123     @Test
testKeyValue_InitializeWorked()124     public void testKeyValue_InitializeWorked() {
125         assertEquals("initialValue", mStorage.readKeyValue("initializedKey", "default", 0));
126         mStorage.clearCache();
127         assertEquals("initialValue", mStorage.readKeyValue("initializedKey", "default", 0));
128     }
129 
130     @Test
testKeyValue_WriteThenRead()131     public void testKeyValue_WriteThenRead() {
132         mStorage.writeKeyValue("key", "value", 0);
133         assertEquals("value", mStorage.readKeyValue("key", "default", 0));
134         mStorage.clearCache();
135         assertEquals("value", mStorage.readKeyValue("key", "default", 0));
136     }
137 
138     @Test
testKeyValue_DefaultValue()139     public void testKeyValue_DefaultValue() {
140         assertEquals("default", mStorage.readKeyValue("unititialized key", "default", 0));
141         assertEquals("default2", mStorage.readKeyValue("unititialized key", "default2", 0));
142     }
143 
144     @Test
testKeyValue_Concurrency()145     public void testKeyValue_Concurrency() {
146         final CountDownLatch latch = new CountDownLatch(1);
147         List<Thread> threads = new ArrayList<>();
148         for (int i = 0; i < 100; i++) {
149             final int threadId = i;
150             threads.add(new Thread("testKeyValue_Concurrency_" + i) {
151                 @Override
152                 public void run() {
153                     try {
154                         latch.await();
155                     } catch (InterruptedException e) {
156                         return;
157                     }
158                     mStorage.writeKeyValue("key", "1 from thread " + threadId, 0);
159                     mStorage.readKeyValue("key", "default", 0);
160                     mStorage.writeKeyValue("key", "2 from thread " + threadId, 0);
161                     mStorage.readKeyValue("key", "default", 0);
162                     mStorage.writeKeyValue("key", "3 from thread " + threadId, 0);
163                     mStorage.readKeyValue("key", "default", 0);
164                     mStorage.writeKeyValue("key", "4 from thread " + threadId, 0);
165                     mStorage.readKeyValue("key", "default", 0);
166                     mStorage.writeKeyValue("key", "5 from thread " + threadId, 0);
167                     mStorage.readKeyValue("key", "default", 0);
168                 }
169             });
170             threads.get(i).start();
171         }
172         mStorage.writeKeyValue("key", "initalValue", 0);
173         latch.countDown();
174         joinAll(threads, 10000);
175         assertEquals('5', mStorage.readKeyValue("key", "default", 0).charAt(0));
176         mStorage.clearCache();
177         assertEquals('5', mStorage.readKeyValue("key", "default", 0).charAt(0));
178     }
179 
180     @Test
testKeyValue_CacheStarvedWriter()181     public void testKeyValue_CacheStarvedWriter() {
182         final CountDownLatch latch = new CountDownLatch(1);
183         List<Thread> threads = new ArrayList<>();
184         for (int i = 0; i < 100; i++) {
185             final int threadId = i;
186             threads.add(new Thread() {
187                 @Override
188                 public void run() {
189                     try {
190                         latch.await();
191                     } catch (InterruptedException e) {
192                         return;
193                     }
194                     if (threadId == 50) {
195                         mStorage.writeKeyValue("starvedWriterKey", "value", 0);
196                     } else {
197                         mStorage.readKeyValue("starvedWriterKey", "default", 0);
198                     }
199                 }
200             });
201             threads.get(i).start();
202         }
203         latch.countDown();
204         for (int i = 0; i < threads.size(); i++) {
205             try {
206                 threads.get(i).join();
207             } catch (InterruptedException e) {
208             }
209         }
210         String cached = mStorage.readKeyValue("key", "default", 0);
211         mStorage.clearCache();
212         String storage = mStorage.readKeyValue("key", "default", 0);
213         assertEquals("Cached value didn't match stored value", storage, cached);
214     }
215 
216     @Test
testRemoveUser()217     public void testRemoveUser() {
218         mStorage.writeKeyValue("key", "value", 0);
219         writePasswordBytes(PASSWORD_0, 0);
220         writePatternBytes(PATTERN_0, 0);
221 
222         mStorage.writeKeyValue("key", "value", 1);
223         writePasswordBytes(PASSWORD_1, 1);
224         writePatternBytes(PATTERN_1, 1);
225 
226         mStorage.removeUser(0);
227 
228         assertEquals("value", mStorage.readKeyValue("key", "default", 1));
229         assertEquals("default", mStorage.readKeyValue("key", "default", 0));
230         assertEquals(LockPatternUtils.CREDENTIAL_TYPE_NONE, mStorage.readCredentialHash(0).type);
231         assertPatternBytes(PATTERN_1, 1);
232     }
233 
234     @Test
testCredential_Default()235     public void testCredential_Default() {
236         assertEquals(mStorage.readCredentialHash(0).type, LockPatternUtils.CREDENTIAL_TYPE_NONE);
237     }
238 
239     @Test
testPassword_Write()240     public void testPassword_Write() {
241         writePasswordBytes(PASSWORD_0, 0);
242 
243         assertPasswordBytes(PASSWORD_0, 0);
244         mStorage.clearCache();
245         assertPasswordBytes(PASSWORD_0, 0);
246     }
247 
248     @Test
testPassword_WriteProfileWritesParent()249     public void testPassword_WriteProfileWritesParent() {
250         writePasswordBytes(PASSWORD_0, 1);
251         writePasswordBytes(PASSWORD_1, 2);
252 
253         assertPasswordBytes(PASSWORD_0, 1);
254         assertPasswordBytes(PASSWORD_1, 2);
255         mStorage.clearCache();
256         assertPasswordBytes(PASSWORD_0, 1);
257         assertPasswordBytes(PASSWORD_1, 2);
258     }
259 
260     @Test
testLockType_WriteProfileWritesParent()261     public void testLockType_WriteProfileWritesParent() {
262         writePasswordBytes(PASSWORD_0, 10);
263         writePatternBytes(PATTERN_0, 20);
264 
265         assertEquals(LockPatternUtils.CREDENTIAL_TYPE_PASSWORD_OR_PIN,
266                 mStorage.readCredentialHash(10).type);
267         assertEquals(LockPatternUtils.CREDENTIAL_TYPE_PATTERN,
268                 mStorage.readCredentialHash(20).type);
269         mStorage.clearCache();
270         assertEquals(LockPatternUtils.CREDENTIAL_TYPE_PASSWORD_OR_PIN,
271                 mStorage.readCredentialHash(10).type);
272         assertEquals(LockPatternUtils.CREDENTIAL_TYPE_PATTERN,
273                 mStorage.readCredentialHash(20).type);
274     }
275 
276     @Test
testPassword_WriteParentWritesProfile()277     public void testPassword_WriteParentWritesProfile() {
278         writePasswordBytes(PASSWORD_0, 2);
279         writePasswordBytes(PASSWORD_1, 1);
280 
281         assertPasswordBytes(PASSWORD_1, 1);
282         assertPasswordBytes(PASSWORD_0, 2);
283         mStorage.clearCache();
284         assertPasswordBytes(PASSWORD_1, 1);
285         assertPasswordBytes(PASSWORD_0, 2);
286     }
287 
288     @Test
testProfileLock_ReadWriteChildProfileLock()289     public void testProfileLock_ReadWriteChildProfileLock() {
290         assertFalse(mStorage.hasChildProfileLock(20));
291         mStorage.writeChildProfileLock(20, PASSWORD_0);
292         assertArrayEquals(PASSWORD_0, mStorage.readChildProfileLock(20));
293         assertTrue(mStorage.hasChildProfileLock(20));
294         mStorage.clearCache();
295         assertArrayEquals(PASSWORD_0, mStorage.readChildProfileLock(20));
296         assertTrue(mStorage.hasChildProfileLock(20));
297     }
298 
299     @Test
testPattern_Write()300     public void testPattern_Write() {
301         writePatternBytes(PATTERN_0, 0);
302 
303         assertPatternBytes(PATTERN_0, 0);
304         mStorage.clearCache();
305         assertPatternBytes(PATTERN_0, 0);
306     }
307 
308     @Test
testPattern_WriteProfileWritesParent()309     public void testPattern_WriteProfileWritesParent() {
310         writePatternBytes(PATTERN_0, 1);
311         writePatternBytes(PATTERN_1, 2);
312 
313         assertPatternBytes(PATTERN_0, 1);
314         assertPatternBytes(PATTERN_1, 2);
315         mStorage.clearCache();
316         assertPatternBytes(PATTERN_0, 1);
317         assertPatternBytes(PATTERN_1, 2);
318     }
319 
320     @Test
testPattern_WriteParentWritesProfile()321     public void testPattern_WriteParentWritesProfile() {
322         writePatternBytes(PATTERN_1, 2);
323         writePatternBytes(PATTERN_0, 1);
324 
325         assertPatternBytes(PATTERN_0, 1);
326         assertPatternBytes(PATTERN_1, 2);
327         mStorage.clearCache();
328         assertPatternBytes(PATTERN_0, 1);
329         assertPatternBytes(PATTERN_1, 2);
330     }
331 
332     @Test
testPrefetch()333     public void testPrefetch() {
334         mStorage.writeKeyValue("key", "toBeFetched", 0);
335         writePatternBytes(PATTERN_0, 0);
336 
337         mStorage.clearCache();
338         mStorage.prefetchUser(0);
339 
340         assertEquals("toBeFetched", mStorage.readKeyValue("key", "default", 0));
341         assertPatternBytes(PATTERN_0, 0);
342     }
343 
344     @Test
testFileLocation_Owner()345     public void testFileLocation_Owner() {
346         LockSettingsStorage storage = new LockSettingsStorage(InstrumentationRegistry.getContext());
347 
348         assertEquals("/data/system/gatekeeper.pattern.key", storage.getLockPatternFilename(0));
349         assertEquals("/data/system/gatekeeper.password.key", storage.getLockPasswordFilename(0));
350     }
351 
352     @Test
testFileLocation_SecondaryUser()353     public void testFileLocation_SecondaryUser() {
354         LockSettingsStorage storage = new LockSettingsStorage(InstrumentationRegistry.getContext());
355 
356         assertEquals("/data/system/users/1/gatekeeper.pattern.key", storage.getLockPatternFilename(1));
357         assertEquals("/data/system/users/1/gatekeeper.password.key", storage.getLockPasswordFilename(1));
358     }
359 
360     @Test
testFileLocation_ProfileToSecondary()361     public void testFileLocation_ProfileToSecondary() {
362         LockSettingsStorage storage = new LockSettingsStorage(InstrumentationRegistry.getContext());
363 
364         assertEquals("/data/system/users/2/gatekeeper.pattern.key", storage.getLockPatternFilename(2));
365         assertEquals("/data/system/users/2/gatekeeper.password.key", storage.getLockPasswordFilename(2));
366     }
367 
368     @Test
testFileLocation_ProfileToOwner()369     public void testFileLocation_ProfileToOwner() {
370         LockSettingsStorage storage = new LockSettingsStorage(InstrumentationRegistry.getContext());
371 
372         assertEquals("/data/system/users/3/gatekeeper.pattern.key", storage.getLockPatternFilename(3));
373         assertEquals("/data/system/users/3/gatekeeper.password.key", storage.getLockPasswordFilename(3));
374     }
375 
376     @Test
testSyntheticPasswordState()377     public void testSyntheticPasswordState() {
378         final byte[] data = {1,2,3,4};
379         mStorage.writeSyntheticPasswordState(10, 1234L, "state", data);
380         assertArrayEquals(data, mStorage.readSyntheticPasswordState(10, 1234L, "state"));
381         assertEquals(null, mStorage.readSyntheticPasswordState(0, 1234L, "state"));
382 
383         mStorage.deleteSyntheticPasswordState(10, 1234L, "state");
384         assertEquals(null, mStorage.readSyntheticPasswordState(10, 1234L, "state"));
385     }
386 
387     @Test
testPersistentDataBlock_unavailable()388     public void testPersistentDataBlock_unavailable() {
389         mStorage.mPersistentDataBlockManager = null;
390 
391         assertSame(PersistentData.NONE, mStorage.readPersistentDataBlock());
392     }
393 
394     @Test
testPersistentDataBlock_empty()395     public void testPersistentDataBlock_empty() {
396         mStorage.mPersistentDataBlockManager = mock(PersistentDataBlockManagerInternal.class);
397 
398         assertSame(PersistentData.NONE, mStorage.readPersistentDataBlock());
399     }
400 
401     @Test
testPersistentDataBlock_withData()402     public void testPersistentDataBlock_withData() {
403         mStorage.mPersistentDataBlockManager = mock(PersistentDataBlockManagerInternal.class);
404         when(mStorage.mPersistentDataBlockManager.getFrpCredentialHandle())
405                 .thenReturn(PersistentData.toBytes(PersistentData.TYPE_SP_WEAVER, SOME_USER_ID,
406                         DevicePolicyManager.PASSWORD_QUALITY_COMPLEX, PAYLOAD));
407 
408         PersistentData data = mStorage.readPersistentDataBlock();
409 
410         assertEquals(PersistentData.TYPE_SP_WEAVER, data.type);
411         assertEquals(SOME_USER_ID, data.userId);
412         assertEquals(DevicePolicyManager.PASSWORD_QUALITY_COMPLEX, data.qualityForUi);
413         assertArrayEquals(PAYLOAD, data.payload);
414     }
415 
416     @Test
testPersistentDataBlock_exception()417     public void testPersistentDataBlock_exception() {
418         mStorage.mPersistentDataBlockManager = mock(PersistentDataBlockManagerInternal.class);
419         when(mStorage.mPersistentDataBlockManager.getFrpCredentialHandle())
420                 .thenThrow(new IllegalStateException("oops"));
421         assertSame(PersistentData.NONE, mStorage.readPersistentDataBlock());
422     }
423 
424     @Test
testPersistentData_serializeUnserialize()425     public void testPersistentData_serializeUnserialize() {
426         byte[] serialized = PersistentData.toBytes(PersistentData.TYPE_SP, SOME_USER_ID,
427                 DevicePolicyManager.PASSWORD_QUALITY_COMPLEX, PAYLOAD);
428         PersistentData deserialized = PersistentData.fromBytes(serialized);
429 
430         assertEquals(PersistentData.TYPE_SP, deserialized.type);
431         assertEquals(DevicePolicyManager.PASSWORD_QUALITY_COMPLEX, deserialized.qualityForUi);
432         assertArrayEquals(PAYLOAD, deserialized.payload);
433     }
434 
435     @Test
testPersistentData_unserializeNull()436     public void testPersistentData_unserializeNull() {
437         PersistentData deserialized = PersistentData.fromBytes(null);
438         assertSame(PersistentData.NONE, deserialized);
439     }
440 
441     @Test
testPersistentData_unserializeEmptyArray()442     public void testPersistentData_unserializeEmptyArray() {
443         PersistentData deserialized = PersistentData.fromBytes(new byte[0]);
444         assertSame(PersistentData.NONE, deserialized);
445     }
446 
447     @Test
testPersistentData_unserializeInvalid()448     public void testPersistentData_unserializeInvalid() {
449         assertNotNull(suppressAndReturnWtf(() -> {
450             PersistentData deserialized = PersistentData.fromBytes(new byte[]{5});
451             assertSame(PersistentData.NONE, deserialized);
452         }));
453     }
454 
455     @Test
testPersistentData_unserialize_version1()456     public void testPersistentData_unserialize_version1() {
457         // This test ensures that we can read serialized VERSION_1 PersistentData even if we change
458         // the wire format in the future.
459         byte[] serializedVersion1 = new byte[] {
460                 1, /* PersistentData.VERSION_1 */
461                 1, /* PersistentData.TYPE_SP */
462                 0x00, 0x00, 0x04, 0x0A,  /* SOME_USER_ID */
463                 0x00, 0x03, 0x00, 0x00,  /* PASSWORD_NUMERIC_COMPLEX */
464                 1, 2, -1, -2, 33, /* PAYLOAD */
465         };
466         PersistentData deserialized = PersistentData.fromBytes(serializedVersion1);
467         assertEquals(PersistentData.TYPE_SP, deserialized.type);
468         assertEquals(SOME_USER_ID, deserialized.userId);
469         assertEquals(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX,
470                 deserialized.qualityForUi);
471         assertArrayEquals(PAYLOAD, deserialized.payload);
472 
473         // Make sure the constants we use on the wire do not change.
474         assertEquals(0, PersistentData.TYPE_NONE);
475         assertEquals(1, PersistentData.TYPE_SP);
476         assertEquals(2, PersistentData.TYPE_SP_WEAVER);
477     }
478 
assertArrayEquals(byte[] expected, byte[] actual)479     private static void assertArrayEquals(byte[] expected, byte[] actual) {
480         if (!Arrays.equals(expected, actual)) {
481             fail("expected:<" + Arrays.toString(expected) +
482                     "> but was:<" + Arrays.toString(actual) + ">");
483         }
484     }
485 
writePasswordBytes(byte[] password, int userId)486     private void writePasswordBytes(byte[] password, int userId) {
487         mStorage.writeCredentialHash(CredentialHash.create(
488                 password, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD), userId);
489     }
490 
writePatternBytes(byte[] pattern, int userId)491     private void writePatternBytes(byte[] pattern, int userId) {
492         mStorage.writeCredentialHash(CredentialHash.create(
493                 pattern, LockPatternUtils.CREDENTIAL_TYPE_PATTERN), userId);
494     }
495 
assertPasswordBytes(byte[] password, int userId)496     private void assertPasswordBytes(byte[] password, int userId) {
497         CredentialHash cred = mStorage.readCredentialHash(userId);
498         assertEquals(LockPatternUtils.CREDENTIAL_TYPE_PASSWORD_OR_PIN, cred.type);
499         assertArrayEquals(password, cred.hash);
500     }
501 
assertPatternBytes(byte[] pattern, int userId)502     private void assertPatternBytes(byte[] pattern, int userId) {
503         CredentialHash cred = mStorage.readCredentialHash(userId);
504         assertEquals(LockPatternUtils.CREDENTIAL_TYPE_PATTERN, cred.type);
505         assertArrayEquals(pattern, cred.hash);
506     }
507 
508     /**
509      * Suppresses reporting of the WTF to system_server, so we don't pollute the dropbox with
510      * intentionally caused WTFs.
511      */
suppressAndReturnWtf(Runnable r)512     private TerribleFailure suppressAndReturnWtf(Runnable r) {
513         TerribleFailure[] captured = new TerribleFailure[1];
514         TerribleFailureHandler prevWtfHandler = Log.setWtfHandler((t, w, s) -> captured[0] = w);
515         try {
516             r.run();
517         } finally {
518             Log.setWtfHandler(prevWtfHandler);
519         }
520         return captured[0];
521     }
522 
joinAll(List<Thread> threads, long timeoutMillis)523     private static void joinAll(List<Thread> threads, long timeoutMillis) {
524         long deadline = SystemClock.uptimeMillis() + timeoutMillis;
525         for (Thread t : threads) {
526             try {
527                 t.join(deadline - SystemClock.uptimeMillis());
528                 if (t.isAlive()) {
529                     t.interrupt();
530                     throw new RuntimeException(
531                             "Joining " + t + " timed out. Stack: \n" + getStack(t));
532                 }
533             } catch (InterruptedException e) {
534                 throw new RuntimeException("Interrupted while joining " + t, e);
535             }
536         }
537     }
538 
getStack(Thread t)539     private static String getStack(Thread t) {
540         StringBuilder sb = new StringBuilder();
541         sb.append(t.toString()).append('\n');
542         for (StackTraceElement ste : t.getStackTrace()) {
543             sb.append("\tat ").append(ste.toString()).append('\n');
544         }
545         return sb.toString();
546     }
547 }
548