Skip to content

Commit

Permalink
Merge pull request #593 from EdgeApp/william/load-airbitz-stashes
Browse files Browse the repository at this point in the history
Load Airbitz stashes
  • Loading branch information
swansontec authored Mar 29, 2024
2 parents 4831b50 + 820c78a commit 40292d8
Show file tree
Hide file tree
Showing 9 changed files with 247 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

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

Expand Down
12 changes: 11 additions & 1 deletion src/core/fake/fake-world.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,11 @@ export function makeFakeWorld(
},

async makeEdgeContext(opts: EdgeFakeContextOptions): Promise<EdgeContext> {
const { allowNetworkAccess = false, cleanDevice = false } = opts
const {
allowNetworkAccess = false,
cleanDevice = false,
extraFiles = {}
} = opts

const fakeFetch = makeFetchFunction(fakeServer)
const fetch: EdgeFetchFunction = !allowNetworkAccess
Expand Down Expand Up @@ -138,6 +142,12 @@ export function makeFakeWorld(
}
}

if (extraFiles != null) {
for (const path of Object.keys(extraFiles)) {
await fakeIo.disklet.setText(path, extraFiles[path])
}
}

const out = await makeContext({ io: fakeIo, nativeIo }, logBackend, {
...opts
})
Expand Down
111 changes: 111 additions & 0 deletions src/core/login/airbitz-stashes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { asCodec, asObject, asOptional, asString, Cleaner } from 'cleaners'
import { justFolders, navigateDisklet } from 'disklet'

import { fixUsername } from '../../client-side'
import { asBase32, asEdgeBox, asEdgeSnrp } from '../../types/server-cleaners'
import { EdgeIo } from '../../types/types'
import { base58, utf8 } from '../../util/encoding'
import { makeJsonFile } from '../../util/file-helpers'
import { userIdSnrp } from '../scrypt/scrypt-selectors'
import { LoginStash } from './login-stash'

/**
* Reads legacy Airbitz login stashes from disk.
*/
export async function loadAirbitzStashes(
io: EdgeIo,
avoidUsernames: Set<string>
): Promise<LoginStash[]> {
const out: LoginStash[] = []

const paths = await io.disklet.list('Accounts').then(justFolders)
for (const path of paths) {
const folder = navigateDisklet(io.disklet, path)
const [
carePackage,
loginPackage,
otp,
pin2Key,
recovery2Key,
usernameJson
] = await Promise.all([
await carePackageFile.load(folder, 'CarePackage.json'),
await loginPackageFile.load(folder, 'LoginPackage.json'),
await otpFile.load(folder, 'OtpKey.json'),
await pin2KeyFile.load(folder, 'Pin2Key.json'),
await recovery2KeyFile.load(folder, 'Recovery2Key.json'),
await usernameFile.load(folder, 'UserName.json')
])

if (usernameJson == null) continue
const username = fixUsername(usernameJson.userName)
if (avoidUsernames.has(username)) continue
const userId = await io.scrypt(
utf8.parse(username),
userIdSnrp.salt_hex,
userIdSnrp.n,
userIdSnrp.r,
userIdSnrp.p,
32
)

// Assemble a modern stash object:
const stash: LoginStash = {
appId: '',
loginId: userId,
pendingVouchers: [],
username
}
if (carePackage != null && loginPackage != null) {
stash.passwordKeySnrp = carePackage.SNRP2
stash.passwordBox = loginPackage.EMK_LP2
stash.syncKeyBox = loginPackage.ESyncKey
stash.passwordAuthBox = loginPackage.ELP1
}
if (otp != null) {
stash.otpKey = otp.TOTP
}
if (pin2Key != null) {
stash.pin2Key = pin2Key.pin2Key
}
if (recovery2Key != null) {
stash.recovery2Key = recovery2Key.recovery2Key
}

out.push(stash)
}

return out
}

/**
* A string of base58-encoded binary data.
*/
const asBase58: Cleaner<Uint8Array> = asCodec(
raw => base58.parse(asString(raw)),
clean => base58.stringify(clean)
)

