Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 10 additions & 31 deletions app/checkout/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,41 +30,20 @@ import { savedAddressService } from '@/lib/services/saved-address-service'
import { hasEncryptionKey, getEncryptionKeyBytes } from '@/lib/secure-storage'
import { useEncryptionKeyModal } from '@/hooks/use-encryption-key-modal'
import type { Store, CartItem, ShippingAddress, BuyerContact, ParsedPaymentUri, ShippingZone, StorePolicy, SavedAddress } from '@/lib/types'
import { normalizeBytes } from '@/lib/bytes'

/**
* Normalize key data from various formats to Uint8Array
* The seller's encryption public key as bytes, or null if the identity key
* data does not decode to a secp256k1 point (33-byte compressed or 65-byte
* uncompressed).
*/
function normalizeKeyData(data: unknown): Uint8Array | null {
if (!data) return null
if (data instanceof Uint8Array) return data
if (Array.isArray(data)) return new Uint8Array(data)
if (typeof data === 'string') {
const isValidSecpPublicKey = (bytes: Uint8Array) =>
(bytes.length === 33 && (bytes[0] === 0x02 || bytes[0] === 0x03)) ||
(bytes.length === 65 && bytes[0] === 0x04)

// Hex (common for stored keys)
if (/^[0-9a-fA-F]+$/.test(data) && (data.length === 66 || data.length === 130)) {
const bytes = new Uint8Array(data.length / 2)
for (let i = 0; i < bytes.length; i++) {
const byte = parseInt(data.substr(i * 2, 2), 16)
if (Number.isNaN(byte)) return null
bytes[i] = byte
}
if (isValidSecpPublicKey(bytes)) return bytes
}

// Base64
try {
const binaryString = atob(data)
const bytes = Uint8Array.from(binaryString, (c) => c.charCodeAt(0))
if (isValidSecpPublicKey(bytes)) return bytes
return null
} catch {
return null
}
}
return null
const bytes = normalizeBytes(data)
if (!bytes) return null
const isSecpPoint =
(bytes.length === 33 && (bytes[0] === 0x02 || bytes[0] === 0x03)) ||
(bytes.length === 65 && bytes[0] === 0x04)
return isSecpPoint ? bytes : null
}

type CheckoutReadinessBlocker =
Expand Down
21 changes: 2 additions & 19 deletions app/orders/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,7 @@ import { identityService } from '@/lib/services/identity-service'
import { findEncryptionKey } from '@/lib/crypto/encryption-key-lookup'
import { getEncryptionKeyBytes } from '@/lib/secure-storage'
import type { StoreOrder, OrderStatusUpdate, Store, OrderPayload } from '@/lib/types'

/**
* Normalize key data from various formats to Uint8Array
*/
function normalizeKeyData(data: unknown): Uint8Array | null {
if (!data) return null
if (data instanceof Uint8Array) return data
if (Array.isArray(data)) return new Uint8Array(data)
if (typeof data === 'string') {
try {
const binaryString = atob(data)
return Uint8Array.from(binaryString, (c) => c.charCodeAt(0))
} catch {
return null
}
}
return null
}
import { normalizeBytes } from '@/lib/bytes'

