@@ -3,7 +3,7 @@ import _ from 'lodash';
33import $ from '@/core/app' ;
44import { ENV } from '@/vendor/open-api' ;
55import { failed , success } from '@/restful/response' ;
6- import { updateArtifactStore , updateAvatar } from '@/restful/settings' ;
6+ import { updateArtifactStore } from '@/restful/settings' ;
77import resourceCache from '@/utils/resource-cache' ;
88import scriptResourceCache from '@/utils/script-resource-cache' ;
99import headersResourceCache from '@/utils/headers-resource-cache' ;
@@ -17,6 +17,13 @@ import Gist from '@/utils/gist';
1717import migrate from '@/utils/migration' ;
1818import env from '@/utils/env' ;
1919import { formatDateTime } from '@/utils' ;
20+ import {
21+ AGE_SECRET_KEY ,
22+ decryptArmorIfPresent ,
23+ derivePublicKey ,
24+ encryptArmor ,
25+ isAgeArmor ,
26+ } from '@/utils/age' ;
2027
2128export default function register ( $app ) {
2229 // utils
@@ -144,6 +151,65 @@ async function refresh(_, res) {
144151 success ( res ) ;
145152}
146153
154+ function readCurrentBackupContent ( ) {
155+ let content = $ . read ( '#sub-store' ) ;
156+ content = content ? JSON . parse ( content ) : { } ;
157+ if ( $ . env . isNode ) content = JSON . parse ( JSON . stringify ( $ . cache ) ) ;
158+ return content ;
159+ }
160+
161+ function serializeGistBackupContent ( content , encoding , options = { } ) {
162+ const backup = JSON . parse ( JSON . stringify ( content || { } ) ) ;
163+ if ( ! options . keepAgeSecretKey && backup . settings ?. [ AGE_SECRET_KEY ] ) {
164+ delete backup . settings [ AGE_SECRET_KEY ] ;
165+ }
166+ if ( encoding === 'plaintext' ) {
167+ backup . settings = backup . settings || { } ;
168+ backup . settings . gistToken = '恢复后请重新设置 GitHub Token' ;
169+ return JSON . stringify ( backup , null , ` ` ) ;
170+ }
171+
172+ return Base64 . encode ( JSON . stringify ( backup , null , ` ` ) ) ;
173+ }
174+
175+ function normalizeGistBackupEncoding ( encoding ) {
176+ return [ 'base64' , 'plaintext' , 'age' ] . includes ( encoding )
177+ ? encoding
178+ : 'base64' ;
179+ }
180+
181+ function getGistBackupPayloadEncoding ( encoding ) {
182+ return encoding === 'plaintext' ? 'plaintext' : 'base64' ;
183+ }
184+
185+ function isAgeGistBackupEncoding ( encoding ) {
186+ return encoding === 'age' ;
187+ }
188+
189+ async function encryptGistBackupContent ( content , settings , encoding ) {
190+ if ( ! isAgeGistBackupEncoding ( encoding ) ) return content ;
191+
192+ const ageSecretKey = settings ?. [ AGE_SECRET_KEY ] ;
193+ if ( ! ageSecretKey ) {
194+ throw new Error ( 'age 加密模式需要配置 age 解密私钥' ) ;
195+ }
196+
197+ $ . info ( `使用 age 加密 Gist 备份内容` ) ;
198+ return encryptArmor ( content , await derivePublicKey ( ageSecretKey ) ) ;
199+ }
200+
201+ async function decryptGistBackupContent ( content , settings , encoding ) {
202+ if ( ! isAgeGistBackupEncoding ( encoding ) ) return content ;
203+
204+ const ageSecretKey = settings ?. [ AGE_SECRET_KEY ] ;
205+ if ( ! ageSecretKey ) {
206+ throw new Error ( 'age 加密模式需要配置 age 解密私钥' ) ;
207+ }
208+
209+ $ . info ( `尝试使用 age 解密 Gist 备份内容` ) ;
210+ return decryptArmorIfPresent ( content , ageSecretKey ) ;
211+ }
212+
147213async function gistBackupAction ( action , keep , encode ) {
148214 // read token
149215 const { gistToken, syncPlatform } = $ . read ( SETTINGS_KEY ) ;
@@ -154,38 +220,40 @@ async function gistBackupAction(action, keep, encode) {
154220 key : GIST_BACKUP_KEY ,
155221 syncPlatform,
156222 } ) ;
157- let currentContent = $ . read ( '#sub-store' ) ;
158- currentContent = currentContent ? JSON . parse ( currentContent ) : { } ;
159- if ( $ . env . isNode ) currentContent = JSON . parse ( JSON . stringify ( $ . cache ) ) ;
223+ let currentContent = readCurrentBackupContent ( ) ;
160224 let content ;
161225 const settings = $ . read ( SETTINGS_KEY ) ;
162226 const updated = settings . syncTime ;
163227
164- const encoding = encode || settings . gistUpload || 'base64' ;
228+ const encoding = normalizeGistBackupEncoding (
229+ encode || settings . gistUpload || 'base64' ,
230+ ) ;
165231 $ . info (
166232 `Gist backup action: ${ action } , keep: ${ keep } , encode: ${ encode } , settings encode: ${ settings . gistUpload } , final encoding: ${ encoding } ` ,
167233 ) ;
168234 switch ( action ) {
169235 case 'upload' :
170236 try {
171- content = $ . read ( '#sub-store' ) ;
172- content = content ? JSON . parse ( content ) : { } ;
173- if ( $ . env . isNode ) content = JSON . parse ( JSON . stringify ( $ . cache ) ) ;
174- if ( encoding === 'plaintext' ) {
175- content . settings . gistToken =
176- '恢复后请重新设置 GitHub Token' ;
177- content = JSON . stringify ( content , null , ` ` ) ;
178- } else {
179- content = Base64 . encode (
180- JSON . stringify ( content , null , ` ` ) ,
181- ) ;
182- }
237+ const keepAgeSecretKey = isAgeGistBackupEncoding ( encoding ) ;
238+ content = serializeGistBackupContent (
239+ readCurrentBackupContent ( ) ,
240+ getGistBackupPayloadEncoding ( encoding ) ,
241+ { keepAgeSecretKey } ,
242+ ) ;
183243
184244 $ . info ( `下载备份, 与本地内容对比...` ) ;
185- const onlineContent = await gist . download (
245+ const downloadedContent = await gist . download (
186246 GIST_BACKUP_FILE_NAME ,
187247 ) ;
188- if ( onlineContent === content ) {
248+ const onlineContent = await decryptGistBackupContent (
249+ downloadedContent ,
250+ settings ,
251+ encoding ,
252+ ) ;
253+ const canReuseOnlineContent =
254+ ! isAgeGistBackupEncoding ( encoding ) ||
255+ isAgeArmor ( downloadedContent ) ;
256+ if ( canReuseOnlineContent && onlineContent === content ) {
189257 $ . info ( `内容一致, 无需上传备份` ) ;
190258 return ;
191259 }
@@ -196,15 +264,16 @@ async function gistBackupAction(action, keep, encode) {
196264 // update syncTime
197265 settings . syncTime = new Date ( ) . getTime ( ) ;
198266 $ . write ( settings , SETTINGS_KEY ) ;
199- content = $ . read ( '#sub-store' ) ;
200- content = content ? JSON . parse ( content ) : { } ;
201- if ( $ . env . isNode ) content = JSON . parse ( JSON . stringify ( $ . cache ) ) ;
202- if ( encoding === 'plaintext' ) {
203- content . settings . gistToken = '恢复后请重新设置 GitHub Token' ;
204- content = JSON . stringify ( content , null , ` ` ) ;
205- } else {
206- content = Base64 . encode ( JSON . stringify ( content , null , ` ` ) ) ;
207- }
267+ content = serializeGistBackupContent (
268+ readCurrentBackupContent ( ) ,
269+ getGistBackupPayloadEncoding ( encoding ) ,
270+ { keepAgeSecretKey : isAgeGistBackupEncoding ( encoding ) } ,
271+ ) ;
272+ content = await encryptGistBackupContent (
273+ content ,
274+ settings ,
275+ encoding ,
276+ ) ;
208277 $ . info ( `上传备份中...` ) ;
209278 try {
210279 await gist . upload ( {
@@ -221,6 +290,11 @@ async function gistBackupAction(action, keep, encode) {
221290 case 'download' :
222291 $ . info ( `还原备份中...` ) ;
223292 content = await gist . download ( GIST_BACKUP_FILE_NAME ) ;
293+ content = await decryptGistBackupContent (
294+ content ,
295+ settings ,
296+ encoding ,
297+ ) ;
224298 try {
225299 content = JSON . parse ( Base64 . decode ( content ) ) ;
226300 if ( ! ( Object . keys ( content . settings ) . length >= 0 ) ) {
0 commit comments