aladinnetwork-blockstack
Version:
The Aladin Javascript library for authentication, identity, and storage.
219 lines (201 loc) • 6.31 kB
text/typescript
import { Transaction, script, crypto as bjsCrypto, ECPair } from 'bitcoinjs-lib'
import crypto from 'crypto'
// @ts-ignore: Could not find a declaration file for module
import { TokenSigner } from 'jsontokens'
import { ecPairToAddress, hexStringToECPair } from '../utils'
import { fetchPrivate } from '../fetchUtil'
import { getPublicKeyFromPrivate } from '../keys'
import { Logger } from '../logger'
import { FileNotFound } from '../errors'
/**
* @ignore
*/
export const ALADIN_GAIA_HUB_LABEL = 'aladin-gaia-hub-config'
/**
* The configuration for the user's Gaia storage provider.
*/
export interface GaiaHubConfig {
address: string,
url_prefix: string,
token: string,
server: string
}
/**
*
* @param filename
* @param contents
* @param hubConfig
* @param contentType
*
* @ignore
*/
export async function uploadToGaiaHub(
filename: string, contents: any,
hubConfig: GaiaHubConfig,
contentType: string = 'application/octet-stream'
): Promise<string> {
Logger.debug(`uploadToGaiaHub: uploading ${filename} to ${hubConfig.server}`)
const response = await fetchPrivate(
`${hubConfig.server}/store/${hubConfig.address}/${filename}`, {
method: 'POST',
headers: {
'Content-Type': contentType,
Authorization: `bearer ${hubConfig.token}`
},
body: contents
}
)
if (!response.ok) {
throw new Error('Error when uploading to Gaia hub')
}
const responseText = await response.text()
const responseJSON = JSON.parse(responseText)
return responseJSON.publicURL
}
export async function deleteFromGaiaHub(
filename: string,
hubConfig: GaiaHubConfig
): Promise<void> {
Logger.debug(`deleteFromGaiaHub: deleting ${filename} from ${hubConfig.server}`)
const response = await fetch(
`${hubConfig.server}/delete/${hubConfig.address}/${filename}`, {
method: 'DELETE',
headers: {
Authorization: `bearer ${hubConfig.token}`
}
}
)
if (!response.ok) {
let responseMsg = ''
try {
responseMsg = await response.text()
} catch (error) {
Logger.debug(`Error getting bad http response text: ${error}`)
}
const errorMsg = 'Error deleting file from Gaia hub: '
+ `${response.status} ${response.statusText}: ${responseMsg}`
Logger.error(errorMsg)
if (response.status === 404) {
throw new FileNotFound(errorMsg)
} else {
throw new Error(errorMsg)
}
}
}
/**
*
* @param filename
* @param hubConfig
*
* @ignore
*/
export function getFullReadUrl(filename: string,
hubConfig: GaiaHubConfig): Promise<string> {
return Promise.resolve(`${hubConfig.url_prefix}${hubConfig.address}/${filename}`)
}
/**
*
* @param challengeText
* @param signerKeyHex
*
* @ignore
*/
function makeLegacyAuthToken(challengeText: string, signerKeyHex: string): string {
// only sign specific legacy auth challenges.
let parsedChallenge
try {
parsedChallenge = JSON.parse(challengeText)
} catch (err) {
throw new Error('Failed in parsing legacy challenge text from the gaia hub.')
}
if (parsedChallenge[0] === 'gaiahub'
&& parsedChallenge[3] === 'aladin_storage_please_sign') {
const signer = hexStringToECPair(signerKeyHex
+ (signerKeyHex.length === 64 ? '01' : ''))
const digest = bjsCrypto.sha256(Buffer.from(challengeText))
const signatureBuffer = signer.sign(digest)
const signatureWithHash = script.signature.encode(
signatureBuffer, Transaction.SIGHASH_NONE)
// We only want the DER encoding so remove the sighash version byte at the end.
// See: https://github.com/bitcoinjs/bitcoinjs-lib/issues/1241#issuecomment-428062912
const signature = signatureWithHash.toString('hex').slice(0, -2)
const publickey = getPublicKeyFromPrivate(signerKeyHex)
const token = Buffer.from(JSON.stringify(
{ publickey, signature }
)).toString('base64')
return token
} else {
throw new Error('Failed to connect to legacy gaia hub. If you operate this hub, please update.')
}
}
/**
*
* @param hubInfo
* @param signerKeyHex
* @param hubUrl
* @param associationToken
*
* @ignore
*/
function makeV1GaiaAuthToken(hubInfo: any,
signerKeyHex: string,
hubUrl: string,
associationToken?: string): string {
const challengeText = hubInfo.challenge_text
const handlesV1Auth = (hubInfo.latest_auth_version
&& parseInt(hubInfo.latest_auth_version.slice(1), 10) >= 1)
const iss = getPublicKeyFromPrivate(signerKeyHex)
if (!handlesV1Auth) {
return makeLegacyAuthToken(challengeText, signerKeyHex)
}
const salt = crypto.randomBytes(16).toString('hex')
const payload = {
gaiaChallenge: challengeText,
hubUrl,
iss,
salt,
associationToken
}
const token = new TokenSigner('ES256K', signerKeyHex).sign(payload)
return `v1:${token}`
}
/**
*
* @ignore
*/
export async function connectToGaiaHub(
gaiaHubUrl: string,
challengeSignerHex: string,
associationToken?: string
): Promise<GaiaHubConfig> {
Logger.debug(`connectToGaiaHub: ${gaiaHubUrl}/hub_info`)
const response = await fetchPrivate(`${gaiaHubUrl}/hub_info`)
const hubInfo = await response.json()
const readURL = hubInfo.read_url_prefix
const token = makeV1GaiaAuthToken(hubInfo, challengeSignerHex, gaiaHubUrl, associationToken)
const address = ecPairToAddress(hexStringToECPair(challengeSignerHex
+ (challengeSignerHex.length === 64 ? '01' : '')))
return {
url_prefix: readURL,
address,
token,
server: gaiaHubUrl
}
}
/**
*
* @param gaiaHubUrl
* @param appPrivateKey
*
* @ignore
*/
export async function getBucketUrl(gaiaHubUrl: string, appPrivateKey: string): Promise<string> {
const challengeSigner = ECPair.fromPrivateKey(Buffer.from(appPrivateKey, 'hex'))
const response = await fetchPrivate(`${gaiaHubUrl}/hub_info`)
const responseText = await response.text()
const responseJSON = JSON.parse(responseText)
const readURL = responseJSON.read_url_prefix
const address = ecPairToAddress(challengeSigner)
const bucketUrl = `${readURL}${address}/`
return bucketUrl
}