function OrdersPage() {
const router = useRouter()
Expand Down Expand Up @@ -130,7 +113,7 @@ function OrdersPage() {
const sellerIdentity = await identityService.getIdentity(order.sellerId)
const sellerEncryptionKey = sellerIdentity ? findEncryptionKey(sellerIdentity.publicKeys) : undefined
const sellerPubKey = sellerEncryptionKey?.data
? normalizeKeyData(sellerEncryptionKey.data)
? normalizeBytes(sellerEncryptionKey.data)
: null

// Skip decryption if seller public key is missing
Expand Down
11 changes: 2 additions & 9 deletions components/dpns/registration-wizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { ReviewStep } from './steps/review-step'
import { RegisteringStep } from './steps/registering-step'
import { CompleteStep } from './steps/complete-step'
import { keyNetwork } from '@/lib/constants'
import { normalizeBytes } from '@/lib/bytes'

interface DpnsRegistrationWizardProps {
onComplete?: () => void
Expand All @@ -31,15 +32,7 @@ interface DpnsRegistrationWizardProps {
*/
function convertToKeyInfo(keys: IdentityPublicKey[]): IdentityPublicKeyInfo[] {
return keys.map((key) => {
let data: Uint8Array
if (key.data instanceof Uint8Array) {
data = key.data
} else if (typeof key.data === 'string') {
// Base64 decode
data = Uint8Array.from(atob(key.data), (c) => c.charCodeAt(0))
} else {
data = new Uint8Array()
}
const data = normalizeBytes(key.data) ?? new Uint8Array()
return {
id: key.id,
type: key.type,
Expand Down
34 changes: 3 additions & 31 deletions components/settings/private-feed-follow-requests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Link from 'next/link'
import { formatTime } from '@/lib/utils'
import { usePrivateFeedRefreshStore } from '@/lib/stores/private-feed-refresh-store'
import { resolveUserDetails, type UserDetails } from '@/lib/utils/resolve-user-details'
import { normalizeBytes } from '@/lib/bytes'

interface FollowRequestUser extends UserDetails {
requestId: string
Expand Down Expand Up @@ -89,39 +90,10 @@ export function PrivateFeedFollowRequests() {
try {
const { privateFeedService, identityService } = await import('@/lib/services')

// Helper to normalize key data to Uint8Array
const normalizeKeyData = (data: unknown): Uint8Array | null => {
if (!data) return null
if (data instanceof Uint8Array) return data
if (Array.isArray(data)) return new Uint8Array(data)
if (typeof data === 'string') {
// Use length to differentiate hex vs base64:
// 33-byte key: hex = 66 chars, base64 = 44 chars
const isLikelyHex = data.length === 66 && /^[0-9a-fA-F]+$/.test(data)
if (isLikelyHex) {
const hexPairs = data.match(/.{1,2}/g) || []
return new Uint8Array(hexPairs.map(byte => parseInt(byte, 16)))
}
// Try base64
try {
const binary = atob(data)
const bytes = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i)
}
return bytes
} catch {
logger.warn('Invalid base64 encoding for key data')
return null
}
}
return null
}

// First normalize the request.publicKey if it exists but isn't a Uint8Array
let publicKey: Uint8Array | undefined = undefined
if (request.publicKey) {
const normalized = normalizeKeyData(request.publicKey)
const normalized = normalizeBytes(request.publicKey)
if (normalized) {
publicKey = normalized
}
Expand All @@ -134,7 +106,7 @@ export function PrivateFeedFollowRequests() {
if (identity?.publicKeys) {
const encryptionKey = findEncryptionKey(identity.publicKeys)
if (encryptionKey?.data) {
const normalized = normalizeKeyData(encryptionKey.data)
const normalized = normalizeBytes(encryptionKey.data)
if (normalized) {
publicKey = normalized
}
Expand Down
12 changes: 2 additions & 10 deletions contexts/auth-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { logger } from '@/lib/logger'
import { scopedKey } from '@/lib/storage-scope'
import { base64ToBytes } from '@/lib/bytes'
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { Spinner } from '@/components/ui/spinner'
import { useRouter } from 'next/navigation'
Expand Down Expand Up @@ -100,15 +101,6 @@ function toFriendlyVaultWriteError(error: unknown, methodLabel: 'passkey' | 'pas
return error instanceof Error ? error : new Error(message)
}

function decodeBase64ToBytes(value: string): Uint8Array {
const binary = atob(value)
const bytes = new Uint8Array(binary.length)
for (let index = 0; index < binary.length; index += 1) {
bytes[index] = binary.charCodeAt(index)
}
return bytes
}

export function AuthProvider({ children }: { children: React.ReactNode }) {
const router = useRouter()
const controller = useMemo(() => new PlatformAuthController(createYapprPlatformAuthDependencies()), [])
Expand Down Expand Up @@ -186,7 +178,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
loginKey: partialSecrets.loginKey
? partialSecrets.loginKey instanceof Uint8Array
? partialSecrets.loginKey
: decodeBase64ToBytes(partialSecrets.loginKey)
: base64ToBytes(partialSecrets.loginKey)
: undefined,
authKeyWif: partialSecrets.authKeyWif,
encryptionKeyWif: partialSecrets.encryptionKeyWif,
Expand Down
41 changes: 9 additions & 32 deletions hooks/use-private-feed-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
subscribeToPrivateFeedRequestStatus,
type PrivateFeedRequestStatus as CacheStatus,
} from '@/lib/caches/user-status-cache'
import { normalizeBytes } from '@/lib/bytes'
import { getPublicKey } from '@/lib/crypto/keys'

export type PrivateFeedRequestStatus = 'none' | 'pending' | 'loading' | 'error'

Expand Down Expand Up @@ -163,7 +165,7 @@ export function usePrivateFeedRequest({
updateStatus('loading')

try {
const { privateFeedFollowerService, privateFeedCryptoService, identityService } = await import('@/lib/services')
const { privateFeedFollowerService, identityService } = await import('@/lib/services')
const { followService } = await import('@/lib/services/follow-service')
const { getEncryptionKeyBytes } = await import('@/lib/secure-storage')

Expand All @@ -174,42 +176,17 @@ export function usePrivateFeedRequest({
const privateKeyBytes = getEncryptionKeyBytes(currentUserId)
if (privateKeyBytes) {
// Derive public key from stored private key
encryptionPublicKey = privateFeedCryptoService.getPublicKey(privateKeyBytes)
encryptionPublicKey = getPublicKey(privateKeyBytes)
} else {
// Try to get from identity
const { findEncryptionKey } = await import('@/lib/crypto/encryption-key-lookup')
const identity = await identityService.getIdentity(currentUserId)
if (identity?.publicKeys) {
const encryptionKey = findEncryptionKey(identity.publicKeys)
if (encryptionKey?.data) {
// Convert to Uint8Array
if (typeof encryptionKey.data === 'string') {
const keyStr = encryptionKey.data
// Use length to differentiate hex vs base64:
// 33-byte key: hex = 66 chars, base64 = 44 chars
const isLikelyHex = keyStr.length === 66 && /^[0-9a-fA-F]+$/.test(keyStr)

if (isLikelyHex) {
const hexPairs = keyStr.match(/.{1,2}/g) || []
encryptionPublicKey = new Uint8Array(
hexPairs.map(byte => parseInt(byte, 16))
)
} else {
// Try base64 decode
try {
const binary = atob(keyStr)
encryptionPublicKey = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i++) {
encryptionPublicKey[i] = binary.charCodeAt(i)
}
} catch {
logger.error('Failed to decode encryption key as base64:', keyStr.substring(0, 20) + '...')
}
}
} else if (encryptionKey.data instanceof Uint8Array) {
encryptionPublicKey = encryptionKey.data
} else if (Array.isArray(encryptionKey.data)) {
encryptionPublicKey = new Uint8Array(encryptionKey.data)
encryptionPublicKey = normalizeBytes(encryptionKey.data) ?? undefined
if (!encryptionPublicKey) {
logger.error('Failed to decode encryption key data on identity')
}
}
}
Expand Down Expand Up @@ -280,7 +257,7 @@ export function usePrivateFeedRequest({
updateStatus('loading')

try {
const { privateFeedFollowerService, privateFeedCryptoService } = await import('@/lib/services')
const { privateFeedFollowerService } = await import('@/lib/services')
const { followService } = await import('@/lib/services/follow-service')
const { getEncryptionKeyBytes } = await import('@/lib/secure-storage')

Expand All @@ -294,7 +271,7 @@ export function usePrivateFeedRequest({
}

// Derive public key from stored private key
const encryptionPublicKey = privateFeedCryptoService.getPublicKey(privateKeyBytes)
const encryptionPublicKey = getPublicKey(privateKeyBytes)

// Auto-follow the owner if not already following
const isFollowing = await followService.isFollowing(ownerId, currentUserId)
Expand Down
13 changes: 3 additions & 10 deletions lib/auth/platform-auth-adapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ import {
getPrfAssertionForCredentials,
selectDiscoverablePasskey,
} from '@/lib/webauthn/passkey-prf'
import { decodeBinaryFromBase64, wrapDekWithPassword, wrapDekWithPrf } from '@/lib/crypto/auth-vault'
import { wrapDekWithPassword, wrapDekWithPrf } from '@/lib/crypto/auth-vault'
import { base64ToBytes, bytesToBase64 } from '@/lib/bytes'
import { deriveEncryptionKey, validateDerivedKeyMatchesIdentity } from '@/lib/crypto/key-derivation'
import { hasEncryptionKeyOnIdentity } from '@/lib/crypto/encryption-key-lookup'
import { parsePrivateKey, privateKeyToWif } from '@/lib/crypto/wif'
Expand Down Expand Up @@ -87,14 +88,6 @@ async function ensureSdk(): Promise<void> {
})
}

function bytesToBase64(bytes: Uint8Array): string {
let binary = ''
for (let index = 0; index < bytes.length; index += 1) {
binary += String.fromCharCode(bytes[index])
}
return btoa(binary)
}

function fromSessionUser(savedUser: Record<string, unknown>): AuthUser | null {
const identityId = typeof savedUser.identityId === 'string' ? savedUser.identityId : null
if (!identityId) return null
Expand Down Expand Up @@ -136,7 +129,7 @@ function fromLegacyBundle(bundle: LegacyAuthVaultBundle): AuthVaultBundle {
identityId: bundle.identityId,
network: bundle.network,
secretKind: bundle.secretKind,
loginKey: bundle.loginKey ? decodeBinaryFromBase64(bundle.loginKey) : undefined,
loginKey: bundle.loginKey ? base64ToBytes(bundle.loginKey) : undefined,
authKeyWif: bundle.authKeyWif,
encryptionKeyWif: bundle.encryptionKeyWif,
transferKeyWif: bundle.transferKeyWif,
Expand Down
16 changes: 3 additions & 13 deletions lib/bloom-filter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { sha256 } from '@noble/hashes/sha2.js'
import bs58 from 'bs58'
import { base64ToBytes, bytesToBase64 } from './bytes'

// 5KB = 5000 bytes = 40,000 bits
const FILTER_SIZE_BYTES = 5000
Expand Down Expand Up @@ -176,23 +177,12 @@ export class BloomFilter {
* Convert a Uint8Array to base64 string for sessionStorage.
*/
export function bloomFilterToBase64(filter: BloomFilter): string {
const bytes = filter.serialize()
// Use btoa with binary string conversion
let binary = ''
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i])
}
return btoa(binary)
return bytesToBase64(filter.serialize())
}

/**
* Create a BloomFilter from a base64 string.
*/
export function bloomFilterFromBase64(base64: string, itemCount: number = 0): BloomFilter {
const binary = atob(base64)
const bytes = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i)
}
return new BloomFilter(bytes, itemCount)
return new BloomFilter(base64ToBytes(base64), itemCount)
}
Loading
Loading