Skip to content

Commit cb5345c

Browse files
committed
Add logic for loading Airbitz data from disk
1 parent 4831b50 commit cb5345c

6 files changed

Lines changed: 141 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44

5+
- added: `EdgeContextOptions.airbitzSupport`, for loading legacy Airbitz data from disk.
56
- fixed: Export the `EdgeObjectTemplate` type.
67
- fixed: TypeScript v5 compatibility.
78

src/core/login/airbitz-stashes.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { asCodec, asObject, asOptional, asString, Cleaner } from 'cleaners'
2+
import { justFolders, navigateDisklet } from 'disklet'
3+
4+
import { fixUsername } from '../../client-side'
5+
import { asBase32, asEdgeBox, asEdgeSnrp } from '../../types/server-cleaners'
6+
import { EdgeIo } from '../../types/types'
7+
import { base58, utf8 } from '../../util/encoding'
8+
import { makeJsonFile } from '../../util/file-helpers'
9+
import { userIdSnrp } from '../scrypt/scrypt-selectors'
10+
import { LoginStash } from './login-stash'
11+
12+
/**
13+
* Reads legacy Airbitz login stashes from disk.
14+
*/
15+
export async function loadAirbitzStashes(
16+
io: EdgeIo,
17+
avoidUsernames: Set<string>
18+
): Promise<LoginStash[]> {
19+
const out: LoginStash[] = []
20+
21+
const paths = await io.disklet.list('Accounts').then(justFolders)
22+
for (const path of paths) {
23+
const folder = navigateDisklet(io.disklet, path)
24+
const [
25+
carePackage,
26+
loginPackage,
27+
otp,
28+
pin2Key,
29+
recovery2Key,
30+
usernameJson
31+
] = await Promise.all([
32+
await carePackageFile.load(folder, 'CarePackage.json'),
33+
await loginPackageFile.load(folder, 'LoginPackage.json'),
34+
await otpFile.load(folder, 'OtpKey.json'),
35+
await pin2KeyFile.load(folder, 'Pin2Key.json'),
36+
await recovery2KeyFile.load(folder, 'Recovery2Key.json'),
37+
await usernameFile.load(folder, 'UserName.json')
38+
])
39+
40+
if (usernameJson == null) continue
41+
const username = fixUsername(usernameJson.userName)
42+
if (avoidUsernames.has(username)) continue
43+
const userId = await io.scrypt(
44+
utf8.parse(username),
45+
userIdSnrp.salt_hex,
46+
userIdSnrp.n,
47+
userIdSnrp.r,
48+
userIdSnrp.p,
49+
32
50+
)
51+
52+
// Assemble a modern stash object:
53+
const stash: LoginStash = {
54+
appId: '',
55+
loginId: userId,
56+
pendingVouchers: [],
57+
username
58+
}
59+
if (carePackage != null && loginPackage != null) {
60+
stash.passwordKeySnrp = carePackage.SNRP2
61+
stash.passwordBox = loginPackage.EMK_LP2
62+
stash.syncKeyBox = loginPackage.ESyncKey
63+
stash.passwordAuthBox = loginPackage.ELP1
64+
}
65+
if (otp != null) {
66+
stash.otpKey = otp.TOTP
67+
}
68+
if (pin2Key != null) {
69+
stash.pin2Key = pin2Key.pin2Key
70+
}
71+
if (recovery2Key != null) {
72+
stash.recovery2Key = recovery2Key.recovery2Key
73+
}
74+
75+
out.push(stash)
76+
}
77+
78+
return out
79+
}
80+
81+
/**
82+
* A string of base58-encoded binary data.
83+
*/
84+
const asBase58: Cleaner<Uint8Array> = asCodec(
85+
raw => base58.parse(asString(raw)),
86+
clean => base58.stringify(clean)
87+
)
88+
89+
const carePackageFile = makeJsonFile(
90+
asObject({
91+
SNRP2: asEdgeSnrp, // passwordKeySnrp
92+
SNRP3: asOptional(asEdgeSnrp), // recoveryKeySnrp
93+
SNRP4: asOptional(asEdgeSnrp), // questionKeySnrp
94+
ERQ: asOptional(asEdgeBox) // questionBox
95+
})
96+
)
97+
98+
const loginPackageFile = makeJsonFile(
99+
asObject({
100+
EMK_LP2: asEdgeBox, // passwordBox
101+
EMK_LRA3: asOptional(asEdgeBox), // recoveryBox
102+
103+
ESyncKey: asEdgeBox, // syncKeyBox
104+
ELP1: asEdgeBox // passwordAuthBox
105+
})
106+
)
107+
108+
const otpFile = makeJsonFile(asObject({ TOTP: asBase32 }))
109+
const pin2KeyFile = makeJsonFile(asObject({ pin2Key: asBase58 }))
110+
const recovery2KeyFile = makeJsonFile(asObject({ recovery2Key: asBase58 }))
111+
const usernameFile = makeJsonFile(asObject({ userName: asString }))

