Skip to content
Open
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
113 changes: 113 additions & 0 deletions src/chunk/encrypted-cac.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright 2024 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

import { Binary } from 'cafe-utility'
import { Bytes } from '../utils/bytes'
import { Reference, Span } from '../utils/typed-bytes'
import { calculateChunkAddress } from './bmt'
import { MIN_PAYLOAD_SIZE, MAX_PAYLOAD_SIZE } from './cac'
import { newChunkEncrypter, decryptChunkData, KEY_LENGTH, type Key } from './encryption'

const ENCODER = new TextEncoder()

/**
* Encrypted chunk interface
*
* The reference includes both the chunk address and the encryption key (64 bytes total)
*/
export interface EncryptedChunk {
readonly data: Uint8Array // encrypted span + encrypted data
readonly encryptionKey: Key // 32 bytes
span: Span // original (unencrypted) span
payload: Bytes // encrypted payload
address: Reference // BMT hash of encrypted data
reference: Reference // 64 bytes: address (32) + encryption key (32)
}

/**
* Creates an encrypted content addressed chunk
*
* Process:
* 1. Create chunk with span + payload
* 2. Encrypt the chunk data
* 3. Calculate BMT hash on encrypted data
* 4. Return reference = address + encryption key (64 bytes)
*
* @param payloadBytes the data to be stored in the chunk
*/
export function makeEncryptedContentAddressedChunk(payloadBytes: Uint8Array | string): EncryptedChunk {
if (!(payloadBytes instanceof Uint8Array)) {
payloadBytes = ENCODER.encode(payloadBytes)
}

if (payloadBytes.length < MIN_PAYLOAD_SIZE || payloadBytes.length > MAX_PAYLOAD_SIZE) {
throw new RangeError(
`payload size ${payloadBytes.length} exceeds limits [${MIN_PAYLOAD_SIZE}, ${MAX_PAYLOAD_SIZE}]`,
)
}

// Create the original chunk data (span + payload)
const span = Span.fromBigInt(BigInt(payloadBytes.length))
const chunkData = Binary.concatBytes(span.toUint8Array(), payloadBytes)

// Encrypt the chunk
const encrypter = newChunkEncrypter()
const { key, encryptedSpan, encryptedData } = encrypter.encryptChunk(chunkData)

// Concatenate encrypted span and data
const encryptedChunkData = Binary.concatBytes(encryptedSpan, encryptedData)

// Calculate BMT address on encrypted data
const address = calculateChunkAddress(encryptedChunkData)

// Create 64-byte reference: address (32 bytes) + encryption key (32 bytes)
const reference = new Reference(Binary.concatBytes(address.toUint8Array(), key))

return {
data: encryptedChunkData,
encryptionKey: key,
span,
payload: new Bytes(encryptedChunkData.slice(Span.LENGTH)),
address,
reference,
}
}

/**
* Decrypts an encrypted chunk given the encryption key
*
* @param encryptedChunkData The encrypted chunk data (span + payload)
* @param encryptionKey The 32-byte encryption key
*/
export function decryptEncryptedChunk(encryptedChunkData: Uint8Array, encryptionKey: Key): Uint8Array {
return decryptChunkData(encryptionKey, encryptedChunkData)
}

/**
* Extracts encryption key from a 64-byte encrypted reference
*
* @param reference 64-byte reference (address + key)
*/
export function extractEncryptionKey(reference: Reference): Key {
const refBytes = reference.toUint8Array()
if (refBytes.length !== 64) {
throw new Error(`Invalid encrypted reference length: ${refBytes.length}, expected 64`)
}

return refBytes.slice(32, 64)
}

/**
* Extracts the chunk address from a 64-byte encrypted reference
*
* @param reference 64-byte reference (address + key)
*/
export function extractChunkAddress(reference: Reference): Reference {
const refBytes = reference.toUint8Array()
if (refBytes.length !== 64) {
throw new Error(`Invalid encrypted reference length: ${refBytes.length}, expected 64`)
}

return new Reference(refBytes.slice(0, 32))
}
257 changes: 257 additions & 0 deletions src/chunk/encryption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
// Copyright 2024 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

import { Binary } from 'cafe-utility'

export const KEY_LENGTH = 32
export const REFERENCE_SIZE = 64

export type Key = Uint8Array

export interface Encrypter {
key(): Key
encrypt(data: Uint8Array): Uint8Array
}

export interface Decrypter {
key(): Key
decrypt(data: Uint8Array): Uint8Array
}

export interface EncryptionInterface extends Encrypter, Decrypter {
reset(): void
}

