feat: native-mobile-api

This commit is contained in:
SamuelScheit
2023-04-20 13:01:11 +02:00
parent 28be45a9b4
commit ef673f62ca
17 changed files with 940 additions and 74 deletions

View File

@@ -1,6 +1,7 @@
import { randomBytes } from 'crypto'
import NodeCache from 'node-cache'
import type { Logger } from 'pino'
import { v4 as uuidv4 } from 'uuid'
import { DEFAULT_CACHE_TTLS } from '../Defaults'
import type { AuthenticationCreds, CacheStore, SignalDataSet, SignalDataTypeMap, SignalKeyStore, SignalKeyStoreWithTransaction, TransactionCapabilityOptions } from '../Types'
import { Curve, signedKeyPair } from './crypto'
@@ -202,6 +203,13 @@ export const initAuthCreds = (): AuthenticationCreds => {
accountSyncCounter: 0,
accountSettings: {
unarchiveChats: false
}
},
// mobile creds
deviceId: Buffer.from(uuidv4().replace(/-/g, ''), 'hex').toString('base64url'),
phoneId: uuidv4(),
identityId: randomBytes(20),
registered: false,
backupToken: randomBytes(20),
registration: {} as RegistrationOptions
}
}

View File

@@ -107,6 +107,10 @@ export function sha256(buffer: Buffer) {
return createHash('sha256').update(buffer).digest()
}
export function md5(buffer: Buffer) {
return createHash('md5').update(buffer).digest()
}
// HKDF key expansion
export function hkdf(buffer: Uint8Array | Buffer, expandedLength: number, info: { salt?: Buffer, info?: string }) {
return HKDF(!Buffer.isBuffer(buffer) ? Buffer.from(buffer) : buffer, expandedLength, info)

View File

@@ -1,7 +1,7 @@
import { Boom } from '@hapi/boom'
import { Logger } from 'pino'
import { proto } from '../../WAProto'
import { NOISE_MODE, NOISE_WA_HEADER, WA_CERT_DETAILS } from '../Defaults'
import { NOISE_MODE, WA_CERT_DETAILS } from '../Defaults'
import { KeyPair } from '../Types'
import { BinaryNode, decodeBinaryNode } from '../WABinary'
import { aesDecryptGCM, aesEncryptGCM, Curve, hkdf, sha256 } from './crypto'
@@ -13,10 +13,17 @@ const generateIV = (counter: number) => {
return new Uint8Array(iv)
}
export const makeNoiseHandler = (
{ public: publicKey, private: privateKey }: KeyPair,
export const makeNoiseHandler = ({
keyPair: { private: privateKey, public: publicKey },
NOISE_HEADER,
mobile,
logger,
}: {
keyPair: KeyPair
NOISE_HEADER: Uint8Array
mobile: boolean
logger: Logger
) => {
}) => {
logger = logger.child({ class: 'ns' })
const authenticate = (data: Uint8Array) => {
@@ -86,7 +93,7 @@ export const makeNoiseHandler = (
let inBytes = Buffer.alloc(0)
authenticate(NOISE_WA_HEADER)
authenticate(NOISE_HEADER)
authenticate(publicKey)
return {
@@ -103,12 +110,18 @@ export const makeNoiseHandler = (
mixIntoKey(Curve.sharedKey(privateKey, decStaticContent))
const certDecoded = decrypt(serverHello!.payload!)
const { intermediate: certIntermediate } = proto.CertChain.decode(certDecoded)
const { issuerSerial } = proto.CertChain.NoiseCertificate.Details.decode(certIntermediate!.details!)
if(mobile) {
const cert = proto.CertChain.NoiseCertificate.decode(certDecoded)
logger.debug(cert)
} else {
const { intermediate: certIntermediate } = proto.CertChain.decode(certDecoded)
if(issuerSerial !== WA_CERT_DETAILS.SERIAL) {
throw new Boom('certification match failed', { statusCode: 400 })
const { issuerSerial } = proto.CertChain.NoiseCertificate.Details.decode(certIntermediate!.details!)
if(issuerSerial !== WA_CERT_DETAILS.SERIAL) {
throw new Boom('certification match failed', { statusCode: 400 })
}
}
const keyEnc = encrypt(noiseKey.public)
@@ -121,11 +134,11 @@ export const makeNoiseHandler = (
data = encrypt(data)
}
const introSize = sentIntro ? 0 : NOISE_WA_HEADER.length
const introSize = sentIntro ? 0 : NOISE_HEADER.length
const frame = Buffer.alloc(introSize + 3 + data.byteLength)
if(!sentIntro) {
frame.set(NOISE_WA_HEADER)
frame.set(NOISE_HEADER)
sentIntro = true
}

View File

@@ -8,26 +8,31 @@ import { Curve, hmacSign } from './crypto'
import { encodeBigEndian } from './generics'
import { createSignalIdentity } from './signal'
type ClientPayloadConfig = Pick<SocketConfig, 'version' | 'browser' | 'syncFullHistory'>
const getUserAgent = (config: SocketConfig): proto.ClientPayload.IUserAgent => {
const osVersion = config.mobile ? '15.3.1' : '0.1'
const version = config.mobile ? [2, 22, 24] : config.version
const device = config.mobile ? 'iPhone_7' : 'Desktop'
const manufacturer = config.mobile ? 'Apple' : ''
const platform = config.mobile ? proto.ClientPayload.UserAgent.Platform.IOS : proto.ClientPayload.UserAgent.Platform.WEB
const phoneId = config.mobile ? { phoneId: config.auth.creds.phoneId } : {}
const getUserAgent = ({ version }: ClientPayloadConfig): proto.ClientPayload.IUserAgent => {
const osVersion = '0.1'
return {
appVersion: {
primary: version[0],
secondary: version[1],
tertiary: version[2],
},
platform: proto.ClientPayload.UserAgent.Platform.WEB,
platform,
releaseChannel: proto.ClientPayload.UserAgent.ReleaseChannel.RELEASE,
mcc: '000',
mnc: '000',
mcc: config.auth.creds.registration?.phoneNumberMobileCountryCode || '000',
mnc: config.auth.creds.registration?.phoneNumberMobileNetworkCode || '000',
osVersion: osVersion,
manufacturer: '',
device: 'Desktop',
manufacturer,
device,
osBuildNumber: osVersion,
localeLanguageIso6391: 'en',
localeCountryIso31661Alpha2: 'US',
...phoneId
}
}
@@ -36,7 +41,7 @@ const PLATFORM_MAP = {
'Windows': proto.ClientPayload.WebInfo.WebSubPlatform.WIN32
}
const getWebInfo = (config: ClientPayloadConfig): proto.ClientPayload.IWebInfo => {
const getWebInfo = (config: SocketConfig): proto.ClientPayload.IWebInfo => {
let webSubPlatform = proto.ClientPayload.WebInfo.WebSubPlatform.WEB_BROWSER
if(config.syncFullHistory && PLATFORM_MAP[config.browser[0]]) {
webSubPlatform = PLATFORM_MAP[config.browser[0]]
@@ -45,16 +50,43 @@ const getWebInfo = (config: ClientPayloadConfig): proto.ClientPayload.IWebInfo =
return { webSubPlatform }
}
const getClientPayload = (config: ClientPayloadConfig): proto.IClientPayload => {
return {
const getClientPayload = (config: SocketConfig) => {
const payload: proto.IClientPayload = {
connectType: proto.ClientPayload.ConnectType.WIFI_UNKNOWN,
connectReason: proto.ClientPayload.ConnectReason.USER_ACTIVATED,
userAgent: getUserAgent(config),
webInfo: getWebInfo(config),
}
if(!config.mobile) {
payload.webInfo = getWebInfo(config)
}
return payload
}
export const generateLoginNode = (userJid: string, config: ClientPayloadConfig): proto.IClientPayload => {
export const generateMobileNode = (config: SocketConfig): proto.IClientPayload => {
if(!config.auth.creds) {
throw new Boom('No registration data found', { data: config })
}
const payload: proto.IClientPayload = {
...getClientPayload(config),
sessionId: Math.floor(Math.random() * 999999999 + 1),
shortConnect: true,
connectAttemptCount: 0,
device: 0,
dnsSource: {
appCached: false,
dnsMethod: proto.ClientPayload.DNSSource.DNSResolutionMethod.SYSTEM,
},
passive: false, // XMPP heartbeat setting (false: server actively pings) (true: client actively pings)
pushName: 'test',
username: Number(`${config.auth.creds.registration.phoneNumberCountryCode}${config.auth.creds.registration.phoneNumberNationalNumber}`),
}
return proto.ClientPayload.fromObject(payload)
}
export const generateLoginNode = (userJid: string, config: SocketConfig): proto.IClientPayload => {
const { user, device } = jidDecode(userJid)!
const payload: proto.IClientPayload = {
...getClientPayload(config),
@@ -67,7 +99,7 @@ export const generateLoginNode = (userJid: string, config: ClientPayloadConfig):
export const generateRegistrationNode = (
{ registrationId, signedPreKey, signedIdentityKey }: SignalCreds,
config: ClientPayloadConfig
config: SocketConfig
) => {
// the app version needs to be md5 hashed
// and passed in