1+ "use strict" ;
2+
3+ var LifeCycle = ( ( ) => {
4+
5+ const AES = "AES-GCM" ,
6+ keyUsages = [ "encrypt" , "decrypt" ] ;
7+
8+ function toBase64 ( bytes ) {
9+ return btoa ( Array . from ( bytes ) . map ( b => String . fromCharCode ( b ) ) . join ( '' ) ) ;
10+ }
11+ function fromBase64 ( string ) {
12+ return Uint8Array . from ( ( Array . from ( atob ( string ) ) . map ( c => c . charCodeAt ( 0 ) ) ) ) ;
13+ }
14+ async function encrypt ( clearText ) {
15+ let key = await crypto . subtle . generateKey ( {
16+ name : AES ,
17+ length : 256 ,
18+ } ,
19+ true ,
20+ keyUsages ,
21+ ) ;
22+ let iv = crypto . getRandomValues ( new Uint8Array ( 12 ) ) ;
23+ let encoded = new TextEncoder ( ) . encode ( clearText ) ;
24+ let cypherText = await crypto . subtle . encrypt ( {
25+ name : AES ,
26+ iv
27+ } , key , encoded ) ;
28+ return { cypherText, key : await crypto . subtle . exportKey ( "jwk" , key ) , iv} ;
29+ }
30+
31+ var SurvivalTab = {
32+ url : "about:blank" ,
33+ async createAndStore ( ) {
34+ let allSeen = { } ;
35+ await Promise . all ( ( await browser . tabs . query ( { } ) ) . map (
36+ async t => {
37+ let seen = await ns . collectSeen ( t . id ) ;
38+ if ( seen ) allSeen [ t . id ] = seen ;
39+ }
40+ ) ) ;
41+
42+ let { url} = SurvivalTab ;
43+ let tabInfo = {
44+ url,
45+ active : false ,
46+ } ;
47+ if ( browser . windows ) { // it may be missing on mobile
48+ // check if an incognito windows exist and open our "survival" tab there
49+ for ( let w of await browser . windows . getAll ( ) ) {
50+ if ( w . incognito ) {
51+ tabInfo . windowId = w . id ;
52+ break ;
53+ }
54+ }
55+ }
56+ let tab ;
57+ for ( ; ! tab ; ) {
58+ try {
59+ tab = await browser . tabs . create ( tabInfo ) ;
60+ } catch ( e ) {
61+ error ( e ) ;
62+ if ( tabInfo . windowId ) {
63+ // we might not have incognito permissions, let's try using any window
64+ delete tabInfo . windowId ;
65+ } else {
66+ return ; // bailout
67+ }
68+ }
69+ }
70+ let tabId = tab . id ;
71+
72+ let { cypherText, key, iv} = await encrypt ( JSON . stringify ( {
73+ policy : ns . policy . dry ( true ) ,
74+ allSeen,
75+ unrestrictedTabs : [ ...ns . unrestrictedTabs ]
76+ } ) ) ;
77+
78+ await new Promise ( ( resolve , reject ) => {
79+ let l = async ( tabId , changeInfo ) => {
80+ debug ( "Survival tab updating" , changeInfo ) ;
81+ if ( changeInfo . status !== "complete" ) return ;
82+ try {
83+ await Messages . send ( "store" , { url, data : toBase64 ( new Uint8Array ( cypherText ) ) } , { tabId, frameId : 0 } ) ;
84+ resolve ( ) ;
85+ debug ( "Survival tab updated" ) ;
86+ browser . tabs . onUpdated . removeListener ( l ) ;
87+ } catch ( e ) {
88+ if ( ! Messages . isMissingEndpoint ( e ) ) {
89+ error ( e , "Survival tab failed" ) ;
90+ reject ( e ) ;
91+ } // otherwise we keep waiting for further updates from the tab until content script is ready to answer
92+ } ;
93+ }
94+ browser . tabs . onUpdated . addListener ( l , { tabId} ) ;
95+ } ) ;
96+ await Storage . set ( "local" , { "updateInfo" : { key, iv : toBase64 ( iv ) , tabId} } ) ;
97+ debug ( "Ready to reload..." , await Storage . get ( "local" , "updateInfo" ) ) ;
98+ } ,
99+
100+ async retrieveAndDestroy ( ) {
101+ let { updateInfo} = await Storage . get ( "local" , "updateInfo" ) ;
102+ if ( ! updateInfo ) return ;
103+ await Storage . remove ( "local" , "updateInfo" ) ;
104+ let { key, iv, tabId} = updateInfo ;
105+ key = await crypto . subtle . importKey ( "jwk" , key , AES , true , keyUsages ) ;
106+ iv = fromBase64 ( iv ) ;
107+ let cypherText = fromBase64 ( await Messages . send ( "retrieve" ,
108+ { url : SurvivalTab . url } ,
109+ { tabId, frameId : 0 } ) ) ;
110+ let encoded = await crypto . subtle . decrypt ( {
111+ name : AES ,
112+ iv
113+ } , key , cypherText
114+ ) ;
115+ let { policy, allSeen, unrestrictedTabs} = JSON . parse ( new TextDecoder ( ) . decode ( encoded ) ) ;
116+ if ( ! policy ) {
117+ error ( "Ephemeral policy not found!" ) ;
118+ return ;
119+ }
120+ ns . unrestrictedTabs = new Set ( unrestrictedTabs ) ;
121+ browser . tabs . remove ( tabId ) ;
122+ await ns . initializing ;
123+ ns . policy = new Policy ( policy ) ;
124+ await Promise . all (
125+ Object . entries ( allSeen ) . map (
126+ async ( [ tabId , seen ] ) => {
127+ try {
128+ debug ( "Restoring seen %o to tab %s" , seen , tabId ) ;
129+ await Messages . send ( "allSeen" , { seen} , { tabId, frameId : 0 } ) ;
130+ } catch ( e ) {
131+ error ( e , "Cannot send previously seen data to tab" , tabId ) ;
132+ }
133+ }
134+ )
135+ )
136+ }
137+ } ;
138+
139+ return {
140+ async onInstalled ( details ) {
141+ browser . runtime . onInstalled . removeListener ( this . onInstalled ) ;
142+ let { reason, previousVersion} = details ;
143+ if ( reason !== "update" ) return ;
144+
145+ try {
146+ await SurvivalTab . retrieveAndDestroy ( ) ;
147+ } catch ( e ) {
148+ error ( e ) ;
149+ }
150+
151+ await include ( "/lib/Ver.js" ) ;
152+ previousVersion = new Ver ( previousVersion ) ;
153+ let currentVersion = new Ver ( browser . runtime . getManifest ( ) . version ) ;
154+ let upgrading = Ver . is ( previousVersion , "<=" , currentVersion ) ;
155+ if ( ! upgrading ) return ;
156+
157+ // put here any version specific upgrade adjustment in stored data
158+
159+ if ( Ver . is ( previousVersion , "<=" , "11.0.10" ) ) {
160+ log ( `Upgrading from 11.0.10 or below (${ previousVersion } ): configure the "ping" capability.` ) ;
161+ await ns . initializing ;
162+ ns . policy . TRUSTED . capabilities . add ( "ping" )
163+ await ns . savePolicy ( ) ;
164+ }
165+ } ,
166+
167+ async onUpdateAvailable ( details ) {
168+ await include ( "/lib/Ver.js" ) ;
169+ if ( Ver . is ( details . version , "<" , browser . runtime . getManifest ( ) . version ) ) {
170+ // downgrade: temporary survival might not be supported, and we don't care
171+ return ;
172+ }
173+ try {
174+ await SurvivalTab . createAndStore ( ) ;
175+ } catch ( e ) {
176+ console . error ( e ) ;
177+ } finally {
178+ browser . runtime . reload ( ) ; // apply update
179+ }
180+ }
181+ } ;
182+ } ) ( ) ;
0 commit comments