/**
* Core encryption class implementing CTR-mode encryption with Keccak256
* This matches the Go implementation in bee/pkg/encryption/encryption.go
*/
export class Encryption implements EncryptionInterface {
private readonly encryptionKey: Key
private readonly keyLen: number
private readonly padding: number
private index: number
private readonly initCtr: number

constructor(key: Key, padding: number, initCtr: number) {
this.encryptionKey = key
this.keyLen = key.length
this.padding = padding
this.initCtr = initCtr
this.index = 0
}

key(): Key {
return this.encryptionKey
}

/**
* Encrypts data with optional padding
*/
encrypt(data: Uint8Array): Uint8Array {
const length = data.length
let outLength = length
const isFixedPadding = this.padding > 0

if (isFixedPadding) {
if (length > this.padding) {
throw new Error(`data length ${length} longer than padding ${this.padding}`)
}
outLength = this.padding
}

const out = new Uint8Array(outLength)
this.transform(data, out)

return out
}

/**
* Decrypts data (caller must know original length if padding was used)
*/
decrypt(data: Uint8Array): Uint8Array {
const length = data.length

if (this.padding > 0 && length !== this.padding) {
throw new Error(`data length ${length} different than padding ${this.padding}`)
}

const out = new Uint8Array(length)
this.transform(data, out)

return out
}

/**
* Resets the counter - only safe to call after encryption/decryption is completed
*/
reset(): void {
this.index = 0
}

/**
* Transforms data by splitting into key-length segments and encrypting sequentially
*/
private transform(input: Uint8Array, out: Uint8Array): void {
const inLength = input.length

for (let i = 0; i < inLength; i += this.keyLen) {
const l = Math.min(this.keyLen, inLength - i)
this.transcrypt(this.index, input.subarray(i, i + l), out.subarray(i, i + l))
this.index++
}

// Pad the rest if out is longer
if (out.length > inLength) {
pad(out.subarray(inLength))
}
}

/**
* Segment-wise transformation using XOR with Keccak256-derived keys
* Matches the Go implementation's Transcrypt function
*/
private transcrypt(i: number, input: Uint8Array, out: Uint8Array): void {
// First hash: key with counter (initial counter + i)
const ctrBytes = new Uint8Array(4)
const view = new DataView(ctrBytes.buffer)
view.setUint32(0, i + this.initCtr, true) // little-endian

const keyAndCtr = new Uint8Array(this.encryptionKey.length + 4)
keyAndCtr.set(this.encryptionKey)
keyAndCtr.set(ctrBytes, this.encryptionKey.length)
const ctrHash = Binary.keccak256(keyAndCtr)

// Second round of hashing for selective disclosure
const segmentKey = Binary.keccak256(ctrHash)

// XOR bytes up to length of input (out must be at least as long)
const inLength = input.length
for (let j = 0; j < inLength; j++) {
out[j] = input[j] ^ segmentKey[j]
}

// Insert padding if out is longer
if (out.length > inLength) {
pad(out.subarray(inLength))
}
}
}

/**
* Fills buffer with cryptographically secure random data
* Works in both browser (Web Crypto API) and Node.js environments
*/
function getRandomValues(buffer: Uint8Array): void {
if (buffer.length === 0) return

// Use Web Crypto API for secure random bytes
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
crypto.getRandomValues(buffer)
} else {
// Fallback for Node.js
// eslint-disable-next-line @typescript-eslint/no-var-requires
const nodeCrypto = require('crypto')
const randomBytes = nodeCrypto.randomBytes(buffer.length)
buffer.set(randomBytes)
}
}

/**
* Fills buffer with cryptographically secure random data
*/
function pad(buffer: Uint8Array): void {
getRandomValues(buffer)
}

/**
* Generates a cryptographically secure random key
*/
export function generateRandomKey(length: number = KEY_LENGTH): Key {
const key = new Uint8Array(length)
getRandomValues(key)

return key
}

/**
* Creates encryption interface for chunk span (first 8 bytes)
*/
export function newSpanEncryption(key: Key): EncryptionInterface {
// ChunkSize is typically 4096, so ChunkSize/KeyLength = 128
const CHUNK_SIZE = 4096

return new Encryption(key, 0, Math.floor(CHUNK_SIZE / KEY_LENGTH))
}

/**
* Creates encryption interface for chunk data
*/
export function newDataEncryption(key: Key): EncryptionInterface {
const CHUNK_SIZE = 4096

return new Encryption(key, CHUNK_SIZE, 0)
}

export interface ChunkEncrypter {
encryptChunk(chunkData: Uint8Array): {
key: Key
encryptedSpan: Uint8Array
encryptedData: Uint8Array
}
}

/**
* Default chunk encrypter implementation
*/
export class DefaultChunkEncrypter implements ChunkEncrypter {
encryptChunk(chunkData: Uint8Array): {
key: Key
encryptedSpan: Uint8Array
encryptedData: Uint8Array
} {
const key = generateRandomKey(KEY_LENGTH)

// Encrypt span (first 8 bytes)
const spanEncrypter = newSpanEncryption(key)
const encryptedSpan = spanEncrypter.encrypt(chunkData.subarray(0, 8))

// Encrypt data (remaining bytes)
const dataEncrypter = newDataEncryption(key)
const encryptedData = dataEncrypter.encrypt(chunkData.subarray(8))

return {
key,
encryptedSpan,
encryptedData,
}
}
}

/**
* Creates a new chunk encrypter
*/
export function newChunkEncrypter(): ChunkEncrypter {
return new DefaultChunkEncrypter()
}

/**
* Decrypts encrypted chunk data using the provided encryption key
*/
export function decryptChunkData(key: Key, encryptedChunkData: Uint8Array): Uint8Array {
// Decrypt span (first 8 bytes)
const spanDecrypter = newSpanEncryption(key)
const decryptedSpan = spanDecrypter.decrypt(encryptedChunkData.subarray(0, 8))

// Decrypt data (remaining bytes - should be 4096 bytes due to padding)
const dataDecrypter = newDataEncryption(key)
const decryptedData = dataDecrypter.decrypt(encryptedChunkData.subarray(8))

// Concatenate span and data
const result = new Uint8Array(8 + decryptedData.length)
result.set(decryptedSpan)
result.set(decryptedData, 8)

return result
}
Loading