src/core/root.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Dispatch } from './actions'
99
import { CLIENT_FILE_NAME, clientFile } from './context/client-file'
1010
import { INFO_CACHE_FILE_NAME, infoCacheFile } from './context/info-cache-file'
1111
import { filterLogs, LogBackend, makeLog } from './log/log'
12+
import { loadAirbitzStashes } from './login/airbitz-stashes'
1213
import { loadStashes } from './login/login-stash'
1314
import { PluginIos, watchPlugins } from './plugins/plugins-actions'
1415
import { RootOutput, rootPixie, RootProps } from './root-pixie'
@@ -33,15 +34,16 @@ export async function makeContext(
3334
): Promise<EdgeContext> {
3435
const { io } = ios
3536
const {
37+
airbitzSupport = false,
3638
apiKey,
3739
appId = '',
3840
authServer = 'https://login.edge.app/api',
39-
infoServer,
40-
syncServer,
4141
deviceDescription = null,
4242
hideKeys = false,
43+
infoServer,
4344
plugins: pluginsInit = {},
44-
skipBlockHeight = false
45+
skipBlockHeight = false,
46+
syncServer
4547
} = opts
4648
const infoServers =
4749
typeof infoServer === 'string'
@@ -82,19 +84,35 @@ export async function makeContext(
8284
})
8385
const log = makeLog(logBackend, 'edge-core')
8486

87+
// Load the login stashes from disk:
8588
let [clientInfo, infoCache = {}, stashes] = await Promise.all([
8689
clientFile.load(io.disklet, CLIENT_FILE_NAME),
8790
infoCacheFile.load(io.disklet, INFO_CACHE_FILE_NAME),
8891
loadStashes(io.disklet, log)
8992
])
9093

94+
// Load legacy stashes from disk
95+
if (airbitzSupport) {
96+
// Edge will write modern files to disk at login time,
97+
// but it won't delete the legacy Airbitz data.
98+
// Once this happens, we need to ignore the legacy files
99+
// and just use the new files:
100+
const avoidUsernames = new Set<string>()
101+
for (const { username } of stashes) {
102+
if (username != null) avoidUsernames.add(username)
103+
}
104+
105+
const airbitzStashes = await loadAirbitzStashes(io, avoidUsernames)
106+
stashes.push(...airbitzStashes)
107+
}
108+
91109
// Save the clientId if we don't have one:
92110
if (clientInfo == null) {
93111
clientInfo = { clientId: io.random(16) }
94112
await clientFile.save(io.disklet, CLIENT_FILE_NAME, clientInfo)
95113
}
96114

97-
// Load the login stashes from disk:
115+
// Write everything to redux:
98116
redux.dispatch({
99117
type: 'INIT',
100118
payload: {

src/react-native.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export function MakeEdgeContext(props: EdgeContextProps): JSX.Element {
3636
onLog = defaultOnLog,
3737

3838
// Inner context options:
39+
airbitzSupport = false,
3940
apiKey = '',
4041
appId = '',
4142
authServer,
@@ -63,6 +64,7 @@ export function MakeEdgeContext(props: EdgeContextProps): JSX.Element {
6364
bridgifyLogBackend({ crashReporter, onLog }),
6465
pluginUris,
6566
{
67+
airbitzSupport,
6668
apiKey,
6769
appId,
6870
authServer,

src/types/exports.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export interface EdgeContextProps extends CommonProps {
7676
onLog?: EdgeOnLog
7777

7878
// EdgeContextOptions:
79+
airbitzSupport?: boolean
7980
apiKey?: string
8081
appId?: string
8182
authServer?: string

src/types/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1683,6 +1683,9 @@ export interface EdgeContextOptions {
16831683
path?: string // Only used on node.js
16841684
plugins?: EdgeCorePluginsInit
16851685

1686+
/** True to load Airbitz user files from disk */
1687+
airbitzSupport?: boolean
1688+
16861689
/**
16871690
* True to skip updating the `EdgeCurrencyWallet.blockHeight` property.
16881691
* This may improve performance by reducing bridge traffic,
@@ -1857,6 +1860,7 @@ export interface EdgeFakeWorldOptions {
18571860

18581861
export interface EdgeFakeContextOptions {
18591862
// EdgeContextOptions:
1863+
airbitzSupport?: boolean
18601864
apiKey: string
18611865
appId: string
18621866
deviceDescription?: string

0 commit comments

Comments
 (0)