const carePackageFile = makeJsonFile(
asObject({
SNRP2: asEdgeSnrp, // passwordKeySnrp
SNRP3: asOptional(asEdgeSnrp), // recoveryKeySnrp
SNRP4: asOptional(asEdgeSnrp), // questionKeySnrp
ERQ: asOptional(asEdgeBox) // questionBox
})
)

const loginPackageFile = makeJsonFile(
asObject({
EMK_LP2: asEdgeBox, // passwordBox
EMK_LRA3: asOptional(asEdgeBox), // recoveryBox

ESyncKey: asEdgeBox, // syncKeyBox
ELP1: asEdgeBox // passwordAuthBox
})
)

const otpFile = makeJsonFile(asObject({ TOTP: asBase32 }))
const pin2KeyFile = makeJsonFile(asObject({ pin2Key: asBase58 }))
const recovery2KeyFile = makeJsonFile(asObject({ recovery2Key: asBase58 }))
const usernameFile = makeJsonFile(asObject({ userName: asString }))
26 changes: 22 additions & 4 deletions src/core/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Dispatch } from './actions'
import { CLIENT_FILE_NAME, clientFile } from './context/client-file'
import { INFO_CACHE_FILE_NAME, infoCacheFile } from './context/info-cache-file'
import { filterLogs, LogBackend, makeLog } from './log/log'
import { loadAirbitzStashes } from './login/airbitz-stashes'
import { loadStashes } from './login/login-stash'
import { PluginIos, watchPlugins } from './plugins/plugins-actions'
import { RootOutput, rootPixie, RootProps } from './root-pixie'
Expand All @@ -33,15 +34,16 @@ export async function makeContext(
): Promise<EdgeContext> {
const { io } = ios
const {
airbitzSupport = false,
apiKey,
appId = '',
authServer = 'https://login.edge.app/api',
infoServer,
syncServer,
deviceDescription = null,
hideKeys = false,
infoServer,
plugins: pluginsInit = {},
skipBlockHeight = false
skipBlockHeight = false,
syncServer
} = opts
const infoServers =
typeof infoServer === 'string'
Expand Down Expand Up @@ -82,19 +84,35 @@ export async function makeContext(
})
const log = makeLog(logBackend, 'edge-core')

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

// Load legacy stashes from disk
if (airbitzSupport) {
// Edge will write modern files to disk at login time,
// but it won't delete the legacy Airbitz data.
// Once this happens, we need to ignore the legacy files
// and just use the new files:
const avoidUsernames = new Set<string>()
for (const { username } of stashes) {
if (username != null) avoidUsernames.add(username)
}

const airbitzStashes = await loadAirbitzStashes(io, avoidUsernames)
stashes.push(...airbitzStashes)
}

// Save the clientId if we don't have one:
if (clientInfo == null) {
clientInfo = { clientId: io.random(16) }
await clientFile.save(io.disklet, CLIENT_FILE_NAME, clientInfo)
}

