mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 02:25:20 +00:00
NIP-17/44: adopt NIP-44 v2 (XChaCha20-Poly1305, v2: base64url), switch rumor kind to 14, randomize BIP-340 aux; add XChaCha20Poly1305Compat and wire-up
This commit is contained in:
@@ -140,6 +140,8 @@
|
|||||||
F455F011B3B648ADA233F998 /* BinaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */; };
|
F455F011B3B648ADA233F998 /* BinaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */; };
|
||||||
FB8819B4C84FAFEF5C36B216 /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136696FC4436A02D98CE6A77 /* KeychainManager.swift */; };
|
FB8819B4C84FAFEF5C36B216 /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136696FC4436A02D98CE6A77 /* KeychainManager.swift */; };
|
||||||
FBC409E105493C491531B59A /* NostrProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5A9FF4AEA8A923317ED26A /* NostrProtocol.swift */; };
|
FBC409E105493C491531B59A /* NostrProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5A9FF4AEA8A923317ED26A /* NostrProtocol.swift */; };
|
||||||
|
A1B2C3D44E5F60718293A4B5 /* XChaCha20Poly1305Compat.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D44E5F60718293A4B4 /* XChaCha20Poly1305Compat.swift */; };
|
||||||
|
A1B2C3D54E5F60718293A4B6 /* XChaCha20Poly1305Compat.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D44E5F60718293A4B4 /* XChaCha20Poly1305Compat.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -258,6 +260,7 @@
|
|||||||
FDC18D910D6FF2E8B1B6C885 /* SecureIdentityStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdentityStateManager.swift; sourceTree = "<group>"; };
|
FDC18D910D6FF2E8B1B6C885 /* SecureIdentityStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdentityStateManager.swift; sourceTree = "<group>"; };
|
||||||
FE7CCF2BD78A3F3DAE6DA145 /* MockBLEService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBLEService.swift; sourceTree = "<group>"; };
|
FE7CCF2BD78A3F3DAE6DA145 /* MockBLEService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBLEService.swift; sourceTree = "<group>"; };
|
||||||
FF7AF93D874001FBD94C8306 /* bitchat-macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "bitchat-macOS.entitlements"; sourceTree = "<group>"; };
|
FF7AF93D874001FBD94C8306 /* bitchat-macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "bitchat-macOS.entitlements"; sourceTree = "<group>"; };
|
||||||
|
A1B2C3D44E5F60718293A4B4 /* XChaCha20Poly1305Compat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XChaCha20Poly1305Compat.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -522,6 +525,7 @@
|
|||||||
E78C7F4B6769C0A72F5DE544 /* Nostr */ = {
|
E78C7F4B6769C0A72F5DE544 /* Nostr */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
A1B2C3D44E5F60718293A4B4 /* XChaCha20Poly1305Compat.swift */,
|
||||||
049BD39B2E51DBD9001A566B /* NostrEmbeddedBitChat.swift */,
|
049BD39B2E51DBD9001A566B /* NostrEmbeddedBitChat.swift */,
|
||||||
5F8043995007F0D84438EDD9 /* NostrIdentity.swift */,
|
5F8043995007F0D84438EDD9 /* NostrIdentity.swift */,
|
||||||
2E5A9FF4AEA8A923317ED26A /* NostrProtocol.swift */,
|
2E5A9FF4AEA8A923317ED26A /* NostrProtocol.swift */,
|
||||||
@@ -720,6 +724,7 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
A1B2C3D54E5F60718293A4B6 /* XChaCha20Poly1305Compat.swift in Sources */,
|
||||||
AD11E46940D742AEAF547EB2 /* AppInfoView.swift in Sources */,
|
AD11E46940D742AEAF547EB2 /* AppInfoView.swift in Sources */,
|
||||||
9B51E9B63A3EA59B1A7874BD /* BinaryEncodingUtils.swift in Sources */,
|
9B51E9B63A3EA59B1A7874BD /* BinaryEncodingUtils.swift in Sources */,
|
||||||
049BD3B42E51F319001A566B /* NostrTransport.swift in Sources */,
|
049BD3B42E51F319001A566B /* NostrTransport.swift in Sources */,
|
||||||
@@ -774,6 +779,7 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
A1B2C3D44E5F60718293A4B5 /* XChaCha20Poly1305Compat.swift in Sources */,
|
||||||
ABAF130D88561F4A646F0430 /* AppInfoView.swift in Sources */,
|
ABAF130D88561F4A646F0430 /* AppInfoView.swift in Sources */,
|
||||||
AFB6AEFCABBE97441CB3102B /* BinaryEncodingUtils.swift in Sources */,
|
AFB6AEFCABBE97441CB3102B /* BinaryEncodingUtils.swift in Sources */,
|
||||||
049BD3B22E51F319001A566B /* NostrTransport.swift in Sources */,
|
049BD3B22E51F319001A566B /* NostrTransport.swift in Sources */,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
import P256K
|
import P256K
|
||||||
|
import Security
|
||||||
|
|
||||||
// Note: This file depends on Data extension from BinaryEncodingUtils.swift
|
// Note: This file depends on Data extension from BinaryEncodingUtils.swift
|
||||||
// Make sure BinaryEncodingUtils.swift is included in the target
|
// Make sure BinaryEncodingUtils.swift is included in the target
|
||||||
@@ -12,8 +13,9 @@ struct NostrProtocol {
|
|||||||
enum EventKind: Int {
|
enum EventKind: Int {
|
||||||
case metadata = 0
|
case metadata = 0
|
||||||
case textNote = 1
|
case textNote = 1
|
||||||
|
case dm = 14 // NIP-17 DM rumor kind
|
||||||
case seal = 13 // NIP-17 sealed event
|
case seal = 13 // NIP-17 sealed event
|
||||||
case giftWrap = 1059 // NIP-17 gift wrap
|
case giftWrap = 1059 // NIP-59 gift wrap
|
||||||
case ephemeralEvent = 20000
|
case ephemeralEvent = 20000
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +32,7 @@ struct NostrProtocol {
|
|||||||
let rumor = NostrEvent(
|
let rumor = NostrEvent(
|
||||||
pubkey: senderIdentity.publicKeyHex,
|
pubkey: senderIdentity.publicKeyHex,
|
||||||
createdAt: Date(),
|
createdAt: Date(),
|
||||||
kind: .textNote,
|
kind: .dm, // NIP-17: DM rumor kind 14
|
||||||
tags: [],
|
tags: [],
|
||||||
content: content
|
content: content
|
||||||
)
|
)
|
||||||
@@ -227,7 +229,7 @@ struct NostrProtocol {
|
|||||||
return try NostrEvent(from: rumorDict)
|
return try NostrEvent(from: rumorDict)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Encryption (NIP-44 style)
|
// MARK: - Encryption (NIP-44 v2)
|
||||||
|
|
||||||
private static func encrypt(
|
private static func encrypt(
|
||||||
plaintext: String,
|
plaintext: String,
|
||||||
@@ -239,33 +241,31 @@ struct NostrProtocol {
|
|||||||
throw NostrError.invalidPublicKey
|
throw NostrError.invalidPublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encrypting message
|
// Encrypting message (NIP-44 v2: XChaCha20-Poly1305, versioned)
|
||||||
|
|
||||||
// Derive shared secret
|
// Derive shared secret
|
||||||
let sharedSecret = try deriveSharedSecret(
|
let sharedSecret = try deriveSharedSecret(
|
||||||
privateKey: senderKey,
|
privateKey: senderKey,
|
||||||
publicKey: recipientPubkeyData
|
publicKey: recipientPubkeyData
|
||||||
)
|
)
|
||||||
|
// Derive NIP-44 v2 symmetric key (HKDF-SHA256 with label in info)
|
||||||
|
let key = try deriveNIP44V2Key(from: sharedSecret)
|
||||||
|
|
||||||
// Derived shared secret
|
// 24-byte random nonce for XChaCha20-Poly1305
|
||||||
|
var nonce24 = Data(count: 24)
|
||||||
|
_ = nonce24.withUnsafeMutableBytes { ptr in
|
||||||
|
SecRandomCopyBytes(kSecRandomDefault, 24, ptr.baseAddress!)
|
||||||
|
}
|
||||||
|
|
||||||
// Generate nonce
|
let pt = Data(plaintext.utf8)
|
||||||
let nonce = AES.GCM.Nonce()
|
let sealed = try XChaCha20Poly1305Compat.seal(plaintext: pt, key: key, nonce24: nonce24)
|
||||||
|
|
||||||
// Encrypt
|
// v2: base64url(nonce24 || ciphertext || tag)
|
||||||
let sealed = try AES.GCM.seal(
|
var combined = Data()
|
||||||
plaintext.data(using: .utf8)!,
|
combined.append(nonce24)
|
||||||
using: SymmetricKey(data: sharedSecret),
|
combined.append(sealed.ciphertext)
|
||||||
nonce: nonce
|
combined.append(sealed.tag)
|
||||||
)
|
return "v2:" + base64URLEncode(combined)
|
||||||
|
|
||||||
// Combine nonce + ciphertext + tag
|
|
||||||
var result = Data()
|
|
||||||
result.append(nonce.withUnsafeBytes { Data($0) })
|
|
||||||
result.append(sealed.ciphertext)
|
|
||||||
result.append(sealed.tag)
|
|
||||||
|
|
||||||
return result.base64EncodedString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func decrypt(
|
private static func decrypt(
|
||||||
@@ -273,86 +273,33 @@ struct NostrProtocol {
|
|||||||
senderPubkey: String,
|
senderPubkey: String,
|
||||||
recipientKey: P256K.Schnorr.PrivateKey
|
recipientKey: P256K.Schnorr.PrivateKey
|
||||||
) throws -> String {
|
) throws -> String {
|
||||||
|
// Expect NIP-44 v2 format
|
||||||
// Decrypting message
|
guard ciphertext.hasPrefix("v2:") else { throw NostrError.invalidCiphertext }
|
||||||
|
let encoded = String(ciphertext.dropFirst(3))
|
||||||
guard let data = Data(base64Encoded: ciphertext),
|
guard let data = base64URLDecode(encoded),
|
||||||
|
data.count > (24 + 16),
|
||||||
let senderPubkeyData = Data(hexString: senderPubkey) else {
|
let senderPubkeyData = Data(hexString: senderPubkey) else {
|
||||||
SecureLogger.log("❌ Invalid ciphertext or sender pubkey format",
|
|
||||||
category: SecureLogger.session, level: .error)
|
|
||||||
throw NostrError.invalidCiphertext
|
throw NostrError.invalidCiphertext
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ciphertext data parsed
|
let nonce24 = data.prefix(24)
|
||||||
|
let rest = data.dropFirst(24)
|
||||||
// Extract components
|
let tag = rest.suffix(16)
|
||||||
let nonceData = data.prefix(12)
|
let ct = rest.dropLast(16)
|
||||||
let ciphertextData = data.dropFirst(12).dropLast(16)
|
|
||||||
let tagData = data.suffix(16)
|
// Derive shared secret (try even then odd Y)
|
||||||
|
|
||||||
// Components parsed
|
|
||||||
|
|
||||||
// Derive shared secret - try with default Y coordinate first
|
|
||||||
var sharedSecret: Data
|
var sharedSecret: Data
|
||||||
var decrypted: Data? = nil
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
sharedSecret = try deriveSharedSecret(
|
sharedSecret = try deriveSharedSecret(privateKey: recipientKey, publicKey: senderPubkeyData)
|
||||||
privateKey: recipientKey,
|
|
||||||
publicKey: senderPubkeyData
|
|
||||||
)
|
|
||||||
// Derived shared secret with first Y coordinate
|
|
||||||
|
|
||||||
// Try to decrypt
|
|
||||||
let sealedBox = try AES.GCM.SealedBox(
|
|
||||||
nonce: AES.GCM.Nonce(data: nonceData),
|
|
||||||
ciphertext: ciphertextData,
|
|
||||||
tag: tagData
|
|
||||||
)
|
|
||||||
|
|
||||||
do {
|
|
||||||
decrypted = try AES.GCM.open(
|
|
||||||
sealedBox,
|
|
||||||
using: SymmetricKey(data: sharedSecret)
|
|
||||||
)
|
|
||||||
// AES-GCM decryption successful
|
|
||||||
} catch {
|
|
||||||
// AES-GCM decryption failed, trying alternate
|
|
||||||
|
|
||||||
// If the sender pubkey is x-only (32 bytes), try the other Y coordinate
|
|
||||||
if senderPubkeyData.count == 32 {
|
|
||||||
// Trying alternate Y coordinate
|
|
||||||
|
|
||||||
// Force deriveSharedSecret to use odd Y by manipulating the data
|
|
||||||
var altPubkey = Data()
|
|
||||||
altPubkey.append(0x03) // Force odd Y
|
|
||||||
altPubkey.append(senderPubkeyData)
|
|
||||||
|
|
||||||
sharedSecret = try deriveSharedSecretDirect(
|
|
||||||
privateKey: recipientKey,
|
|
||||||
publicKey: altPubkey
|
|
||||||
)
|
|
||||||
|
|
||||||
decrypted = try AES.GCM.open(
|
|
||||||
sealedBox,
|
|
||||||
using: SymmetricKey(data: sharedSecret)
|
|
||||||
)
|
|
||||||
// AES-GCM decryption successful with alternate Y
|
|
||||||
} else {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
SecureLogger.log("❌ Failed to derive shared secret or decrypt: \(error)",
|
guard senderPubkeyData.count == 32 else { throw error }
|
||||||
category: SecureLogger.session, level: .error)
|
var alt = Data([0x03])
|
||||||
throw error
|
alt.append(senderPubkeyData)
|
||||||
|
sharedSecret = try deriveSharedSecretDirect(privateKey: recipientKey, publicKey: alt)
|
||||||
}
|
}
|
||||||
|
let key = try deriveNIP44V2Key(from: sharedSecret)
|
||||||
guard let finalDecrypted = decrypted else {
|
let pt = try XChaCha20Poly1305Compat.open(ciphertext: Data(ct), tag: Data(tag), key: key, nonce24: Data(nonce24))
|
||||||
throw NostrError.encryptionFailed
|
return String(data: pt, encoding: .utf8) ?? ""
|
||||||
}
|
|
||||||
|
|
||||||
return String(data: finalDecrypted, encoding: .utf8) ?? ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func deriveSharedSecret(
|
private static func deriveSharedSecret(
|
||||||
@@ -412,17 +359,8 @@ struct NostrProtocol {
|
|||||||
let sharedSecretData = sharedSecret.withUnsafeBytes { Data($0) }
|
let sharedSecretData = sharedSecret.withUnsafeBytes { Data($0) }
|
||||||
// ECDH shared secret derived
|
// ECDH shared secret derived
|
||||||
|
|
||||||
// Derive key using HKDF for NIP-44 v2
|
// Return raw ECDH shared secret; HKDF is applied by deriveNIP44V2Key
|
||||||
let derivedKey = HKDF<CryptoKit.SHA256>.deriveKey(
|
return sharedSecretData
|
||||||
inputKeyMaterial: SymmetricKey(data: sharedSecretData),
|
|
||||||
salt: "nip44-v2".data(using: .utf8)!,
|
|
||||||
info: Data(),
|
|
||||||
outputByteCount: 32
|
|
||||||
)
|
|
||||||
|
|
||||||
let result = derivedKey.withUnsafeBytes { Data($0) }
|
|
||||||
// Final derived key ready
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Direct version that doesn't try to add prefixes
|
// Direct version that doesn't try to add prefixes
|
||||||
@@ -452,15 +390,8 @@ struct NostrProtocol {
|
|||||||
// Convert SharedSecret to Data
|
// Convert SharedSecret to Data
|
||||||
let sharedSecretData = sharedSecret.withUnsafeBytes { Data($0) }
|
let sharedSecretData = sharedSecret.withUnsafeBytes { Data($0) }
|
||||||
|
|
||||||
// Derive key using HKDF for NIP-44 v2
|
// Return raw ECDH shared secret; HKDF is applied by deriveNIP44V2Key
|
||||||
let derivedKey = HKDF<CryptoKit.SHA256>.deriveKey(
|
return sharedSecretData
|
||||||
inputKeyMaterial: SymmetricKey(data: sharedSecretData),
|
|
||||||
salt: "nip44-v2".data(using: .utf8)!,
|
|
||||||
info: Data(),
|
|
||||||
outputByteCount: 32
|
|
||||||
)
|
|
||||||
|
|
||||||
return derivedKey.withUnsafeBytes { Data($0) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func randomizedTimestamp() -> Date {
|
private static func randomizedTimestamp() -> Date {
|
||||||
@@ -537,7 +468,10 @@ struct NostrEvent: Codable {
|
|||||||
|
|
||||||
// Sign with Schnorr
|
// Sign with Schnorr
|
||||||
var messageBytes = [UInt8](eventIdHash)
|
var messageBytes = [UInt8](eventIdHash)
|
||||||
var auxRand = [UInt8](repeating: 0, count: 32) // Zero auxiliary randomness for deterministic signing
|
var auxRand = [UInt8](repeating: 0, count: 32)
|
||||||
|
_ = auxRand.withUnsafeMutableBytes { ptr in
|
||||||
|
SecRandomCopyBytes(kSecRandomDefault, 32, ptr.baseAddress!)
|
||||||
|
}
|
||||||
let schnorrSignature = try schnorrKey.signature(message: &messageBytes, auxiliaryRand: &auxRand)
|
let schnorrSignature = try schnorrKey.signature(message: &messageBytes, auxiliaryRand: &auxRand)
|
||||||
|
|
||||||
let signatureHex = schnorrSignature.dataRepresentation.hexEncodedString()
|
let signatureHex = schnorrSignature.dataRepresentation.hexEncodedString()
|
||||||
@@ -581,3 +515,32 @@ enum NostrError: Error {
|
|||||||
case signingFailed
|
case signingFailed
|
||||||
case encryptionFailed
|
case encryptionFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - NIP-44 v2 helpers (XChaCha20-Poly1305 + base64url)
|
||||||
|
|
||||||
|
private extension NostrProtocol {
|
||||||
|
static func base64URLEncode(_ data: Data) -> String {
|
||||||
|
return data.base64EncodedString()
|
||||||
|
.replacingOccurrences(of: "+", with: "-")
|
||||||
|
.replacingOccurrences(of: "/", with: "_")
|
||||||
|
.replacingOccurrences(of: "=", with: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
static func base64URLDecode(_ s: String) -> Data? {
|
||||||
|
var str = s
|
||||||
|
let pad = (4 - (str.count % 4)) % 4
|
||||||
|
if pad > 0 { str += String(repeating: "=", count: pad) }
|
||||||
|
str = str.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
|
||||||
|
return Data(base64Encoded: str)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func deriveNIP44V2Key(from sharedSecretData: Data) throws -> Data {
|
||||||
|
let derivedKey = HKDF<CryptoKit.SHA256>.deriveKey(
|
||||||
|
inputKeyMaterial: SymmetricKey(data: sharedSecretData),
|
||||||
|
salt: Data(),
|
||||||
|
info: "nip44-v2".data(using: .utf8)!,
|
||||||
|
outputByteCount: 32
|
||||||
|
)
|
||||||
|
return derivedKey.withUnsafeBytes { Data($0) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import Foundation
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
|
/// Minimal XChaCha20-Poly1305 compatibility wrapper using CryptoKit's ChaChaPoly.
|
||||||
|
/// Implements HChaCha20 to derive a subkey and reduces the 24-byte nonce to a 12-byte nonce
|
||||||
|
/// as per XChaCha20 construction.
|
||||||
|
enum XChaCha20Poly1305Compat {
|
||||||
|
struct SealBox {
|
||||||
|
let ciphertext: Data
|
||||||
|
let tag: Data
|
||||||
|
}
|
||||||
|
|
||||||
|
static func seal(plaintext: Data, key: Data, nonce24: Data, aad: Data? = nil) throws -> SealBox {
|
||||||
|
precondition(key.count == 32, "XChaCha20 key must be 32 bytes")
|
||||||
|
precondition(nonce24.count == 24, "XChaCha20 nonce must be 24 bytes")
|
||||||
|
|
||||||
|
let subkey = hchacha20(key: key, nonce16: nonce24.prefix(16))
|
||||||
|
let nonce12 = derive12ByteNonce(from24: nonce24)
|
||||||
|
let chachaKey = SymmetricKey(data: subkey)
|
||||||
|
let nonce = try ChaChaPoly.Nonce(data: nonce12)
|
||||||
|
let sealed = try ChaChaPoly.seal(plaintext, using: chachaKey, nonce: nonce, authenticating: aad ?? Data())
|
||||||
|
return SealBox(ciphertext: sealed.ciphertext, tag: sealed.tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func open(ciphertext: Data, tag: Data, key: Data, nonce24: Data, aad: Data? = nil) throws -> Data {
|
||||||
|
precondition(key.count == 32, "XChaCha20 key must be 32 bytes")
|
||||||
|
precondition(nonce24.count == 24, "XChaCha20 nonce must be 24 bytes")
|
||||||
|
|
||||||
|
let subkey = hchacha20(key: key, nonce16: nonce24.prefix(16))
|
||||||
|
let nonce12 = derive12ByteNonce(from24: nonce24)
|
||||||
|
let chachaKey = SymmetricKey(data: subkey)
|
||||||
|
let box = try ChaChaPoly.SealedBox(nonce: ChaChaPoly.Nonce(data: nonce12), ciphertext: ciphertext, tag: tag)
|
||||||
|
return try ChaChaPoly.open(box, using: chachaKey, authenticating: aad ?? Data())
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Internals
|
||||||
|
|
||||||
|
private static func derive12ByteNonce(from24 nonce24: Data) -> Data {
|
||||||
|
// XChaCha20-Poly1305: 12-byte nonce = 4 zero bytes || last 8 bytes of the 24-byte nonce
|
||||||
|
var out = Data(count: 12)
|
||||||
|
out.replaceSubrange(0..<4, with: [0, 0, 0, 0])
|
||||||
|
out.replaceSubrange(4..<12, with: nonce24.suffix(8))
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func hchacha20(key: Data, nonce16: Data) -> Data {
|
||||||
|
// HChaCha20 based on the original ChaCha20 core with a 16-byte nonce.
|
||||||
|
precondition(key.count == 32)
|
||||||
|
precondition(nonce16.count == 16)
|
||||||
|
|
||||||
|
// Constants "expand 32-byte k"
|
||||||
|
var state: [UInt32] = [
|
||||||
|
0x61707865, 0x3320646e, 0x79622d32, 0x6b206574,
|
||||||
|
// key (8 words)
|
||||||
|
key.loadLEWord(0), key.loadLEWord(4), key.loadLEWord(8), key.loadLEWord(12),
|
||||||
|
key.loadLEWord(16), key.loadLEWord(20), key.loadLEWord(24), key.loadLEWord(28),
|
||||||
|
// nonce (4 words)
|
||||||
|
nonce16.loadLEWord(0), nonce16.loadLEWord(4), nonce16.loadLEWord(8), nonce16.loadLEWord(12)
|
||||||
|
]
|
||||||
|
|
||||||
|
// 20 rounds (10 double rounds)
|
||||||
|
for _ in 0..<10 {
|
||||||
|
// Column rounds
|
||||||
|
quarterRound(&state, 0, 4, 8, 12)
|
||||||
|
quarterRound(&state, 1, 5, 9, 13)
|
||||||
|
quarterRound(&state, 2, 6, 10, 14)
|
||||||
|
quarterRound(&state, 3, 7, 11, 15)
|
||||||
|
// Diagonal rounds
|
||||||
|
quarterRound(&state, 0, 5, 10, 15)
|
||||||
|
quarterRound(&state, 1, 6, 11, 12)
|
||||||
|
quarterRound(&state, 2, 7, 8, 13)
|
||||||
|
quarterRound(&state, 3, 4, 9, 14)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output subkey: state[0..3] and state[12..15]
|
||||||
|
var out = Data(count: 32)
|
||||||
|
out.storeLEWord(state[0], at: 0)
|
||||||
|
out.storeLEWord(state[1], at: 4)
|
||||||
|
out.storeLEWord(state[2], at: 8)
|
||||||
|
out.storeLEWord(state[3], at: 12)
|
||||||
|
out.storeLEWord(state[12], at: 16)
|
||||||
|
out.storeLEWord(state[13], at: 20)
|
||||||
|
out.storeLEWord(state[14], at: 24)
|
||||||
|
out.storeLEWord(state[15], at: 28)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func quarterRound(_ s: inout [UInt32], _ a: Int, _ b: Int, _ c: Int, _ d: Int) {
|
||||||
|
s[a] = s[a] &+ s[b]; s[d] ^= s[a]; s[d] = (s[d] << 16) | (s[d] >> 16)
|
||||||
|
s[c] = s[c] &+ s[d]; s[b] ^= s[c]; s[b] = (s[b] << 12) | (s[b] >> 20)
|
||||||
|
s[a] = s[a] &+ s[b]; s[d] ^= s[a]; s[d] = (s[d] << 8) | (s[d] >> 24)
|
||||||
|
s[c] = s[c] &+ s[d]; s[b] ^= s[c]; s[b] = (s[b] << 7) | (s[b] >> 25)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Data {
|
||||||
|
func loadLEWord(_ offset: Int) -> UInt32 {
|
||||||
|
let range = offset..<(offset+4)
|
||||||
|
let bytes = self[range]
|
||||||
|
return bytes.withUnsafeBytes { ptr -> UInt32 in
|
||||||
|
let b = ptr.bindMemory(to: UInt8.self)
|
||||||
|
return UInt32(b[0]) | (UInt32(b[1]) << 8) | (UInt32(b[2]) << 16) | (UInt32(b[3]) << 24)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func storeLEWord(_ value: UInt32, at offset: Int) {
|
||||||
|
let bytes: [UInt8] = [
|
||||||
|
UInt8(value & 0xff),
|
||||||
|
UInt8((value >> 8) & 0xff),
|
||||||
|
UInt8((value >> 16) & 0xff),
|
||||||
|
UInt8((value >> 24) & 0xff)
|
||||||
|
]
|
||||||
|
replaceSubrange(offset..<(offset+4), with: bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user