Skip to content

Commit bd78cb9

Browse files
author
christophhagen
committed
Ensure compatibility with Apple CryptoKit
1 parent 8a436cc commit bd78cb9

5 files changed

Lines changed: 225 additions & 34 deletions

File tree

Sources/CryptoKit25519/Curve25519.swift

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,19 @@ public enum Curve25519 {
1717
- Throws: `CryptoKitError.noRandomnessSource`, `CryptoKitError.noRandomnessAvailable`
1818
- Returns: 32 new random bytes.
1919
*/
20-
static func newNormalizedKey() throws -> [UInt8] {
21-
var data = try Randomness.randomBytes(count: keyLength).bytes
22-
23-
data[0] &= 0xf8
24-
data[31] &= 0x3f
25-
data[31] |= 0x40
20+
static func newKey() throws -> [UInt8] {
21+
return try Randomness.randomBytes(count: keyLength).bytes
22+
}
23+
}
24+
25+
extension Array where Element == UInt8 {
26+
27+
var normalized: [Element] {
28+
var data = self
2629

30+
data[0] &= 248
31+
data[31] &= 63
32+
data[31] |= 64
2733
return data
2834
}
2935
}

Sources/CryptoKit25519/Encryption/GCM.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public extension AES {
3434

3535
let ciphertext: [UInt8]
3636
do {
37-
let cryptor = try CryptoSwift.AES(key: key.bytes, blockMode: gcm, padding: .pkcs7)
37+
let cryptor = try CryptoSwift.AES(key: key.bytes, blockMode: gcm, padding: .noPadding)
3838
ciphertext = try cryptor.encrypt(message.bytes)
3939
} catch {
4040
throw CryptoKitError.encryptionFailed
@@ -60,7 +60,7 @@ public extension AES {
6060
additionalAuthenticatedData: authenticatedData?.bytes)
6161
let plaintext: [UInt8]
6262
do {
63-
let cryptor = try CryptoSwift.AES(key: key.bytes, blockMode: gcm, padding: .pkcs7)
63+
let cryptor = try CryptoSwift.AES(key: key.bytes, blockMode: gcm, padding: .noPadding)
6464
plaintext = try cryptor.decrypt(sealedBox.ciphertext.bytes)
6565
} catch {
6666
throw CryptoKitError.decryptionFailed

Sources/CryptoKit25519/KeyAgreement/PrivateKey.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public extension Curve25519.KeyAgreement {
2424
- Throws: `CryptoKitError.noRandomnessSource`, `CryptoKitError.noRandomnessAvailable`
2525
*/
2626
public init() throws {
27-
self.bytes = try Curve25519.newNormalizedKey()
27+
self.bytes = try Curve25519.newKey().normalized
2828
}
2929

3030
/**
@@ -50,7 +50,7 @@ public extension Curve25519.KeyAgreement {
5050
var pubBuffer = [UInt8](repeating: 0, count: Curve25519.keyLength)
5151

5252
let _: Int32 = pubBuffer.withUnsafeMutableBytes { keyPtr in
53-
bytes.withUnsafeBytes { privPtr in
53+
bytes.normalized.withUnsafeBytes { privPtr in
5454
Curve25519.KeyAgreement.PrivateKey.basepoint.withUnsafeBytes {
5555
curve25519_donna(
5656
keyPtr.baseAddress!.assumingMemoryBound(to: UInt8.self),

Sources/CryptoKit25519/Signing/SigningPrivateKey.swift

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,21 @@ public extension Curve25519.Signing {
1313
/// A Curve25519 private key used to create cryptographic signatures.
1414
struct PrivateKey {
1515

16-
/// The key (32 bytes)
1716
private let bytes: [UInt8]
1817

18+
/// The key (32 bytes)
19+
private let privateKeyBytes: [UInt8]
20+
21+
/// The public key bytes
22+
private let publicKeyBytes: [UInt8]
23+
1924
/**
2025
Creates a random Curve25519 private key for signing.
2126
- Throws: `CryptoKitError.noRandomnessSource`, `CryptoKitError.noRandomnessAvailable`
2227
*/
2328
public init() throws {
24-
self.bytes = try Curve25519.newNormalizedKey()
29+
let seed = try Curve25519.newKey()
30+
self.init(bytes: seed)
2531
}
2632

2733
/**
@@ -33,7 +39,26 @@ public extension Curve25519.Signing {
3339
guard rawRepresentation.count == Curve25519.keyLength else {
3440
throw CryptoKitError.invalidKeyLength
3541
}
36-
self.bytes = [UInt8](rawRepresentation)
42+
self.init(bytes: Array(rawRepresentation))
43+
}
44+
45+
public init(bytes: [UInt8]) {
46+
var pub = [UInt8](repeating: 0, count: 32)
47+
var priv = [UInt8](repeating: 0, count: 32)
48+
pub.withUnsafeMutableBufferPointer { pP in
49+
priv.withUnsafeMutableBufferPointer { sP in
50+
bytes.withUnsafeBytes { s in
51+
ed25519_create_keypair(
52+
pP.baseAddress,
53+
sP.baseAddress,
54+
s.bindMemory(to: UInt8.self).baseAddress)
55+
}
56+
}
57+
}
58+
59+
self.bytes = bytes
60+
self.privateKeyBytes = priv
61+
self.publicKeyBytes = pub
3762
}
3863

3964
/// The corresponding public key.
@@ -42,31 +67,18 @@ public extension Curve25519.Signing {
4267
return PublicKey(bytes: publicKeyBytes)
4368
}
4469

45-
/// The raw bytes of the corresponding public key.
46-
private var publicKeyBytes: [UInt8] {
47-
var pubBuffer = [UInt8](repeating: 0, count: Curve25519.keyLength)
48-
49-
bytes.withUnsafeBufferPointer { priv in
50-
pubBuffer.withUnsafeMutableBufferPointer { pub in
51-
ed25519_create_public_key(pub.baseAddress, priv.baseAddress)
52-
}
53-
}
54-
return pubBuffer
55-
}
56-
5770
/**
5871
Generates an EdDSA signature over Curve25519.
5972
- Parameter data: The data to sign.
6073
- Returns: The signature for the data.
6174
*/
6275
public func signature(for data: Data) -> Data {
6376
var signature = [UInt8](repeating: 0, count: 64)
64-
let publicKey = publicKeyBytes
6577

66-
signature.withUnsafeMutableBufferPointer { signature in
67-
publicKey.withUnsafeBufferPointer { pub in
68-
data.withUnsafeBytes { msg in
69-
bytes.withUnsafeBufferPointer { priv in
78+
privateKeyBytes.withUnsafeBufferPointer { priv in
79+
signature.withUnsafeMutableBufferPointer { signature in
80+
publicKeyBytes.withUnsafeBufferPointer { pub in
81+
data.withUnsafeBytes { msg in
7082
ed25519_sign(signature.baseAddress,
7183
msg.bindMemory(to: UInt8.self).baseAddress,
7284
data.count,
Lines changed: 177 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,186 @@
11
import XCTest
22
@testable import CryptoKit25519
3+
import CryptoKit
4+
import CEd25519
35

6+
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, macCatalyst 13.0, *)
47
final class CryptoKit25519Tests: XCTestCase {
58

6-
func testSignature() throws {
9+
static var allTests = [
10+
("testKeyAgreementPublicKey", testKeyAgreementPublicKey),
11+
("testSigningPublicKey", testSigningPublicKey),
12+
("testVerifySignature", testVerifySignature),
13+
("testSignatureCompatibility", testSignatureCompatibility),
14+
("testKeyAgreement", testKeyAgreement)
15+
]
16+
17+
func testKeyAgreementPublicKey() throws {
18+
// Noise protocol test vector
19+
let sk1 = Data(base64Encoded: "S51mhgw53jFJK9s7CQUnv2bvHqdfEFu2+HMo37uf4zc=")!
20+
let pk1 = Data(base64Encoded: "Ht4jMICpMF9liuztB+8EztNwtfG7oJmzq8Oex7T1qD8=")!
21+
// Check CryptoKit
22+
let key1 = try CryptoKit.Curve25519.KeyAgreement.PrivateKey(rawRepresentation: sk1)
23+
XCTAssertEqual(key1.publicKey.rawRepresentation, pk1)
24+
// Check CryptoKit25519
25+
let key2 = try CryptoKit25519.Curve25519.KeyAgreement.PrivateKey(rawRepresentation: sk1)
26+
XCTAssertEqual(key2.publicKey.rawRepresentation, pk1)
27+
28+
// Curve25519 test vector
29+
let sk2 = Data(base64Encoded: "yAZDncnSxHb/7Y8lgMCIjVirQGv3rjaYh5AhuWu0v1k=")!
30+
let pk2 = Data(base64Encoded: "G7dZZvLpOjaR3/+UK7KkZqHAi414yj9Nbfi4v6Lk7ig=")!
31+
32+
// Check CryptoKit
33+
let key3 = try CryptoKit.Curve25519.KeyAgreement.PrivateKey(rawRepresentation: sk2)
34+
XCTAssertEqual(key3.publicKey.rawRepresentation, pk2)
35+
36+
// Check CryptoKit25519
37+
let key4 = try CryptoKit25519.Curve25519.KeyAgreement.PrivateKey(rawRepresentation: sk2)
38+
XCTAssertEqual(key4.publicKey.rawRepresentation, pk2)
39+
}
40+
41+
func testSigningPublicKey() throws {
42+
// Test a few iterations
43+
//for _ in 0..<10 {
44+
let sk = Data(base64Encoded: "nWGxne/9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A=")!
45+
let pk = Data(base64Encoded: "11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=")!
46+
47+
// Generate private key through CryptoKit
48+
let key1 = try CryptoKit.Curve25519.Signing.PrivateKey(rawRepresentation: sk)
49+
XCTAssertEqual(key1.publicKey.rawRepresentation, pk)
50+
51+
// Compare with CryptoKit25519
52+
let key2 = try CryptoKit25519.Curve25519.Signing.PrivateKey(rawRepresentation: sk)
53+
XCTAssertEqual(key2.publicKey.rawRepresentation, pk)
54+
}
55+
56+
func testVerifySignature() throws {
57+
let sk = try CryptoKit25519.Curve25519.Signing.PrivateKey()
58+
let signature = sk.signature(for: Data())
59+
let pk = sk.publicKey
60+
XCTAssertTrue(pk.isValidSignature(signature, for: Data()))
61+
}
62+
63+
func testSignatureCompatibility() throws {
64+
let sk = Data(base64Encoded: "nWGxne/9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A=")!
65+
66+
let key1 = try CryptoKit.Curve25519.Signing.PrivateKey(rawRepresentation: sk)
67+
let key2 = try CryptoKit25519.Curve25519.Signing.PrivateKey(rawRepresentation: sk)
68+
69+
let signature1 = try key1.signature(for: Data())
70+
XCTAssertTrue(key1.publicKey.isValidSignature(signature1, for: Data()))
71+
XCTAssertTrue(key2.publicKey.isValidSignature(signature1, for: Data()))
72+
73+
let signature2 = key2.signature(for: Data())
74+
XCTAssertTrue(key1.publicKey.isValidSignature(signature2, for: Data()))
75+
XCTAssertTrue(key2.publicKey.isValidSignature(signature2, for: Data()))
76+
}
77+
78+
func testKeyAgreement() throws {
79+
let salt = "Salt".data(using: .utf8)!
80+
let sharedInfo = "Info".data(using: .utf8)!
81+
let keyA = CryptoKit.Curve25519.KeyAgreement.PrivateKey()
82+
let keyB = try CryptoKit25519.Curve25519.KeyAgreement.PrivateKey()
83+
84+
// Calculate key with CryptoKit
85+
let pubB = try CryptoKit.Curve25519.KeyAgreement.PublicKey(rawRepresentation: keyB.publicKey.rawRepresentation)
86+
let s1 = try keyA.sharedSecretFromKeyAgreement(with: pubB)
87+
let k1 = s1.hkdfDerivedSymmetricKey(
88+
using: SHA256.self, salt: salt, sharedInfo: sharedInfo, outputByteCount: 32)
89+
90+
let pubA = try CryptoKit25519.Curve25519.KeyAgreement.PublicKey(rawRepresentation: keyA.publicKey.rawRepresentation)
91+
let s2 = try keyB.sharedSecretFromKeyAgreement(with: pubA)
92+
let k2 = try s2.hkdfDerivedSymmetricKey(
93+
using: .sha256, salt: salt, sharedInfo: sharedInfo, outputByteCount: 32)
94+
95+
XCTAssertEqual(k1.rawBytes, k2.rawBytes)
96+
}
97+
98+
func testEncrypt() throws {
99+
let message = "Hi there".data(using: .utf8)!
100+
101+
let key1 = CryptoKit.SymmetricKey(size: .bits256)
102+
let key2 = CryptoKit25519.SymmetricKey(data: key1.rawBytes)
7103

104+
let nonce1 = CryptoKit.AES.GCM.Nonce()
105+
let nonce2 = try CryptoKit25519.AES.GCM.Nonce(data: nonce1.rawRepresentation)
106+
107+
// Encrypt the data
108+
let encrypted1 = try CryptoKit.AES.GCM.seal(message, using: key1, nonce: nonce1)
109+
let encrypted2 = try CryptoKit25519.AES.GCM.seal(message, using: key2, nonce: nonce2)
110+
XCTAssertEqual(encrypted1.ciphertext, encrypted2.ciphertext)
111+
XCTAssertEqual(encrypted1.nonce.rawRepresentation, nonce1.rawRepresentation)
112+
XCTAssertEqual(encrypted1.tag, encrypted2.tag)
113+
XCTAssertEqual(encrypted1.combined!, encrypted2.combined)
114+
115+
// Decrypt the data
116+
let box1 = try CryptoKit.AES.GCM.SealedBox(combined: encrypted2.combined)
117+
let box2 = try CryptoKit25519.AES.GCM.SealedBox(combined: encrypted1.combined!)
118+
119+
let decrypted1 = try CryptoKit.AES.GCM.open(box1, using: key1)
120+
let decrypted2 = try CryptoKit25519.AES.GCM.open(box2, using: key2)
121+
122+
XCTAssertEqual(decrypted1, message)
123+
XCTAssertEqual(decrypted2, message)
8124
}
125+
}
9126

10-
static var allTests = [
11-
("testSignature", testSignature),
12-
]
127+
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, macCatalyst 13.0, *)
128+
extension CryptoKit.SymmetricKey {
129+
130+
var rawBytes: Data {
131+
withUnsafeBytes { Data(Array($0)) }
132+
}
13133
}
134+
135+
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, macCatalyst 13.0, *)
136+
extension CryptoKit.AES.GCM.Nonce {
137+
138+
var rawRepresentation: Data {
139+
self.withUnsafeBytes { Data(Array($0)) }
140+
}
141+
}
142+
143+
extension Data {
144+
145+
public func hexEncodedString() -> String {
146+
return map { String(format: "%02hhx", $0) }.joined()
147+
}
148+
149+
// Convert 0 ... 9, a ... f, A ...F to their decimal value,
150+
// return nil for all other input characters
151+
private func decodeNibble(_ u: UInt16) -> UInt8? {
152+
switch(u) {
153+
case 0x30 ... 0x39:
154+
return UInt8(u - 0x30)
155+
case 0x41 ... 0x46:
156+
return UInt8(u - 0x41 + 10)
157+
case 0x61 ... 0x66:
158+
return UInt8(u - 0x61 + 10)
159+
default:
160+
return nil
161+
}
162+
}
163+
164+
public init?(hexEncoded string: String) {
165+
var str = string
166+
if str.count%2 != 0 {
167+
// insert 0 to get even number of chars
168+
str.insert("0", at: str.startIndex)
169+
}
170+
171+
let utf16 = str.utf16
172+
self.init(capacity: utf16.count/2)
173+
174+
var i = utf16.startIndex
175+
while i != str.utf16.endIndex {
176+
guard let hi = decodeNibble(utf16[i]),
177+
let lo = decodeNibble(utf16[utf16.index(i, offsetBy: 1, limitedBy: utf16.endIndex)!]) else {
178+
return nil
179+
}
180+
var value = hi << 4 + lo
181+
self.append(&value, count: 1)
182+
i = utf16.index(i, offsetBy: 2, limitedBy: utf16.endIndex)!
183+
}
184+
}
185+
}
186+

0 commit comments

Comments
 (0)