// Load the login stashes from disk:
// Write everything to redux:
redux.dispatch({
type: 'INIT',
payload: {
Expand Down
2 changes: 2 additions & 0 deletions src/react-native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export function MakeEdgeContext(props: EdgeContextProps): JSX.Element {
onLog = defaultOnLog,

// Inner context options:
airbitzSupport = false,
apiKey = '',
appId = '',
authServer,
Expand Down Expand Up @@ -63,6 +64,7 @@ export function MakeEdgeContext(props: EdgeContextProps): JSX.Element {
bridgifyLogBackend({ crashReporter, onLog }),
pluginUris,
{
airbitzSupport,
apiKey,
appId,
authServer,
Expand Down
1 change: 1 addition & 0 deletions src/types/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export interface EdgeContextProps extends CommonProps {
onLog?: EdgeOnLog

// EdgeContextOptions:
airbitzSupport?: boolean
apiKey?: string
appId?: string
authServer?: string
Expand Down
7 changes: 7 additions & 0 deletions src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1683,6 +1683,9 @@ export interface EdgeContextOptions {
path?: string // Only used on node.js
plugins?: EdgeCorePluginsInit

/** True to load Airbitz user files from disk */
airbitzSupport?: boolean

/**
* True to skip updating the `EdgeCurrencyWallet.blockHeight` property.
* This may improve performance by reducing bridge traffic,
Expand Down Expand Up @@ -1857,6 +1860,7 @@ export interface EdgeFakeWorldOptions {

export interface EdgeFakeContextOptions {
// EdgeContextOptions:
airbitzSupport?: boolean
apiKey: string
appId: string
deviceDescription?: string
Expand All @@ -1870,6 +1874,9 @@ export interface EdgeFakeContextOptions {

// Fake device options:
cleanDevice?: boolean

/** Extra files to be saved on the fake device. */
extraFiles?: { [path: string]: string }
}

/**
Expand Down
34 changes: 34 additions & 0 deletions test/core/login/airbitz.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { expect } from 'chai'
import { describe, it } from 'mocha'

import { makeFakeEdgeWorld } from '../../../src/index'
import { airbitzFiles, fakeUser } from '../../fake/fake-user'

const quiet = { onLog() {} }

describe('airbitz stashes', function () {
it('can log into legacy airbitz files', async function () {
const world = await makeFakeEdgeWorld([fakeUser], quiet)
const context = await world.makeEdgeContext({
airbitzSupport: true,
apiKey: '',
appId: '',
cleanDevice: true,
extraFiles: airbitzFiles
})

expect(context.localUsers).deep.equals([
{
keyLoginEnabled: true,
lastLogin: undefined,
loginId: 'BTnpEn7pabDXbcv7VxnKBDsn4CVSwLRA25J8U84qmg4h',
pinLoginEnabled: true,
recovery2Key: 'NVADGXzb5Zc55PYXVVT7GRcXPnY9NZJUjiZK8aQnidc',
username: 'js test 0',
voucherId: undefined
}
])

await context.loginWithPIN(fakeUser.username, fakeUser.pin)
})
})
58 changes: 58 additions & 0 deletions test/fake/fake-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,61 @@ export const fakeUserDump: EdgeFakeUser = {
}

export const fakeUser = { ...fakeUserDump, ...info }

/**
* This data comes from the old C++ abc-cli tool.
* It was created by uploading the "js test 0" user above into a
* testing login server, and then running `abc-cli sign-in`
* with the matching username and password.
* These are the exact files abc-cli wrote to disk
* once the sign-in was complete.
*/
export const airbitzFiles = {
'Accounts/Account0/CarePackage.json': JSON.stringify({
SNRP2: {
n: 16384,
p: 1,
r: 2,
salt_hex:
'ed6396d127b60d6ffc469634b9a53bdcfb4ee381e9b9df5e66a0f97895871981'
}
}),
'Accounts/Account0/LoginPackage.json': JSON.stringify({
ELP1: {
data_base64:
'ZHhQtHA48aPf083XbEeNMAzbu4KE5dNLU6q0WzTUwJkxGG72elIha9wMjpAvwxmJ2PC3ZCMya1eiVgHPqTO+zS8dWHmuqzbpNY+IdoAtjF//dZ6O4mCcMR8enmj5xYaVBIIQ8WCcang+2RTqDzOoI+W8p6mM9N528ypy0lkpYi9lpGrxAAAJjhk+9xdBRcL4O5jkCZ0VQEvoRCqlU2y99YtRYtB/+nYj51PTtU00MUpKq7PggNZI5EDmZC9vK/BRnBArLbnwj7L88vuKEXBumYX0GA9ZhTPXMuRfABzvCxPkTKLGG2KmfQAtSAehCDMtkgQzocXSCiUuzqBdId56WkNFYC+Phq6vgflPK2qcxkV6Kz2qu8Yr1nBveyLsUTGOZgoBlya2UEZrQ4B96mUv5Q==',
encryptionType: 0,
iv_hex: 'c801b7e3265734544c08c68bdff86979'
},
EMK_LP2: {
data_base64:
'sXBdJaaeVNWOuBWdRVvULaS+VqPkTF1eLR0BMSi2a4F+DCc+4JbMqgBPK3uyp7MHd3qpOOt7Fcth5gnT5hspzh47ONsTTaQNglwZ4lY25OKGsK7ldWrcohiDEgswgG8whGM3tqio6iIMndkuZn3Dn9aj0SwWNdCuW1xFYvbMa7pCgWr0QT+zjWJAPnlT0U1hqJNjGDqFK6jYorClWKsbBZtVJ/dCRMv5+xu05S7fCdgQnz1m5O5nMHTcw6NFR0eBApOOh3KbghOeh0QcBAa5jNm4L61BK5wMCgPydh2/u+MSu34ERsomA5kwp86N35EKHGJH3p0Jq/jf9ToR9wU/MlPivmHvbbspxIzay0feJcanodfyFqLLnsfknSptgiaX3ppat83xrdndQH+JNYweNTgoZmd5pt/8hu/LGk1iAs8Z6e61FaYXm+UI/yxUQFy3A8meST1UfVAxeFw3IRCRZRplll8fgALH67kO15s4bts=',
encryptionType: 0,
iv_hex: '0989bebe4103816be3db48a2ed3ff338'
},
ESyncKey: {
data_base64:
'ruf9v3eWUg2GZJf+94boCPyui4nQ9HnJCWx07kmRg+nKns+1MlqSFQQNINgHXLWDrQvho69AnQrP9Ep+PcXOnG+m0kiqlmzm8UdhQoQJOKP/O5S2TFwMuLpLrGN+I4F5HbdGVMA20WJjhfQ7Kzc3H2hHwm1BUo0xItbV/audT6KySR+ugeW+jF5glzB0/8eAYFKloYd0YC6TZg1gmZU7jqBatpylk7a9znrZG6zKVyPQxnkKr6TnF3xQihSw2H7g1Gd4AI4Pttye/RYsVbwqFFnD2OZwgp7kqeyLwVRsU6OboRtkBYuaa4adYhXHda94',
encryptionType: 0,
iv_hex: '59309614b12c169af977681e01d6ad8b'
}
}),
'Accounts/Account0/OtpKey.json': JSON.stringify({
TOTP: 'HELLO==='
}),
'Accounts/Account0/Pin2Key.json': JSON.stringify({
pin2Key: '22b6wM3F6bd3LpT1UHLhb7pDr5BxzAuVNhuU5HGudZHq'
}),
'Accounts/Account0/Recovery2Key.json': JSON.stringify({
recovery2Key: 'NVADGXzb5Zc55PYXVVT7GRcXPnY9NZJUjiZK8aQnidc'
}),
'Accounts/Account0/RootKey.json': JSON.stringify({
data_base64:
'pR+yQsnkynA03Xqa8AYHzRzunxsBoFM39huz09DL+20RZxAAid4iWkkBNei+Z6Mp0sdhDNfilPQmU5rOuABo70NIO+E3GNZ66RmG6SkN0Jo0Fgp28Qfyg/aD6BlMNw++oXS8yGuDvPotDpM/rgYd6l7/OuLLfg5cZw85Qe1D9UM9dqP8EVpKPQTqSsAnTE0RsHG3HFVIFVRQAsIqqsynAC+h8QiKdAFaqzdFVbB75iu4KV27wdjfRnZrTVPqGA9fnC96vhRRUNRmQnWbJRvdyhIXRHYXJbu/ip1eFts054yfjhyxHffOXfcSpm3xwL0itf3Y4rEUG0dEQO5IwfpuRxspFFn3S/Fi4wGkw+PJNNtF3r5djryeYOFE854n3YOkBayhyhnNAJuaaHeOnrP7QaD3V4hDuFezHqTWCU8lA7W0u7SmFZ1IXXXxvjITvglkmTrnx8CbWkmjRXqIbMl8tg==',
encryptionType: 0,
iv_hex: '96cc1ebc2d11a0b38c9259c056d3ca23'
}),
'Accounts/Account0/UserName.json': JSON.stringify({
userName: 'js test 0'
})
}

0 comments on commit 40292d8

Please sign in to comment.